blob: 3465f503e9e8d2e95c41d6d1b4d3e8f94d1f146e [file] [log] [blame]
Renato Botelho do Coutoead1e532019-10-31 13:31:07 -05001#!/usr/bin/env python3
Ole Troan5f9dcff2016-08-01 04:59:13 +02002#
3# Copyright (c) 2016 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.
Ole Troana03f4ef2016-12-02 12:53:55 +010015#
Ole Troan5f9dcff2016-08-01 04:59:13 +020016
Ole Troan5f9dcff2016-08-01 04:59:13 +020017from __future__ import print_function
Ole Troana7564e82018-06-12 21:06:44 +020018from __future__ import absolute_import
Paul Vinciguerra2af6e922019-06-06 07:06:09 -040019import ctypes
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -040020import ipaddress
Ole Troan4df97162017-07-07 16:06:08 +020021import sys
Paul Vinciguerra2af6e922019-06-06 07:06:09 -040022import multiprocessing as mp
Ole Troan4df97162017-07-07 16:06:08 +020023import os
Paul Vinciguerrad7adc292020-12-02 14:34:27 -050024import queue
Ole Troan4df97162017-07-07 16:06:08 +020025import logging
Paul Vinciguerra5fced042019-02-26 20:39:44 -080026import functools
Ole Troan4df97162017-07-07 16:06:08 +020027import json
28import threading
Chris Luke52bf22e2017-11-03 23:32:38 -040029import fnmatch
Klement Sekera180402d2018-02-17 10:58:37 +010030import weakref
Ole Troan4df97162017-07-07 16:06:08 +020031import atexit
Ole Troanfd574082019-11-27 23:12:48 +010032import time
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -040033from . vpp_format import verify_enum_hint
Paul Vinciguerra3825d932020-12-03 21:06:28 -050034from . vpp_serializer import VPPType, VPPEnumType, VPPEnumFlagType, VPPUnionType
Ole Troan53fffa12018-11-13 12:36:56 +010035from . vpp_serializer import VPPMessage, vpp_get_type, VPPTypeAlias
Ole Troan4df97162017-07-07 16:06:08 +020036
Paul Vinciguerra46d68642020-12-01 02:00:35 -050037try:
38 import VppTransport
39except ModuleNotFoundError:
40 class V:
41 """placeholder for VppTransport as the implementation is dependent on
42 VPPAPIClient's initialization values
43 """
44
45 VppTransport = V
46
Ole Troan041372b2021-05-04 12:45:57 +020047from . vpp_transport_socket import VppTransport
48
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -050049logger = logging.getLogger('vpp_papi')
Paul Vinciguerra46d68642020-12-01 02:00:35 -050050logger.addHandler(logging.NullHandler())
51
Paul Vinciguerraadcc0b32020-12-16 17:37:57 +000052__all__ = ('FuncWrapper', 'VppApiDynamicMethodHolder',
Paul Vinciguerra3825d932020-12-03 21:06:28 -050053 'VppEnum', 'VppEnumType', 'VppEnumFlag',
Paul Vinciguerraae8819f2019-06-07 13:35:37 -040054 'VPPIOError', 'VPPRuntimeError', 'VPPValueError',
55 'VPPApiClient', )
56
Ole Troanafddd832018-02-28 14:55:20 +010057
Paul Vinciguerra5fced042019-02-26 20:39:44 -080058def metaclass(metaclass):
59 @functools.wraps(metaclass)
60 def wrapper(cls):
61 return metaclass(cls.__name__, cls.__bases__, cls.__dict__.copy())
62
63 return wrapper
64
65
Ole Troan0685da42018-10-16 14:42:50 +020066class VppEnumType(type):
67 def __getattr__(cls, name):
68 t = vpp_get_type(name)
69 return t.enum
70
71
Paul Vinciguerra5fced042019-02-26 20:39:44 -080072@metaclass(VppEnumType)
Paul Vinciguerrad7adc292020-12-02 14:34:27 -050073class VppEnum:
Paul Vinciguerra5fced042019-02-26 20:39:44 -080074 pass
Ole Troan0685da42018-10-16 14:42:50 +020075
76
Paul Vinciguerra3825d932020-12-03 21:06:28 -050077@metaclass(VppEnumType)
78class VppEnumFlag:
79 pass
80
81
Klement Sekera180402d2018-02-17 10:58:37 +010082def vpp_atexit(vpp_weakref):
Ole Troan5016f992017-01-19 09:44:44 +010083 """Clean up VPP connection on shutdown."""
Klement Sekera180402d2018-02-17 10:58:37 +010084 vpp_instance = vpp_weakref()
Ole Troan94495f22018-08-02 11:58:12 +020085 if vpp_instance and vpp_instance.transport.connected:
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -050086 logger.debug('Cleaning up VPP on exit')
Klement Sekera180402d2018-02-17 10:58:37 +010087 vpp_instance.disconnect()
Ole Troan5016f992017-01-19 09:44:44 +010088
Ole Troan8006c6a2018-12-17 12:02:26 +010089
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -040090def add_convenience_methods():
91 # provide convenience methods to IP[46]Address.vapi_af
92 def _vapi_af(self):
93 if 6 == self._version:
94 return VppEnum.vl_api_address_family_t.ADDRESS_IP6.value
95 if 4 == self._version:
96 return VppEnum.vl_api_address_family_t.ADDRESS_IP4.value
97 raise ValueError("Invalid _version.")
98
99 def _vapi_af_name(self):
100 if 6 == self._version:
101 return 'ip6'
102 if 4 == self._version:
103 return 'ip4'
104 raise ValueError("Invalid _version.")
105
106 ipaddress._IPAddressBase.vapi_af = property(_vapi_af)
107 ipaddress._IPAddressBase.vapi_af_name = property(_vapi_af_name)
108
109
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500110class VppApiDynamicMethodHolder:
Klement Sekera7112c542017-03-01 09:53:19 +0100111 pass
112
113
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500114class FuncWrapper:
Klement Sekera7112c542017-03-01 09:53:19 +0100115 def __init__(self, func):
116 self._func = func
117 self.__name__ = func.__name__
Paul Vinciguerrab8daa252019-03-19 15:04:17 -0700118 self.__doc__ = func.__doc__
Klement Sekera7112c542017-03-01 09:53:19 +0100119
120 def __call__(self, **kwargs):
121 return self._func(**kwargs)
122
Paul Vinciguerra48664592019-06-19 22:19:02 -0400123 def __repr__(self):
124 return '<FuncWrapper(func=<%s(%s)>)>' % (self.__name__, self.__doc__)
125
Klement Sekera7112c542017-03-01 09:53:19 +0100126
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800127class VPPApiError(Exception):
128 pass
129
130
131class VPPNotImplementedError(NotImplementedError):
132 pass
133
134
135class VPPIOError(IOError):
136 pass
137
138
139class VPPRuntimeError(RuntimeError):
140 pass
141
142
143class VPPValueError(ValueError):
144 pass
145
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400146
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500147class VPPApiJSONFiles:
Chris Luke52bf22e2017-11-03 23:32:38 -0400148 @classmethod
Ole Troanedfe2c02019-07-30 15:38:13 +0200149 def find_api_dir(cls, dirs):
Chris Luke52bf22e2017-11-03 23:32:38 -0400150 """Attempt to find the best directory in which API definition
151 files may reside. If the value VPP_API_DIR exists in the environment
152 then it is first on the search list. If we're inside a recognized
153 location in a VPP source tree (src/scripts and src/vpp-api/python)
154 then entries from there to the likely locations in build-root are
155 added. Finally the location used by system packages is added.
156
157 :returns: A single directory name, or None if no such directory
158 could be found.
159 """
Chris Luke52bf22e2017-11-03 23:32:38 -0400160
161 # perhaps we're in the 'src/scripts' or 'src/vpp-api/python' dir;
162 # in which case, plot a course to likely places in the src tree
163 import __main__ as main
164 if hasattr(main, '__file__'):
165 # get the path of the calling script
166 localdir = os.path.dirname(os.path.realpath(main.__file__))
167 else:
168 # use cwd if there is no calling script
Andrey "Zed" Zaikin68e2ffb2018-04-24 14:50:02 +0300169 localdir = os.getcwd()
Chris Luke52bf22e2017-11-03 23:32:38 -0400170 localdir_s = localdir.split(os.path.sep)
171
172 def dmatch(dir):
173 """Match dir against right-hand components of the script dir"""
174 d = dir.split('/') # param 'dir' assumes a / separator
Ole Troanafddd832018-02-28 14:55:20 +0100175 length = len(d)
176 return len(localdir_s) > length and localdir_s[-length:] == d
Chris Luke52bf22e2017-11-03 23:32:38 -0400177
178 def sdir(srcdir, variant):
179 """Build a path from srcdir to the staged API files of
180 'variant' (typically '' or '_debug')"""
181 # Since 'core' and 'plugin' files are staged
182 # in separate directories, we target the parent dir.
183 return os.path.sep.join((
184 srcdir,
185 'build-root',
186 'install-vpp%s-native' % variant,
187 'vpp',
188 'share',
189 'vpp',
190 'api',
191 ))
192
193 srcdir = None
194 if dmatch('src/scripts'):
195 srcdir = os.path.sep.join(localdir_s[:-2])
196 elif dmatch('src/vpp-api/python'):
197 srcdir = os.path.sep.join(localdir_s[:-3])
198 elif dmatch('test'):
199 # we're apparently running tests
200 srcdir = os.path.sep.join(localdir_s[:-1])
201
202 if srcdir:
203 # we're in the source tree, try both the debug and release
204 # variants.
Chris Luke52bf22e2017-11-03 23:32:38 -0400205 dirs.append(sdir(srcdir, '_debug'))
206 dirs.append(sdir(srcdir, ''))
207
208 # Test for staged copies of the scripts
209 # For these, since we explicitly know if we're running a debug versus
210 # release variant, target only the relevant directory
211 if dmatch('build-root/install-vpp_debug-native/vpp/bin'):
212 srcdir = os.path.sep.join(localdir_s[:-4])
213 dirs.append(sdir(srcdir, '_debug'))
214 if dmatch('build-root/install-vpp-native/vpp/bin'):
215 srcdir = os.path.sep.join(localdir_s[:-4])
216 dirs.append(sdir(srcdir, ''))
217
218 # finally, try the location system packages typically install into
219 dirs.append(os.path.sep.join(('', 'usr', 'share', 'vpp', 'api')))
220
Paul Vinciguerra19542292019-03-17 17:34:46 -0700221 # check the directories for existence; first one wins
Chris Luke52bf22e2017-11-03 23:32:38 -0400222 for dir in dirs:
223 if os.path.isdir(dir):
224 return dir
225
226 return None
227
228 @classmethod
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500229 def find_api_files(cls, api_dir=None, patterns='*'): # -> list
Chris Luke52bf22e2017-11-03 23:32:38 -0400230 """Find API definition files from the given directory tree with the
231 given pattern. If no directory is given then find_api_dir() is used
232 to locate one. If no pattern is given then all definition files found
233 in the directory tree are used.
234
235 :param api_dir: A directory tree in which to locate API definition
236 files; subdirectories are descended into.
237 If this is None then find_api_dir() is called to discover it.
238 :param patterns: A list of patterns to use in each visited directory
239 when looking for files.
240 This can be a list/tuple object or a comma-separated string of
241 patterns. Each value in the list will have leading/trialing
242 whitespace stripped.
243 The pattern specifies the first part of the filename, '.api.json'
244 is appended.
245 The results are de-duplicated, thus overlapping patterns are fine.
246 If this is None it defaults to '*' meaning "all API files".
247 :returns: A list of file paths for the API files found.
248 """
249 if api_dir is None:
Ole Troanedfe2c02019-07-30 15:38:13 +0200250 api_dir = cls.find_api_dir([])
Chris Luke52bf22e2017-11-03 23:32:38 -0400251 if api_dir is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800252 raise VPPApiError("api_dir cannot be located")
Chris Luke52bf22e2017-11-03 23:32:38 -0400253
254 if isinstance(patterns, list) or isinstance(patterns, tuple):
255 patterns = [p.strip() + '.api.json' for p in patterns]
256 else:
257 patterns = [p.strip() + '.api.json' for p in patterns.split(",")]
258
259 api_files = []
260 for root, dirnames, files in os.walk(api_dir):
261 # iterate all given patterns and de-dup the result
262 files = set(sum([fnmatch.filter(files, p) for p in patterns], []))
263 for filename in files:
264 api_files.append(os.path.join(root, filename))
265
266 return api_files
267
Ole Troanedfe2c02019-07-30 15:38:13 +0200268 @classmethod
269 def process_json_file(self, apidef_file):
270 api = json.load(apidef_file)
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500271 return self._process_json(api)
272
273 @classmethod
274 def process_json_str(self, json_str):
275 api = json.loads(json_str)
276 return self._process_json(api)
277
278 @staticmethod
279 def _process_json(api): # -> Tuple[Dict, Dict]
Ole Troanedfe2c02019-07-30 15:38:13 +0200280 types = {}
281 services = {}
282 messages = {}
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500283 try:
284 for t in api['enums']:
285 t[0] = 'vl_api_' + t[0] + '_t'
286 types[t[0]] = {'type': 'enum', 'data': t}
287 except KeyError:
288 pass
Paul Vinciguerra3825d932020-12-03 21:06:28 -0500289 try:
290 for t in api['enumflags']:
291 t[0] = 'vl_api_' + t[0] + '_t'
292 types[t[0]] = {'type': 'enum', 'data': t}
293 except KeyError:
294 pass
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500295 try:
296 for t in api['unions']:
297 t[0] = 'vl_api_' + t[0] + '_t'
298 types[t[0]] = {'type': 'union', 'data': t}
299 except KeyError:
300 pass
301
302 try:
303 for t in api['types']:
304 t[0] = 'vl_api_' + t[0] + '_t'
305 types[t[0]] = {'type': 'type', 'data': t}
306 except KeyError:
307 pass
308
309 try:
310 for t, v in api['aliases'].items():
311 types['vl_api_' + t + '_t'] = {'type': 'alias', 'data': v}
312 except KeyError:
313 pass
314
315 try:
316 services.update(api['services'])
317 except KeyError:
318 pass
Ole Troanedfe2c02019-07-30 15:38:13 +0200319
320 i = 0
321 while True:
322 unresolved = {}
323 for k, v in types.items():
324 t = v['data']
325 if not vpp_get_type(k):
326 if v['type'] == 'enum':
327 try:
328 VPPEnumType(t[0], t[1:])
329 except ValueError:
330 unresolved[k] = v
Paul Vinciguerra3825d932020-12-03 21:06:28 -0500331 if not vpp_get_type(k):
332 if v['type'] == 'enumflag':
333 try:
334 VPPEnumFlagType(t[0], t[1:])
335 except ValueError:
336 unresolved[k] = v
Ole Troanedfe2c02019-07-30 15:38:13 +0200337 elif v['type'] == 'union':
338 try:
339 VPPUnionType(t[0], t[1:])
340 except ValueError:
341 unresolved[k] = v
342 elif v['type'] == 'type':
343 try:
344 VPPType(t[0], t[1:])
345 except ValueError:
346 unresolved[k] = v
347 elif v['type'] == 'alias':
348 try:
349 VPPTypeAlias(k, t)
350 except ValueError:
351 unresolved[k] = v
352 if len(unresolved) == 0:
353 break
354 if i > 3:
355 raise VPPValueError('Unresolved type definitions {}'
356 .format(unresolved))
357 types = unresolved
358 i += 1
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500359 try:
360 for m in api['messages']:
361 try:
362 messages[m[0]] = VPPMessage(m[0], m[1:])
363 except VPPNotImplementedError:
364 ### OLE FIXME
365 logger.error('Not implemented error for {}'.format(m[0]))
366 except KeyError:
367 pass
Ole Troanedfe2c02019-07-30 15:38:13 +0200368 return messages, services
369
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400370
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500371class VPPApiClient:
Ole Troanedfe2c02019-07-30 15:38:13 +0200372 """VPP interface.
373
374 This class provides the APIs to VPP. The APIs are loaded
375 from provided .api.json files and makes functions accordingly.
376 These functions are documented in the VPP .api files, as they
377 are dynamically created.
378
379 Additionally, VPP can send callback messages; this class
380 provides a means to register a callback function to receive
381 these messages in a background thread.
382 """
383 apidir = None
384 VPPApiError = VPPApiError
385 VPPRuntimeError = VPPRuntimeError
386 VPPValueError = VPPValueError
387 VPPNotImplementedError = VPPNotImplementedError
388 VPPIOError = VPPIOError
389
390
Ole Troandf6d9862021-04-15 16:53:39 +0200391 def __init__(self, *, apifiles=None, testmode=False, async_thread=True,
Ole Troanedfe2c02019-07-30 15:38:13 +0200392 logger=None, loglevel=None,
Ole Troandf6d9862021-04-15 16:53:39 +0200393 read_timeout=5, use_socket=True,
Ole Troanedfe2c02019-07-30 15:38:13 +0200394 server_address='/run/vpp/api.sock'):
395 """Create a VPP API object.
396
397 apifiles is a list of files containing API
398 descriptions that will be loaded - methods will be
399 dynamically created reflecting these APIs. If not
400 provided this will load the API files from VPP's
401 default install location.
402
403 logger, if supplied, is the logging logger object to log to.
404 loglevel, if supplied, is the log level this logger is set
405 to report at (from the loglevels in the logging module).
406 """
407 if logger is None:
408 logger = logging.getLogger(
409 "{}.{}".format(__name__, self.__class__.__name__))
410 if loglevel is not None:
411 logger.setLevel(loglevel)
412 self.logger = logger
413
414 self.messages = {}
415 self.services = {}
416 self.id_names = []
417 self.id_msgdef = []
418 self.header = VPPType('header', [['u16', 'msgid'],
419 ['u32', 'client_index']])
420 self.apifiles = []
421 self.event_callback = None
422 self.message_queue = queue.Queue()
423 self.read_timeout = read_timeout
424 self.async_thread = async_thread
425 self.event_thread = None
426 self.testmode = testmode
Ole Troanedfe2c02019-07-30 15:38:13 +0200427 self.server_address = server_address
428 self._apifiles = apifiles
Ole Troanfd574082019-11-27 23:12:48 +0100429 self.stats = {}
Ole Troanedfe2c02019-07-30 15:38:13 +0200430
Ole Troanedfe2c02019-07-30 15:38:13 +0200431 if not apifiles:
432 # Pick up API definitions from default directory
433 try:
434 apifiles = VPPApiJSONFiles.find_api_files(self.apidir)
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500435 except (RuntimeError, VPPApiError):
Ole Troanedfe2c02019-07-30 15:38:13 +0200436 # In test mode we don't care that we can't find the API files
437 if testmode:
438 apifiles = []
439 else:
440 raise VPPRuntimeError
441
442 for file in apifiles:
443 with open(file) as apidef_file:
444 m, s = VPPApiJSONFiles.process_json_file(apidef_file)
445 self.messages.update(m)
446 self.services.update(s)
447
448 self.apifiles = apifiles
449
450 # Basic sanity check
451 if len(self.messages) == 0 and not testmode:
452 raise VPPValueError(1, 'Missing JSON message definitions')
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400453 if not(verify_enum_hint(VppEnum.vl_api_address_family_t)):
454 raise VPPRuntimeError("Invalid address family hints. "
455 "Cannot continue.")
Ole Troanedfe2c02019-07-30 15:38:13 +0200456
457 self.transport = VppTransport(self, read_timeout=read_timeout,
458 server_address=server_address)
459 # Make sure we allow VPP to clean up the message rings.
460 atexit.register(vpp_atexit, weakref.ref(self))
461
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400462 add_convenience_methods()
463
Ole Troanedfe2c02019-07-30 15:38:13 +0200464 def get_function(self, name):
465 return getattr(self._api, name)
466
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500467 class ContextId:
Ole Troanedfe2c02019-07-30 15:38:13 +0200468 """Multiprocessing-safe provider of unique context IDs."""
469 def __init__(self):
470 self.context = mp.Value(ctypes.c_uint, 0)
471 self.lock = mp.Lock()
472
473 def __call__(self):
474 """Get a new unique (or, at least, not recently used) context."""
475 with self.lock:
476 self.context.value += 1
477 return self.context.value
478 get_context = ContextId()
479
480 def get_type(self, name):
481 return vpp_get_type(name)
482
Klement Sekera7112c542017-03-01 09:53:19 +0100483 @property
484 def api(self):
485 if not hasattr(self, "_api"):
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800486 raise VPPApiError("Not connected, api definitions not available")
Klement Sekera7112c542017-03-01 09:53:19 +0100487 return self._api
488
Ole Troaneabd6072018-08-09 12:50:55 +0200489 def make_function(self, msg, i, multipart, do_async):
490 if (do_async):
Ole Troana7564e82018-06-12 21:06:44 +0200491 def f(**kwargs):
492 return self._call_vpp_async(i, msg, **kwargs)
493 else:
494 def f(**kwargs):
495 return self._call_vpp(i, msg, multipart, **kwargs)
496
497 f.__name__ = str(msg.name)
498 f.__doc__ = ", ".join(["%s %s" %
499 (msg.fieldtypes[j], k)
500 for j, k in enumerate(msg.fields)])
Ole Troanf159f582019-02-28 20:20:47 +0100501 f.msg = msg
502
Ole Troana7564e82018-06-12 21:06:44 +0200503 return f
504
Ole Troaneabd6072018-08-09 12:50:55 +0200505 def _register_functions(self, do_async=False):
Ole Troana03f4ef2016-12-02 12:53:55 +0100506 self.id_names = [None] * (self.vpp_dictionary_maxid + 1)
507 self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1)
Klement Sekera8aedf5e2018-07-06 11:07:21 +0200508 self._api = VppApiDynamicMethodHolder()
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500509 for name, msg in self.messages.items():
Ole Troana7564e82018-06-12 21:06:44 +0200510 n = name + '_' + msg.crc[2:]
Ole Troandaa4bff2019-08-28 14:12:02 +0200511 i = self.transport.get_msg_index(n)
Ole Troan3cc49712017-03-08 12:02:24 +0100512 if i > 0:
Ole Troana7564e82018-06-12 21:06:44 +0200513 self.id_msgdef[i] = msg
Ole Troana03f4ef2016-12-02 12:53:55 +0100514 self.id_names[i] = name
Ole Troandfb984d2018-12-07 14:31:16 +0100515
516 # Create function for client side messages.
517 if name in self.services:
Ole Troanf5db3712020-05-20 15:47:06 +0200518 f = self.make_function(msg, i, self.services[name], do_async)
Ole Troandfb984d2018-12-07 14:31:16 +0100519 setattr(self._api, name, FuncWrapper(f))
Ole Troan3cc49712017-03-08 12:02:24 +0100520 else:
Ole Troan4df97162017-07-07 16:06:08 +0200521 self.logger.debug(
522 'No such message type or failed CRC checksum: %s', n)
Ole Troana03f4ef2016-12-02 12:53:55 +0100523
Ole Troan4df97162017-07-07 16:06:08 +0200524 def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200525 do_async):
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700526 pfx = chroot_prefix.encode('utf-8') if chroot_prefix else None
Ole Troan94495f22018-08-02 11:58:12 +0200527
Ole Troandaa4bff2019-08-28 14:12:02 +0200528 rv = self.transport.connect(name, pfx,
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700529 msg_handler, rx_qlen)
Ole Troana03f4ef2016-12-02 12:53:55 +0100530 if rv != 0:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800531 raise VPPIOError(2, 'Connect failed')
Ole Troan94495f22018-08-02 11:58:12 +0200532 self.vpp_dictionary_maxid = self.transport.msg_table_max_index()
Ole Troaneabd6072018-08-09 12:50:55 +0200533 self._register_functions(do_async=do_async)
Ole Troana03f4ef2016-12-02 12:53:55 +0100534
535 # Initialise control ping
Ole Troana7564e82018-06-12 21:06:44 +0200536 crc = self.messages['control_ping'].crc
Ole Troan94495f22018-08-02 11:58:12 +0200537 self.control_ping_index = self.transport.get_msg_index(
Ole Troandaa4bff2019-08-28 14:12:02 +0200538 ('control_ping' + '_' + crc[2:]))
Ole Troana03f4ef2016-12-02 12:53:55 +0100539 self.control_ping_msgdef = self.messages['control_ping']
Klement Sekera180402d2018-02-17 10:58:37 +0100540 if self.async_thread:
541 self.event_thread = threading.Thread(
542 target=self.thread_msg_handler)
543 self.event_thread.daemon = True
544 self.event_thread.start()
Vratko Polak94e45312019-05-27 18:36:23 +0200545 else:
546 self.event_thread = None
Ole Troan4df97162017-07-07 16:06:08 +0200547 return rv
Ole Troana03f4ef2016-12-02 12:53:55 +0100548
Ole Troaneabd6072018-08-09 12:50:55 +0200549 def connect(self, name, chroot_prefix=None, do_async=False, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100550 """Attach to VPP.
551
552 name - the name of the client.
553 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
Ole Troaneabd6072018-08-09 12:50:55 +0200554 do_async - if true, messages are sent without waiting for a reply
Ole Troandfc9b7c2017-03-06 23:51:57 +0100555 rx_qlen - the length of the VPP message receive queue between
556 client and server.
557 """
Ole Troan94495f22018-08-02 11:58:12 +0200558 msg_handler = self.transport.get_callback(do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100559 return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200560 do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100561
Ole Troan6bf177c2017-08-17 10:34:32 +0200562 def connect_sync(self, name, chroot_prefix=None, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100563 """Attach to VPP in synchronous mode. Application must poll for events.
564
565 name - the name of the client.
566 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
567 rx_qlen - the length of the VPP message receive queue between
568 client and server.
569 """
570
Ole Troan94495f22018-08-02 11:58:12 +0200571 return self.connect_internal(name, None, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200572 do_async=False)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100573
Ole Troana03f4ef2016-12-02 12:53:55 +0100574 def disconnect(self):
Ole Troan5016f992017-01-19 09:44:44 +0100575 """Detach from VPP."""
Ole Troan94495f22018-08-02 11:58:12 +0200576 rv = self.transport.disconnect()
Vratko Polak94e45312019-05-27 18:36:23 +0200577 if self.event_thread is not None:
578 self.message_queue.put("terminate event thread")
Ole Troana03f4ef2016-12-02 12:53:55 +0100579 return rv
580
Ole Troan5016f992017-01-19 09:44:44 +0100581 def msg_handler_sync(self, msg):
582 """Process an incoming message from VPP in sync mode.
583
584 The message may be a reply or it may be an async notification.
585 """
586 r = self.decode_incoming_msg(msg)
587 if r is None:
Ole Troana03f4ef2016-12-02 12:53:55 +0100588 return
589
Ole Troan5016f992017-01-19 09:44:44 +0100590 # If we have a context, then use the context to find any
591 # request waiting for a reply
592 context = 0
593 if hasattr(r, 'context') and r.context > 0:
594 context = r.context
Ole Troan5f9dcff2016-08-01 04:59:13 +0200595
Ole Troan5016f992017-01-19 09:44:44 +0100596 if context == 0:
597 # No context -> async notification that we feed to the callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100598 self.message_queue.put_nowait(r)
Ole Troana03f4ef2016-12-02 12:53:55 +0100599 else:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800600 raise VPPIOError(2, 'RPC reply message received in event handler')
Ole Troan5016f992017-01-19 09:44:44 +0100601
Ole Troan413f4a52018-11-28 11:36:05 +0100602 def has_context(self, msg):
603 if len(msg) < 10:
604 return False
605
606 header = VPPType('header_with_context', [['u16', 'msgid'],
607 ['u32', 'client_index'],
608 ['u32', 'context']])
609
610 (i, ci, context), size = header.unpack(msg, 0)
611 if self.id_names[i] == 'rx_thread_exit':
612 return
613
614 #
615 # Decode message and returns a tuple.
616 #
617 msgobj = self.id_msgdef[i]
618 if 'context' in msgobj.field_by_name and context >= 0:
619 return True
620 return False
621
Ole Troan0bcad322018-12-11 13:04:01 +0100622 def decode_incoming_msg(self, msg, no_type_conversion=False):
Ole Troan5016f992017-01-19 09:44:44 +0100623 if not msg:
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -0500624 logger.warning('vpp_api.read failed')
Ole Troan5016f992017-01-19 09:44:44 +0100625 return
Ole Troan413f4a52018-11-28 11:36:05 +0100626
Ole Troanc84cbad2018-09-06 22:58:05 +0200627 (i, ci), size = self.header.unpack(msg, 0)
Ole Troan5016f992017-01-19 09:44:44 +0100628 if self.id_names[i] == 'rx_thread_exit':
629 return
630
631 #
632 # Decode message and returns a tuple.
633 #
Ole Troana7564e82018-06-12 21:06:44 +0200634 msgobj = self.id_msgdef[i]
635 if not msgobj:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800636 raise VPPIOError(2, 'Reply message undefined')
Ole Troan5016f992017-01-19 09:44:44 +0100637
Ole Troan0bcad322018-12-11 13:04:01 +0100638 r, size = msgobj.unpack(msg, ntc=no_type_conversion)
Ole Troana03f4ef2016-12-02 12:53:55 +0100639 return r
640
Ole Troan5016f992017-01-19 09:44:44 +0100641 def msg_handler_async(self, msg):
642 """Process a message from VPP in async mode.
643
644 In async mode, all messages are returned to the callback.
645 """
646 r = self.decode_incoming_msg(msg)
647 if r is None:
648 return
649
650 msgname = type(r).__name__
651
Ole Troan4df97162017-07-07 16:06:08 +0200652 if self.event_callback:
653 self.event_callback(msgname, r)
Ole Troan5016f992017-01-19 09:44:44 +0100654
655 def _control_ping(self, context):
656 """Send a ping command."""
657 self._call_vpp_async(self.control_ping_index,
Ole Troan4df97162017-07-07 16:06:08 +0200658 self.control_ping_msgdef,
Ole Troan5016f992017-01-19 09:44:44 +0100659 context=context)
660
Ole Troana7564e82018-06-12 21:06:44 +0200661 def validate_args(self, msg, kwargs):
662 d = set(kwargs.keys()) - set(msg.field_by_name.keys())
663 if d:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800664 raise VPPValueError('Invalid argument {} to {}'
Ole Troan0bcad322018-12-11 13:04:01 +0100665 .format(list(d), msg.name))
Ole Troana7564e82018-06-12 21:06:44 +0200666
Ole Troanfd574082019-11-27 23:12:48 +0100667 def _add_stat(self, name, ms):
668 if not name in self.stats:
669 self.stats[name] = {'max': ms, 'count': 1, 'avg': ms}
670 else:
671 if ms > self.stats[name]['max']:
672 self.stats[name]['max'] = ms
673 self.stats[name]['count'] += 1
674 n = self.stats[name]['count']
675 self.stats[name]['avg'] = self.stats[name]['avg'] * (n - 1) / n + ms / n
676
677 def get_stats(self):
678 s = '\n=== API PAPI STATISTICS ===\n'
679 s += '{:<30} {:>4} {:>6} {:>6}\n'.format('message', 'cnt', 'avg', 'max')
680 for n in sorted(self.stats.items(), key=lambda v: v[1]['avg'], reverse=True):
681 s += '{:<30} {:>4} {:>6.2f} {:>6.2f}\n'.format(n[0], n[1]['count'],
682 n[1]['avg'], n[1]['max'])
683 return s
684
Paul Vinciguerra65315142020-04-26 22:04:32 -0400685 def get_field_options(self, msg, fld_name):
686 # when there is an option, the msgdef has 3 elements.
687 # ['u32', 'ring_size', {'default': 1024}]
688 for _def in self.messages[msg].msgdef:
689 if isinstance(_def, list) and \
690 len(_def) == 3 and \
691 _def[1] == fld_name:
692 return _def[2]
693
Ole Troanf5db3712020-05-20 15:47:06 +0200694 def _call_vpp(self, i, msgdef, service, **kwargs):
Ole Troan5016f992017-01-19 09:44:44 +0100695 """Given a message, send the message and await a reply.
696
697 msgdef - the message packing definition
698 i - the message type index
699 multipart - True if the message returns multiple
700 messages in return.
701 context - context number - chosen at random if not
702 supplied.
703 The remainder of the kwargs are the arguments to the API call.
704
705 The return value is the message or message array containing
706 the response. It will raise an IOError exception if there was
707 no response within the timeout window.
708 """
Ole Troanfd574082019-11-27 23:12:48 +0100709 ts = time.time()
Ole Troan4df97162017-07-07 16:06:08 +0200710 if 'context' not in kwargs:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100711 context = self.get_context()
712 kwargs['context'] = context
713 else:
714 context = kwargs['context']
715 kwargs['_vl_msg_id'] = i
Ole Troan5016f992017-01-19 09:44:44 +0100716
Ole Troan0bcad322018-12-11 13:04:01 +0100717 no_type_conversion = kwargs.pop('_no_type_conversion', False)
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500718 timeout = kwargs.pop('_timeout', None)
Ole Troan0bcad322018-12-11 13:04:01 +0100719
Ole Troan94495f22018-08-02 11:58:12 +0200720 try:
721 if self.transport.socket_index:
722 kwargs['client_index'] = self.transport.socket_index
723 except AttributeError:
724 pass
Ole Troan413f4a52018-11-28 11:36:05 +0100725 self.validate_args(msgdef, kwargs)
726
Vratko Polakb6590202019-07-16 14:32:55 +0200727 s = 'Calling {}({})'.format(msgdef.name,
728 ','.join(['{!r}:{!r}'.format(k, v) for k, v in kwargs.items()]))
729 self.logger.debug(s)
Ole Troan413f4a52018-11-28 11:36:05 +0100730
731 b = msgdef.pack(kwargs)
Ole Troan94495f22018-08-02 11:58:12 +0200732 self.transport.suspend()
733
734 self.transport.write(b)
Ole Troan5016f992017-01-19 09:44:44 +0100735
Ole Troanf5db3712020-05-20 15:47:06 +0200736 msgreply = service['reply']
737 stream = True if 'stream' in service else False
738 if stream:
739 if 'stream_msg' in service:
740 # New service['reply'] = _reply and service['stream_message'] = _details
741 stream_message = service['stream_msg']
742 modern =True
743 else:
744 # Old service['reply'] = _details
745 stream_message = msgreply
746 msgreply = 'control_ping_reply'
747 modern = False
748 # Send a ping after the request - we use its response
749 # to detect that we have seen all results.
750 self._control_ping(context)
Ole Troan5016f992017-01-19 09:44:44 +0100751
752 # Block until we get a reply.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100753 rl = []
754 while (True):
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500755 r = self.read_blocking(no_type_conversion, timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200756 if r is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800757 raise VPPIOError(2, 'VPP API client: read failed')
Ole Troandfc9b7c2017-03-06 23:51:57 +0100758 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200759 if context not in r or r.context == 0 or context != r.context:
Ole Troan94495f22018-08-02 11:58:12 +0200760 # Message being queued
Ole Troandfc9b7c2017-03-06 23:51:57 +0100761 self.message_queue.put_nowait(r)
762 continue
Ole Troanf5db3712020-05-20 15:47:06 +0200763 if msgname != msgreply and (stream and (msgname != stream_message)):
764 print('REPLY MISMATCH', msgreply, msgname, stream_message, stream)
765 if not stream:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100766 rl = r
767 break
Ole Troanf5db3712020-05-20 15:47:06 +0200768 if msgname == msgreply:
769 if modern: # Return both reply and list
770 rl = r, rl
Ole Troandfc9b7c2017-03-06 23:51:57 +0100771 break
772
773 rl.append(r)
774
Ole Troan94495f22018-08-02 11:58:12 +0200775 self.transport.resume()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100776
Klement Sekera5e2f84d2019-09-12 09:01:06 +0000777 s = 'Return value: {!r}'.format(r)
778 if len(s) > 80:
779 s = s[:80] + "..."
780 self.logger.debug(s)
Ole Troanfd574082019-11-27 23:12:48 +0100781 te = time.time()
782 self._add_stat(msgdef.name, (te - ts) * 1000)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100783 return rl
Ole Troan5016f992017-01-19 09:44:44 +0100784
Ole Troana7564e82018-06-12 21:06:44 +0200785 def _call_vpp_async(self, i, msg, **kwargs):
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200786 """Given a message, send the message and return the context.
Ole Troan5016f992017-01-19 09:44:44 +0100787
788 msgdef - the message packing definition
789 i - the message type index
790 context - context number - chosen at random if not
791 supplied.
792 The remainder of the kwargs are the arguments to the API call.
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200793
794 The reply message(s) will be delivered later to the registered callback.
795 The returned context will help with assigning which call
796 the reply belongs to.
Ole Troan5016f992017-01-19 09:44:44 +0100797 """
Ole Troan4df97162017-07-07 16:06:08 +0200798 if 'context' not in kwargs:
Ole Troan7e3a8752016-12-05 10:27:09 +0100799 context = self.get_context()
800 kwargs['context'] = context
801 else:
802 context = kwargs['context']
Ole Troan94495f22018-08-02 11:58:12 +0200803 try:
804 if self.transport.socket_index:
805 kwargs['client_index'] = self.transport.socket_index
806 except AttributeError:
807 kwargs['client_index'] = 0
Ole Troan7e3a8752016-12-05 10:27:09 +0100808 kwargs['_vl_msg_id'] = i
Ole Troana7564e82018-06-12 21:06:44 +0200809 b = msg.pack(kwargs)
Ole Troan7e3a8752016-12-05 10:27:09 +0100810
Ole Troan94495f22018-08-02 11:58:12 +0200811 self.transport.write(b)
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200812 return context
Ole Troan7e3a8752016-12-05 10:27:09 +0100813
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500814 def read_blocking(self, no_type_conversion=False, timeout=None):
Vratko Polak09385472019-09-10 13:35:11 +0200815 """Get next received message from transport within timeout, decoded.
816
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500817 Note that notifications have context zero
Vratko Polak09385472019-09-10 13:35:11 +0200818 and are not put into receive queue (at least for socket transport),
819 use async_thread with registered callback for processing them.
820
821 If no message appears in the queue within timeout, return None.
822
823 Optionally, type conversion can be skipped,
824 as some of conversions are into less precise types.
825
826 When r is the return value of this, the caller can get message name as:
827 msgname = type(r).__name__
828 and context number (type long) as:
829 context = r.context
830
831 :param no_type_conversion: If false, type conversions are applied.
832 :type no_type_conversion: bool
833 :returns: Decoded message, or None if no message (within timeout).
834 :rtype: Whatever VPPType.unpack returns, depends on no_type_conversion.
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500835 :raises VppTransportShmemIOError if timed out.
Vratko Polak09385472019-09-10 13:35:11 +0200836 """
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500837 msg = self.transport.read(timeout=timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200838 if not msg:
839 return None
840 return self.decode_incoming_msg(msg, no_type_conversion)
841
Ole Troana03f4ef2016-12-02 12:53:55 +0100842 def register_event_callback(self, callback):
Ole Troan5016f992017-01-19 09:44:44 +0100843 """Register a callback for async messages.
Ole Troana03f4ef2016-12-02 12:53:55 +0100844
Ole Troan5016f992017-01-19 09:44:44 +0100845 This will be called for async notifications in sync mode,
846 and all messages in async mode. In sync mode, replies to
847 requests will not come here.
848
849 callback is a fn(msg_type_name, msg_type) that will be
850 called when a message comes in. While this function is
851 executing, note that (a) you are in a background thread and
852 may wish to use threading.Lock to protect your datastructures,
853 and (b) message processing from VPP will stop (so if you take
854 a long while about it you may provoke reply timeouts or cause
855 VPP to fill the RX buffer). Passing None will disable the
856 callback.
857 """
858 self.event_callback = callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100859
860 def thread_msg_handler(self):
Ole Troan94495f22018-08-02 11:58:12 +0200861 """Python thread calling the user registered message handler.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100862
863 This is to emulate the old style event callback scheme. Modern
864 clients should provide their own thread to poll the event
865 queue.
866 """
867 while True:
868 r = self.message_queue.get()
Klement Sekera180402d2018-02-17 10:58:37 +0100869 if r == "terminate event thread":
870 break
Ole Troandfc9b7c2017-03-06 23:51:57 +0100871 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200872 if self.event_callback:
873 self.event_callback(msgname, r)
Chris Luke52bf22e2017-11-03 23:32:38 -0400874
Ole Troanc046d702019-10-14 23:07:06 +0200875 def validate_message_table(self, namecrctable):
876 """Take a dictionary of name_crc message names
877 and returns an array of missing messages"""
878
879 missing_table = []
880 for name_crc in namecrctable:
881 i = self.transport.get_msg_index(name_crc)
882 if i <= 0:
883 missing_table.append(name_crc)
884 return missing_table
885
886 def dump_message_table(self):
887 """Return VPPs API message table as name_crc dictionary"""
888 return self.transport.message_table
889
890 def dump_message_table_filtered(self, msglist):
891 """Return VPPs API message table as name_crc dictionary,
892 filtered by message name list."""
893
894 replies = [self.services[n]['reply'] for n in msglist]
895 message_table_filtered = {}
896 for name in msglist + replies:
897 for k,v in self.transport.message_table.items():
898 if k.startswith(name):
899 message_table_filtered[k] = v
900 break
901 return message_table_filtered
902
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400903 def __repr__(self):
904 return "<VPPApiClient apifiles=%s, testmode=%s, async_thread=%s, " \
Ole Troan041372b2021-05-04 12:45:57 +0200905 "logger=%s, read_timeout=%s, " \
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400906 "server_address='%s'>" % (
907 self._apifiles, self.testmode, self.async_thread,
Ole Troan041372b2021-05-04 12:45:57 +0200908 self.logger, self.read_timeout, self.server_address)
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400909
Ole Troanf5db3712020-05-20 15:47:06 +0200910 def details_iter(self, f, **kwargs):
911 cursor = 0
912 while True:
913 kwargs['cursor'] = cursor
914 rv, details = f(**kwargs)
Ole Troanf5db3712020-05-20 15:47:06 +0200915 for d in details:
916 yield d
917 if rv.retval == 0 or rv.retval != -165:
918 break
919 cursor = rv.cursor