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])