go.ligato.io/vpp-agent/v3@v3.5.0/tests/robot/libraries/vpp_api.py (about)

     1  #!/usr/bin/env python3
     2  
     3  # Copyright (c) 2019 Cisco and/or its affiliates.
     4  # Licensed under the Apache License, Version 2.0 (the "License");
     5  # you may not use this file except in compliance with the License.
     6  # You may obtain a copy of the License at:
     7  #
     8  #     http://www.apache.org/licenses/LICENSE-2.0
     9  #
    10  # Unless required by applicable law or agreed to in writing, software
    11  # distributed under the License is distributed on an "AS IS" BASIS,
    12  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  # See the License for the specific language governing permissions and
    14  # limitations under the License.
    15  
    16  import binascii
    17  import json
    18  
    19  from paramiko import SSHClient, AutoAddPolicy
    20  
    21  from robot.api import logger
    22  
    23  CLIENT_NAME = 'ligato_papi'
    24  
    25  
    26  class vpp_api(object):
    27      @staticmethod
    28      def execute_api(host, username, password, node, command, **arguments):
    29          with PapiExecutor(host, username, password, node) as papi_exec:
    30              papi_resp = papi_exec.add(command, **arguments).get_replies()
    31  
    32          return papi_resp.reply
    33  
    34  
    35  class PapiResponse(object):
    36      """Class for metadata specifying the Papi reply, stdout, stderr and return
    37      code.
    38      """
    39  
    40      def __init__(self, papi_reply=None, stdout="", stderr="", requests=None):
    41          """Construct the Papi response by setting the values needed.
    42  
    43          :param papi_reply: API reply from last executed PAPI command(s).
    44          :param stdout: stdout from last executed PAPI command(s).
    45          :param stderr: stderr from last executed PAPI command(s).
    46          :param requests: List of used PAPI requests. It is used while verifying
    47              replies. If None, expected replies must be provided for verify_reply
    48              and verify_replies methods.
    49          :type papi_reply: list or None
    50          :type stdout: str
    51          :type stderr: str
    52          :type requests: list
    53          """
    54  
    55          # API reply from last executed PAPI command(s).
    56          self.reply = papi_reply
    57  
    58          # stdout from last executed PAPI command(s).
    59          self.stdout = stdout
    60  
    61          # stderr from last executed PAPI command(s).
    62          self.stderr = stderr
    63  
    64          # List of used PAPI requests.
    65          self.requests = requests
    66  
    67          # List of expected PAPI replies. It is used while verifying replies.
    68          if self.requests:
    69              self.expected_replies = \
    70                  ["{rqst}_reply".format(rqst=rqst) for rqst in self.requests]
    71  
    72      def __str__(self):
    73          """Return string with human readable description of the PapiResponse.
    74  
    75          :returns: Readable description.
    76          :rtype: str
    77          """
    78          return (
    79              "papi_reply={papi_reply},stdout={stdout},stderr={stderr},"
    80              "requests={requests}").format(
    81              papi_reply=self.reply, stdout=self.stdout, stderr=self.stderr,
    82              requests=self.requests)
    83  
    84      def __repr__(self):
    85          """Return string executable as Python constructor call.
    86  
    87          :returns: Executable constructor call.
    88          :rtype: str
    89          """
    90          return "PapiResponse({str})".format(str=str(self))
    91  
    92  
    93  class PapiExecutor(object):
    94      """Contains methods for executing VPP Python API commands on DUTs.
    95  
    96      Note: Use only with "with" statement, e.g.:
    97  
    98          with PapiExecutor(node) as papi_exec:
    99              papi_resp = papi_exec.add('show_version').get_replies(err_msg)
   100  
   101      This class processes three classes of VPP PAPI methods:
   102      1. simple request / reply: method='request',
   103      2. dump functions: method='dump',
   104      3. vpp-stats: method='stats'.
   105  
   106      The recommended ways of use are (examples):
   107  
   108      1. Simple request / reply
   109  
   110      a. One request with no arguments:
   111  
   112          with PapiExecutor(node) as papi_exec:
   113              data = papi_exec.add('show_version').get_replies().\
   114                  verify_reply()
   115  
   116      b. Three requests with arguments, the second and the third ones are the same
   117         but with different arguments.
   118  
   119          with PapiExecutor(node) as papi_exec:
   120              data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
   121                  add(cmd2, **args3).get_replies(err_msg).verify_replies()
   122  
   123      2. Dump functions
   124  
   125          cmd = 'sw_interface_rx_placement_dump'
   126          with PapiExecutor(node) as papi_exec:
   127              papi_resp = papi_exec.add(cmd, sw_if_index=ifc['vpp_sw_index']).\
   128                  get_dump(err_msg)
   129  
   130      3. vpp-stats
   131  
   132          path = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
   133  
   134          with PapiExecutor(node) as papi_exec:
   135              data = papi_exec.add(api_name='vpp-stats', path=path).get_stats()
   136  
   137          print('RX interface core 0, sw_if_index 0:\n{0}'.\
   138              format(data[0]['/if/rx'][0][0]))
   139  
   140          or
   141  
   142          path_1 = ['^/if', ]
   143          path_2 = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
   144  
   145          with PapiExecutor(node) as papi_exec:
   146              data = papi_exec.add('vpp-stats', path=path_1).\
   147                  add('vpp-stats', path=path_2).get_stats()
   148  
   149          print('RX interface core 0, sw_if_index 0:\n{0}'.\
   150              format(data[1]['/if/rx'][0][0]))
   151  
   152          Note: In this case, when PapiExecutor method 'add' is used:
   153          - its parameter 'csit_papi_command' is used only to keep information
   154            that vpp-stats are requested. It is not further processed but it is
   155            included in the PAPI history this way:
   156            vpp-stats(path=['^/if', '/err/ip4-input', '/sys/node/ip4-input'])
   157            Always use csit_papi_command="vpp-stats" if the VPP PAPI method
   158            is "stats".
   159          - the second parameter must be 'path' as it is used by PapiExecutor
   160            method 'add'.
   161      """
   162  
   163      def __init__(self, host, username, password, node):
   164          """Initialization.
   165          """
   166  
   167          # Node to run command(s) on.
   168          self.host = host
   169          self.node = node
   170          self.username = username
   171          self.password = password
   172  
   173          self._ssh = SSHClient()
   174          self._ssh.set_missing_host_key_policy(AutoAddPolicy())
   175  
   176          # The list of PAPI commands to be executed on the node.
   177          self._api_command_list = list()
   178  
   179      def __enter__(self):
   180          try:
   181              self._ssh.connect(self.host, username=self.username, password=self.password)
   182          except IOError:
   183              raise RuntimeError("Cannot open SSH connection to host {host} to "
   184                                 "execute PAPI command(s)".
   185                                 format(host=self.host))
   186          return self
   187  
   188      def __exit__(self, exc_type, exc_val, exc_tb):
   189          self._ssh.close()
   190  
   191      def add(self, csit_papi_command="vpp-stats", **kwargs):
   192          """Add next command to internal command list; return self.
   193  
   194          The argument name 'csit_papi_command' must be unique enough as it cannot
   195          be repeated in kwargs.
   196  
   197          :param csit_papi_command: VPP API command.
   198          :param kwargs: Optional key-value arguments.
   199          :type csit_papi_command: str
   200          :type kwargs: dict
   201          :returns: self, so that method chaining is possible.
   202          :rtype: PapiExecutor
   203          """
   204          self._api_command_list.append(dict(api_name=csit_papi_command,
   205                                             api_args=kwargs))
   206          return self
   207  
   208      def get_replies(self,
   209                      process_reply=True, ignore_errors=False, timeout=120):
   210          """Get reply/replies from VPP Python API.
   211  
   212          :param process_reply: Process PAPI reply if True.
   213          :param ignore_errors: If true, the errors in the reply are ignored.
   214          :param timeout: Timeout in seconds.
   215          :type process_reply: bool
   216          :type ignore_errors: bool
   217          :type timeout: int
   218          :returns: Papi response including: papi reply, stdout, stderr and
   219              return code.
   220          :rtype: PapiResponse
   221          """
   222          return self._execute(
   223              method='request', process_reply=process_reply,
   224              ignore_errors=ignore_errors, timeout=timeout)
   225  
   226      @staticmethod
   227      def _process_api_data(api_d):
   228          """Process API data for smooth converting to JSON string.
   229  
   230          Apply binascii.hexlify() method for string values.
   231  
   232          :param api_d: List of APIs with their arguments.
   233          :type api_d: list
   234          :returns: List of APIs with arguments pre-processed for JSON.
   235          :rtype: list
   236          """
   237  
   238          def process_value(val):
   239              """Process value.
   240  
   241              :param val: Value to be processed.
   242              :type val: object
   243              :returns: Processed value.
   244              :rtype: dict or str or int
   245              """
   246              if isinstance(val, dict):
   247                  val_dict = dict()
   248                  for val_k, val_v in val.items():
   249                      val_dict[str(val_k)] = process_value(val_v)
   250                  return val_dict
   251              else:
   252                  return binascii.hexlify(val) if isinstance(val, str) else val
   253  
   254          api_data_processed = list()
   255          for api in api_d:
   256              api_args_processed = dict()
   257              for a_k, a_v in api["api_args"].iteritems():
   258                  api_args_processed[str(a_k)] = process_value(a_v)
   259              api_data_processed.append(dict(api_name=api["api_name"],
   260                                             api_args=api_args_processed))
   261          return api_data_processed
   262  
   263      @staticmethod
   264      def _revert_api_reply(api_r):
   265          """Process API reply / a part of API reply.
   266  
   267          Apply binascii.unhexlify() method for unicode values.
   268  
   269          :param api_r: API reply.
   270          :type api_r: dict
   271          :returns: Processed API reply / a part of API reply.
   272          :rtype: dict
   273          """
   274          reply_dict = dict()
   275          reply_value = dict()
   276          for reply_key, reply_v in api_r.items():
   277              for a_k, a_v in reply_v.iteritems():
   278                  reply_value[a_k] = binascii.unhexlify(a_v) \
   279                      if isinstance(a_v, str) else a_v
   280              reply_dict[reply_key] = reply_value
   281          return reply_dict
   282  
   283      def _process_reply(self, api_reply):
   284          """Process API reply.
   285  
   286          :param api_reply: API reply.
   287          :type api_reply: dict or list of dict
   288          :returns: Processed API reply.
   289          :rtype: list or dict
   290          """
   291          if isinstance(api_reply, list):
   292              reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
   293          else:
   294              reverted_reply = self._revert_api_reply(api_reply)
   295          return reverted_reply
   296  
   297      def _execute_papi(self, api_data, method='request', timeout=120):
   298          """Execute PAPI command(s) on remote node and store the result.
   299  
   300          :param api_data: List of APIs with their arguments.
   301          :param method: VPP Python API method. Supported methods are: 'request',
   302              'dump' and 'stats'.
   303          :param timeout: Timeout in seconds.
   304          :type api_data: list
   305          :type method: str
   306          :type timeout: int
   307          :returns: Stdout and stderr.
   308          :rtype: 2-tuple of str
   309          :raises SSHTimeout: If PAPI command(s) execution has timed out.
   310          :raises RuntimeError: If PAPI executor failed due to another reason.
   311          :raises AssertionError: If PAPI command(s) execution has failed.
   312          """
   313  
   314          if not api_data:
   315              RuntimeError("No API data provided.")
   316  
   317          json_data = json.dumps(api_data) \
   318              if method in ("stats", "stats_request") \
   319              else json.dumps(self._process_api_data(api_data))
   320  
   321          cmd = "docker exec {node} python3 {fw_dir}/{papi_provider} --data '{json}'". \
   322              format(node=self.node,
   323                     fw_dir="/opt",
   324                     papi_provider="vpp_api_executor.py",
   325                     json=json_data)
   326          logger.debug(cmd)
   327          stdin, stdout, stderr = self._ssh.exec_command(
   328              cmd, timeout=timeout)
   329          stdout = stdout.read()
   330          stderr = stderr.read()
   331          return stdout, stderr
   332  
   333      def _execute(self, method='request', process_reply=True,
   334                   ignore_errors=False, timeout=120):
   335          """Turn internal command list into proper data and execute; return
   336          PAPI response.
   337  
   338          This method also clears the internal command list.
   339  
   340          IMPORTANT!
   341          Do not use this method in L1 keywords. Use:
   342          - get_stats()
   343          - get_replies()
   344          - get_dump()
   345  
   346          :param method: VPP Python API method. Supported methods are: 'request',
   347              'dump' and 'stats'.
   348          :param process_reply: Process PAPI reply if True.
   349          :param ignore_errors: If true, the errors in the reply are ignored.
   350          :param timeout: Timeout in seconds.
   351          :type method: str
   352          :type process_reply: bool
   353          :type ignore_errors: bool
   354          :type timeout: int
   355          :returns: Papi response including: papi reply, stdout, stderr and
   356              return code.
   357          :rtype: PapiResponse
   358          :raises KeyError: If the reply is not correct.
   359          """
   360  
   361          local_list = self._api_command_list
   362  
   363          # Clear first as execution may fail.
   364          self._api_command_list = list()
   365  
   366          stdout, stderr = self._execute_papi(
   367              local_list, method=method, timeout=timeout)
   368          papi_reply = list()
   369          if process_reply:
   370              try:
   371                  json_data = json.loads(stdout)
   372              except ValueError:
   373                  logger.error(
   374                      "An error occured while processing the PAPI reply:\n"
   375                      "stdout: {stdout}\n"
   376                      "stderr: {stderr}".format(stdout=stdout, stderr=stderr))
   377                  raise
   378              for data in json_data:
   379                  try:
   380                      api_reply_processed = dict(
   381                          api_name=data["api_name"],
   382                          api_reply=self._process_reply(data["api_reply"]))
   383                  except KeyError:
   384                      if ignore_errors:
   385                          continue
   386                      else:
   387                          raise
   388                  papi_reply.append(api_reply_processed)
   389  
   390          # Log processed papi reply to be able to check API replies changes
   391          logger.debug("Processed PAPI reply: {reply}".format(reply=papi_reply))
   392  
   393          return PapiResponse(
   394              papi_reply=papi_reply, stdout=stdout, stderr=stderr,
   395              requests=[rqst["api_name"] for rqst in local_list])