aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python/HoststackUtil.py
AgeCommit message (Expand)AuthorFilesLines
2020-01-29perf: QUIC transport hoststack test suiteDave Wallace1-4/+16
2020-01-29IPSEC: Change plugin namingPeter Mikus1-1/+1
2020-01-28perf: hoststack iperf3 test tuningDave Wallace1-6/+8
2020-01-17perf: add TCP Iperf3+LDPRELOAD test suiteDave Wallace1-0/+44
2020-01-15Hoststack perf infrastructure refactoringDave Wallace1-0/+287
>128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
# Copyright (c) 2019 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Python API executor library."""

import binascii
import json

from robot.api import logger

from resources.libraries.python.Constants import Constants
from resources.libraries.python.ssh import SSH, SSHTimeout
from resources.libraries.python.PapiHistory import PapiHistory

__all__ = ["PapiExecutor", "PapiResponse"]


class PapiResponse(object):
    """Class for metadata specifying the Papi reply, stdout, stderr and return
    code.
    """

    def __init__(self, papi_reply=None, stdout="", stderr="", ret_code=None):
        """Construct the Papi response by setting the values needed.

        :param papi_reply: API reply from last executed PAPI command(s).
        :param stdout: stdout from last executed PAPI command(s).
        :param stderr: stderr from last executed PAPI command(s).
        :param ret_code: ret_code from last executed PAPI command(s).
        :type papi_reply: list
        :type stdout: str
        :type stderr: str
        :type ret_code: int
        """

        # API reply from last executed PAPI command(s)
        self.reply = papi_reply

        # stdout from last executed PAPI command(s)
        self.stdout = stdout

        # stderr from last executed PAPI command(s).
        self.stderr = stderr

        # return code from last executed PAPI command(s)
        self.ret_code = ret_code

    def __str__(self):
        """Return string with human readable description of the group.

        :returns: Readable description.
        :rtype: str
        """
        return ("papi_reply={papi_reply} "
                "stdout={stdout} "
                "stderr={stderr} "
                "ret_code={ret_code}".
                format(papi_reply=self.reply,
                       stdout=self.stdout,
                       stderr=self.stderr,
                       ret_code=self.ret_code))

    def __repr__(self):
        """Return string executable as Python constructor call.

        :returns: Executable constructor call.
        :rtype: str
        """
        return ("PapiResponse(papi_reply={papi_reply} "
                "stdout={stdout} "
                "stderr={stderr} "
                "ret_code={ret_code})".
                format(papi_reply=self.reply,
                       stdout=self.stdout,
                       stderr=self.stderr,
                       ret_code=self.ret_code))


