aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries
diff options
context:
space:
mode:
authorJan Gelety <jgelety@cisco.com>2018-09-25 15:41:10 +0200
committerJan Gelety <jgelety@cisco.com>2018-11-27 14:58:18 +0000
commit287406e3097d8409bcf12cba3eb3304f91857e90 (patch)
tree2ef5a640045cf806ac7663da8557177cd66ed54b /resources/libraries
parent9bc5017b07c7a779af9bff0360d022388adbf2d1 (diff)
CSIT python API introduction
Jira: CSIT-1336 Change-Id: I96d2b0221c5a7466484a82339fc132c5921532d1 Signed-off-by: Jan Gelety <jgelety@cisco.com>
Diffstat (limited to 'resources/libraries')
-rw-r--r--resources/libraries/bash/function/artifacts.sh4
-rw-r--r--resources/libraries/python/PapiErrors.py42
-rw-r--r--resources/libraries/python/PapiExecutor.py223
-rw-r--r--resources/libraries/python/VPPUtil.py64
-rw-r--r--resources/libraries/python/constants.py3
5 files changed, 324 insertions, 12 deletions
diff --git a/resources/libraries/bash/function/artifacts.sh b/resources/libraries/bash/function/artifacts.sh
index abb0b5f428..6695b4d977 100644
--- a/resources/libraries/bash/function/artifacts.sh
+++ b/resources/libraries/bash/function/artifacts.sh
@@ -64,7 +64,7 @@ function download_ubuntu_artifacts () {
}
# If version is set we will add suffix.
artifacts=()
- vpp=(vpp vpp-dbg vpp-dev vpp-lib vpp-plugins)
+ vpp=(vpp vpp-dbg vpp-dev vpp-lib vpp-plugins vpp-api-python)
if [ -z "${VPP_VERSION-}" ]; then
artifacts+=(${vpp[@]})
else
@@ -97,7 +97,7 @@ function download_centos_artifacts () {
}
# If version is set we will add suffix.
artifacts=()
- vpp=(vpp vpp-selinux-policy vpp-devel vpp-lib vpp-plugins)
+ vpp=(vpp vpp-selinux-policy vpp-devel vpp-lib vpp-plugins vpp-api-python)
if [ -z "${VPP_VERSION-}" ]; then
artifacts+=(${vpp[@]})
else
diff --git a/resources/libraries/python/PapiErrors.py b/resources/libraries/python/PapiErrors.py
new file mode 100644
index 0000000000..5afebbf2ce
--- /dev/null
+++ b/resources/libraries/python/PapiErrors.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2018 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.
+
+"""PAPI Errors class file."""
+
+__all__ = ['PapiError', 'PapiInitError', 'PapiJsonFileError',
+ 'PapiCommandError', 'PapiCommandInputError']
+
+
+class PapiError(Exception):
+ """Python API error."""
+ pass
+
+
+class PapiInitError(PapiError):
+ """This exception is raised when construction of VPP instance failed."""
+ pass
+
+
+class PapiJsonFileError(PapiError):
+ """This exception is raised in case of JSON API file error."""
+ pass
+
+
+class PapiCommandError(PapiError):
+ """This exception is raised when PAPI command(s) execution failed."""
+ pass
+
+
+class PapiCommandInputError(PapiCommandError):
+ """This exception is raised when incorrect input of Python API is used."""
+ pass
diff --git a/resources/libraries/python/PapiExecutor.py b/resources/libraries/python/PapiExecutor.py
new file mode 100644
index 0000000000..6a47b9497f
--- /dev/null
+++ b/resources/libraries/python/PapiExecutor.py
@@ -0,0 +1,223 @@
+# Copyright (c) 2018 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 paramiko.ssh_exception import SSHException
+from robot.api import logger
+
+from resources.libraries.python.constants import Constants
+from resources.libraries.python.PapiErrors import PapiInitError, \
+ PapiJsonFileError, PapiCommandError, PapiCommandInputError
+# TODO: from resources.libraries.python.PapiHistory import PapiHistory
+from resources.libraries.python.ssh import SSH, SSHTimeout
+
+__all__ = ['PapiExecutor']
+
+
+class PapiExecutor(object):
+ """Contains methods for executing Python API commands on DUTs."""
+
+ def __init__(self, node):
+ self._stdout = None
+ self._stderr = None
+ self._ret_code = None
+ self._node = node
+ self._json_data = None
+ self._api_reply = list()
+ self._api_data = None
+
+ self._ssh = SSH()
+ try:
+ self._ssh.connect(node)
+ except:
+ raise SSHException('Cannot open SSH connection to host {host} to '
+ 'execute PAPI command(s)'.
+ format(host=self._node['host']))
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+ @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_name = api['api_name']
+ api_args = api['api_args']
+ api_processed = dict(api_name=api_name)
+ api_args_processed = dict()
+ for a_k, a_v in api_args.iteritems():
+ value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
+ api_args_processed[str(a_k)] = value
+ api_processed['api_args'] = api_args_processed
+ api_data_processed.append(api_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.
+
+ :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_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 = list()
+ for a_r in api_reply:
+ reverted_reply.append(self._revert_api_reply(a_r))
+ else:
+ reverted_reply = self._revert_api_reply(api_reply)
+ return reverted_reply
+
+ def _process_json_data(self):
+ """Process received JSON data."""
+
+ for data in self._json_data:
+ api_name = data['api_name']
+ api_reply = data['api_reply']
+ api_reply_processed = dict(
+ api_name=api_name, api_reply=self._process_reply(api_reply))
+ self._api_reply.append(api_reply_processed)
+
+ 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 is timed out.
+ :raises PapiInitError: If PAPI initialization failed.
+ :raises PapiJsonFileError: If no api.json file found.
+ :raises PapiCommandError: If PAPI command(s) execution failed.
+ :raises PapiCommandInputError: If invalid attribute name or invalid
+ value is used in API call.
+ :raises RuntimeError: If PAPI executor failed due to another reason.
+ """
+ self._api_data = api_data
+ 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=self._api_data))
+ raise
+ except (PapiInitError, PapiJsonFileError, PapiCommandError,
+ PapiCommandInputError):
+ logger.error('PAPI command(s) execution failed on host {host}'.
+ format(host=self._node['host']))
+ raise
+ except:
+ raise RuntimeError('PAPI command(s) execution on host {host} '
+ 'failed: {apis}'.format(host=self._node['host'],
+ apis=self._api_data))
+
+ self._ret_code = ret_code
+ self._stdout = stdout
+ self._stderr = stderr
+
+ def papi_should_have_failed(self):
+ """Read return code from last executed script and raise exception if the
+ PAPI command(s) didn't fail.
+
+ :raises RuntimeError: When no PAPI command executed.
+ :raises AssertionError: If PAPI command(s) execution passed.
+ """
+
+ if self._ret_code is None:
+ raise RuntimeError("First execute the PAPI command(s)!")
+ if self._ret_code == 0:
+ raise AssertionError(
+ "PAPI command(s) execution passed, but failure was expected: "
+ "{apis}".format(apis=self._api_data))
+
+ def papi_should_have_passed(self):
+ """Read return code from last executed script and raise exception if the
+ PAPI command(s) failed.
+
+ :raises RuntimeError: When no PAPI command executed.
+ :raises AssertionError: If PAPI command(s) execution failed.
+ """
+
+ if self._ret_code is None:
+ raise RuntimeError("First execute the PAPI command(s)!")
+ if self._ret_code != 0:
+ raise AssertionError(
+ "PAPI command(s) execution failed, but success was expected: "
+ "{apis}".format(apis=self._api_data))
+
+ def get_papi_stdout(self):
+ """Returns value of stdout from last executed PAPI command(s)."""
+
+ return self._stdout
+
+ def get_papi_stderr(self):
+ """Returns value of stderr from last executed PAPI command(s)."""
+
+ return self._stderr
+
+ def get_papi_reply(self):
+ """Returns api reply from last executed PAPI command(s)."""
+
+ self._json_data = json.loads(self._stdout)
+ self._process_json_data()
+
+ return self._api_reply
diff --git a/resources/libraries/python/VPPUtil.py b/resources/libraries/python/VPPUtil.py
index 3714d3b780..d6c02a3e8a 100644
--- a/resources/libraries/python/VPPUtil.py
+++ b/resources/libraries/python/VPPUtil.py
@@ -15,8 +15,12 @@
import time
+from robot.api import logger
+
from resources.libraries.python.constants import Constants
from resources.libraries.python.DUTSetup import DUTSetup
+from resources.libraries.python.PapiExecutor import PapiExecutor
+from resources.libraries.python.PapiErrors import PapiError
from resources.libraries.python.ssh import exec_cmd, exec_cmd_no_error
from resources.libraries.python.topology import NodeType
from resources.libraries.python.VatExecutor import VatExecutor
@@ -135,24 +139,64 @@ class VPPUtil(object):
VPPUtil.verify_vpp_on_dut(node)
@staticmethod
- def vpp_show_version_verbose(node):
- """Run "show version verbose" CLI command.
+ def vpp_show_version(node, verbose=False):
+ """Run "show_version" API command.
:param node: Node to run command on.
+ :param verbose: Show version, compile date and compile location if True
+ otherwise show only version.
:type node: dict
+ :type verbose: bool
+ :raises PapiError: If no reply received for show_version API command.
"""
- vat = VatExecutor()
- vat.execute_script("show_version_verbose.vat", node, json_out=False)
+ # TODO: move composition of api data to separate method
+ api_data = list()
+ api = dict(api_name='show_version')
+ api_args = dict()
+ api['api_args'] = api_args
+ api_data.append(api)
+
+ api_reply = None
+ with PapiExecutor(node) as papi_executor:
+ papi_executor.execute_papi(api_data)
+ try:
+ papi_executor.papi_should_have_passed()
+ except AssertionError:
+ raise RuntimeError('Failed to get VPP version on host: {host}'.
+ format(host=node['host']))
+ api_reply = papi_executor.get_papi_reply()
+
+ if api_reply is not None:
+ version_data = api_reply[0]['api_reply']['show_version_reply']
+ ver = version_data['version'].rstrip('\0x00')
+ if verbose:
+ date = version_data['build_date'].rstrip('\0x00')
+ loc = version_data['build_directory'].rstrip('\0x00')
+ version = \
+ 'VPP Version: {ver}\n' \
+ 'Compile date: {date}\n' \
+ 'Compile location: {loc}\n '\
+ .format(ver=ver, date=date, loc=loc)
+ else:
+ version = 'VPP version:{ver}'.format(ver=ver)
+ logger.info(version)
+ else:
+ raise PapiError('No reply received for show_version API command on '
+ 'host {host}'.format(host=node['host']))
- try:
- vat.script_should_have_passed()
- except AssertionError:
- raise RuntimeError('Failed to get VPP version on host: {name}'.
- format(name=node['host']))
+ @staticmethod
+ def vpp_show_version_verbose(node):
+ """Run "show_version" API command and return verbose string of version
+ data.
+
+ :param node: Node to run command on.
+ :type node: dict
+ """
+ VPPUtil.vpp_show_version(node, verbose=True)
@staticmethod
def show_vpp_version_on_all_duts(nodes):
- """Show VPP version verbose on all DUTs.
+ """Show VPP version on all DUTs.
:param nodes: VPP nodes.
:type nodes: dict
diff --git a/resources/libraries/python/constants.py b/resources/libraries/python/constants.py
index a0a427af4e..43fbf1a76d 100644
--- a/resources/libraries/python/constants.py
+++ b/resources/libraries/python/constants.py
@@ -23,6 +23,9 @@ class Constants(object):
# shell scripts location
RESOURCES_LIB_SH = 'resources/libraries/bash'
+ # Python API provider location
+ RESOURCES_PAPI_PROVIDER = 'resources/tools/papi/vpp_papi_provider.py'
+
# vat templates location
RESOURCES_TPL_VAT = 'resources/templates/vat'