class PapiExecutor(object):
    """Contains methods for executing Python API commands on DUTs.

    Use only with "with" statement, e.g.:

    with PapiExecutor(node) as papi_exec:
        papi_resp = papi_exec.add('show_version').execute_should_pass(err_msg)
    """

    def __init__(self, node):
        """Initialization.

        :param node: Node to run command(s) on.
        :type node: dict
        """

        # Node to run command(s) on.
        self._node = node

        # The list of PAPI commands to be executed on the node.
        self._api_command_list = list()

        # The response on the PAPI commands.
        self.response = PapiResponse()

        self._ssh = SSH()

    def __enter__(self):
        try:
            self._ssh.connect(self._node)
        except IOError:
            raise RuntimeError("Cannot open SSH connection to host {host} to "
                               "execute PAPI command(s)".
                               format(host=self._node["host"]))
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._ssh.disconnect(self._node)

    def clear(self):
        """Empty the internal command list; return self.

        Use when not sure whether previous usage has left something in the list.

        :returns: self, so that method chaining is possible.
        :rtype: PapiExecutor
        """
        self._api_command_list = list()
        return self

    def add(self, command, **kwargs):
        """Add next command to internal command list; return self.

        :param command: VPP API command.
        :param kwargs: Optional key-value arguments.
        :type command: str
        :type kwargs: dict
        :returns: self, so that method chaining is possible.
        :rtype: PapiExecutor
        """
        PapiHistory.add_to_papi_history(self._node, command, **kwargs)
        self._api_command_list.append(dict(api_name=command, api_args=kwargs))
        return self

    def execute(self, process_reply=True, ignore_errors=False, timeout=120):
        """Turn internal command list into proper data and execute; return
        PAPI response.

        This method also clears the internal command list.

        :param process_reply: Process PAPI reply if True.
        :param ignore_errors: If true, the errors in the reply are ignored.
        :param timeout: Timeout in seconds.
        :type process_reply: bool
        :type ignore_errors: bool
        :type timeout: int
        :returns: Papi response including: papi reply, stdout, stderr and
            return code.
        :rtype: PapiResponse
        :raises KeyError: If the reply is not correct.
        """

        local_list = self._api_command_list

        # Clear first as execution may fail.
        self.clear()

        ret_code, stdout, stderr = self._execute_papi(local_list, timeout)

        papi_reply = list()
        if process_reply:
            json_data = json.loads(stdout)
            for data in json_data:
                try:
                    api_reply_processed = dict(
                        api_name=data["api_name"],
                        api_reply=self._process_reply(data["api_reply"]))
                except KeyError:
                    if ignore_errors:
                        continue
                    else:
                        raise
                papi_reply.append(api_reply_processed)

        return PapiResponse(papi_reply=papi_reply,
                            stdout=stdout,
                            stderr=stderr,
                            ret_code=ret_code)

    def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
                            process_reply=True, ignore_errors=False,
                            timeout=120):
        """Execute the PAPI commands and check the return code.
        Raise exception if the PAPI command(s) failed.

        Note: There are two exceptions raised to distinguish two situations. If
        not needed, re-implement using only RuntimeError.

        :param err_msg: The message used if the PAPI command(s) execution fails.
        :param process_reply: Indicate whether or not to process PAPI reply.
        :param ignore_errors: If true, the errors in the reply are ignored.
        :param timeout: Timeout in seconds.
        :type err_msg: str
        :type process_reply: bool
        :type ignore_errors: bool
        :type timeout: int
        :returns: Papi response including: papi reply, stdout, stderr and
            return code.
        :rtype: PapiResponse
        :raises RuntimeError: If no PAPI command(s) executed.
        :raises AssertionError: If PAPI command(s) execution passed.
        """

        response = self.execute(process_reply=process_reply,
                                ignore_errors=ignore_errors,
                                timeout=timeout)

        if response.ret_code != 0:
            raise AssertionError(err_msg)
        return response

    def execute_should_fail(self,
                            err_msg="Execution of PAPI command did not fail.",
                            process_reply=False, ignore_errors=False,
                            timeout=120):
        """Execute the PAPI commands and check the return code.
        Raise exception if the PAPI command(s) did not fail.

        It does not return anything as we expect it fails.

        Note: There are two exceptions raised to distinguish two situations. If
        not needed, re-implement using only RuntimeError.

        :param err_msg: The message used if the PAPI command(s) execution fails.
        :param process_reply: Indicate whether or not to process PAPI reply.
        :param ignore_errors: If true, the errors in the reply are ignored.
        :param timeout: Timeout in seconds.
        :type err_msg: str
        :type process_reply: bool
        :type ignore_errors: bool
        :type timeout: int
        :raises RuntimeError: If no PAPI command(s) executed.
        :raises AssertionError: If PAPI command(s) execution passed.
        """

        response = self.execute(process_reply=process_reply,
                                ignore_errors=ignore_errors,
                                timeout=timeout)

        if response.ret_code == 0:
            raise AssertionError(err_msg)

    @staticmethod
    def _process_api_data(api_d):
        """Process API data for smooth converting to JSON string.

        Apply binascii.hexlify() method for string values.

        :param api_d: List of APIs with their arguments.
        :type api_d: list
        :returns: List of APIs with arguments pre-processed for JSON.
        :rtype: list
        """

        api_data_processed = list()
        for api in api_d:
            api_args_processed = dict()
            for a_k, a_v in api["api_args"].iteritems():
                value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
                api_args_processed[str(a_k)] = value
            api_data_processed.append(dict(api_name=api["api_name"],
                                           api_args=api_args_processed))
        return api_data_processed

    @staticmethod
    def _revert_api_reply(api_r):
        """Process API reply / a part of API reply.

        Apply binascii.unhexlify() method for unicode values.

        TODO: Remove the disabled code when definitely not needed.

        :param api_r: API reply.
        :type api_r: dict
        :returns: Processed API reply / a part of API reply.
        :rtype: dict
        """

        reply_dict = dict()
        reply_value = dict()
        for reply_key, reply_v in api_r.iteritems():
            for a_k, a_v in reply_v.iteritems():
                # value = binascii.unhexlify(a_v) if isinstance(a_v, unicode) \
                #     else a_v
                # reply_value[a_k] = value
                reply_value[a_k] = a_v
            reply_dict[reply_key] = reply_value
        return reply_dict

    def _process_reply(self, api_reply):
        """Process API reply.

        :param api_reply: API reply.
        :type api_reply: dict or list of dict
        :returns: Processed API reply.
        :rtype: list or dict
        """

        if isinstance(api_reply, list):
            reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
        else:
            reverted_reply = self._revert_api_reply(api_reply)
        return reverted_reply

    def _execute_papi(self, api_data, timeout=120):
        """Execute PAPI command(s) on remote node and store the result.

        :param api_data: List of APIs with their arguments.
        :param timeout: Timeout in seconds.
        :type api_data: list
        :type timeout: int
        :raises SSHTimeout: If PAPI command(s) execution has timed out.
        :raises RuntimeError: If PAPI executor failed due to another reason.
        """

        if not api_data:
            RuntimeError("No API data provided.")

        api_data_processed = self._process_api_data(api_data)
        json_data = json.dumps(api_data_processed)

        cmd = "python {fw_dir}/{papi_provider} --json_data '{json}'".format(
            fw_dir=Constants.REMOTE_FW_DIR,
            papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
            json=json_data)

        try:
            ret_code, stdout, stderr = self._ssh.exec_command_sudo(
                cmd=cmd, timeout=timeout)
        except SSHTimeout:
            logger.error("PAPI command(s) execution timeout on host {host}:"
                         "\n{apis}".format(host=self._node["host"],
                                           apis=api_data))
            raise
        except Exception:
            raise RuntimeError("PAPI command(s) execution on host {host} "
                               "failed: {apis}".format(host=self._node["host"],
                                                       apis=api_data))
        return ret_code, stdout, stderr