blob: 86bf7cbee308348c44f3fbb9014693f514fdc00d [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
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -050047logger = logging.getLogger('vpp_papi')
Paul Vinciguerra46d68642020-12-01 02:00:35 -050048logger.addHandler(logging.NullHandler())
49
Paul Vinciguerraadcc0b32020-12-16 17:37:57 +000050__all__ = ('FuncWrapper', 'VppApiDynamicMethodHolder',
Paul Vinciguerra3825d932020-12-03 21:06:28 -050051 'VppEnum', 'VppEnumType', 'VppEnumFlag',
Paul Vinciguerraae8819f2019-06-07 13:35:37 -040052 'VPPIOError', 'VPPRuntimeError', 'VPPValueError',
53 'VPPApiClient', )
54
Ole Troanafddd832018-02-28 14:55:20 +010055
Paul Vinciguerra5fced042019-02-26 20:39:44 -080056def metaclass(metaclass):
57 @functools.wraps(metaclass)
58 def wrapper(cls):
59 return metaclass(cls.__name__, cls.__bases__, cls.__dict__.copy())
60
61 return wrapper
62
63
Ole Troan0685da42018-10-16 14:42:50 +020064class VppEnumType(type):
65 def __getattr__(cls, name):
66 t = vpp_get_type(name)
67 return t.enum
68
69
Paul Vinciguerra5fced042019-02-26 20:39:44 -080070@metaclass(VppEnumType)
Paul Vinciguerrad7adc292020-12-02 14:34:27 -050071class VppEnum:
Paul Vinciguerra5fced042019-02-26 20:39:44 -080072 pass
Ole Troan0685da42018-10-16 14:42:50 +020073
74
Paul Vinciguerra3825d932020-12-03 21:06:28 -050075@metaclass(VppEnumType)
76class VppEnumFlag:
77 pass
78
79
Klement Sekera180402d2018-02-17 10:58:37 +010080def vpp_atexit(vpp_weakref):
Ole Troan5016f992017-01-19 09:44:44 +010081 """Clean up VPP connection on shutdown."""
Klement Sekera180402d2018-02-17 10:58:37 +010082 vpp_instance = vpp_weakref()
Ole Troan94495f22018-08-02 11:58:12 +020083 if vpp_instance and vpp_instance.transport.connected:
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -050084 logger.debug('Cleaning up VPP on exit')
Klement Sekera180402d2018-02-17 10:58:37 +010085 vpp_instance.disconnect()
Ole Troan5016f992017-01-19 09:44:44 +010086
Ole Troan8006c6a2018-12-17 12:02:26 +010087
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -040088def add_convenience_methods():
89 # provide convenience methods to IP[46]Address.vapi_af
90 def _vapi_af(self):
91 if 6 == self._version:
92 return VppEnum.vl_api_address_family_t.ADDRESS_IP6.value
93 if 4 == self._version:
94 return VppEnum.vl_api_address_family_t.ADDRESS_IP4.value
95 raise ValueError("Invalid _version.")
96
97 def _vapi_af_name(self):
98 if 6 == self._version:
99 return 'ip6'
100 if 4 == self._version:
101 return 'ip4'
102 raise ValueError("Invalid _version.")
103
104 ipaddress._IPAddressBase.vapi_af = property(_vapi_af)
105 ipaddress._IPAddressBase.vapi_af_name = property(_vapi_af_name)
106
107
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500108class VppApiDynamicMethodHolder:
Klement Sekera7112c542017-03-01 09:53:19 +0100109 pass
110
111
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500112class FuncWrapper:
Klement Sekera7112c542017-03-01 09:53:19 +0100113 def __init__(self, func):
114 self._func = func
115 self.__name__ = func.__name__
Paul Vinciguerrab8daa252019-03-19 15:04:17 -0700116 self.__doc__ = func.__doc__
Klement Sekera7112c542017-03-01 09:53:19 +0100117
118 def __call__(self, **kwargs):
119 return self._func(**kwargs)
120
Paul Vinciguerra48664592019-06-19 22:19:02 -0400121 def __repr__(self):
122 return '<FuncWrapper(func=<%s(%s)>)>' % (self.__name__, self.__doc__)
123
Klement Sekera7112c542017-03-01 09:53:19 +0100124
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800125class VPPApiError(Exception):
126 pass
127
128
129class VPPNotImplementedError(NotImplementedError):
130 pass
131
132
133class VPPIOError(IOError):
134 pass
135
136
137class VPPRuntimeError(RuntimeError):
138 pass
139
140
141class VPPValueError(ValueError):
142 pass
143
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400144
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500145class VPPApiJSONFiles:
Chris Luke52bf22e2017-11-03 23:32:38 -0400146 @classmethod
Ole Troanedfe2c02019-07-30 15:38:13 +0200147 def find_api_dir(cls, dirs):
Chris Luke52bf22e2017-11-03 23:32:38 -0400148 """Attempt to find the best directory in which API definition
149 files may reside. If the value VPP_API_DIR exists in the environment
150 then it is first on the search list. If we're inside a recognized
151 location in a VPP source tree (src/scripts and src/vpp-api/python)
152 then entries from there to the likely locations in build-root are
153 added. Finally the location used by system packages is added.
154
155 :returns: A single directory name, or None if no such directory
156 could be found.
157 """
Chris Luke52bf22e2017-11-03 23:32:38 -0400158
159 # perhaps we're in the 'src/scripts' or 'src/vpp-api/python' dir;
160 # in which case, plot a course to likely places in the src tree
161 import __main__ as main
162 if hasattr(main, '__file__'):
163 # get the path of the calling script
164 localdir = os.path.dirname(os.path.realpath(main.__file__))
165 else:
166 # use cwd if there is no calling script
Andrey "Zed" Zaikin68e2ffb2018-04-24 14:50:02 +0300167 localdir = os.getcwd()
Chris Luke52bf22e2017-11-03 23:32:38 -0400168 localdir_s = localdir.split(os.path.sep)
169
170 def dmatch(dir):
171 """Match dir against right-hand components of the script dir"""
172 d = dir.split('/') # param 'dir' assumes a / separator
Ole Troanafddd832018-02-28 14:55:20 +0100173 length = len(d)
174 return len(localdir_s) > length and localdir_s[-length:] == d
Chris Luke52bf22e2017-11-03 23:32:38 -0400175
176 def sdir(srcdir, variant):
177 """Build a path from srcdir to the staged API files of
178 'variant' (typically '' or '_debug')"""
179 # Since 'core' and 'plugin' files are staged
180 # in separate directories, we target the parent dir.
181 return os.path.sep.join((
182 srcdir,
183 'build-root',
184 'install-vpp%s-native' % variant,
185 'vpp',
186 'share',
187 'vpp',
188 'api',
189 ))
190
191 srcdir = None
192 if dmatch('src/scripts'):
193 srcdir = os.path.sep.join(localdir_s[:-2])
194 elif dmatch('src/vpp-api/python'):
195 srcdir = os.path.sep.join(localdir_s[:-3])
196 elif dmatch('test'):
197 # we're apparently running tests
198 srcdir = os.path.sep.join(localdir_s[:-1])
199
200 if srcdir:
201 # we're in the source tree, try both the debug and release
202 # variants.
Chris Luke52bf22e2017-11-03 23:32:38 -0400203 dirs.append(sdir(srcdir, '_debug'))
204 dirs.append(sdir(srcdir, ''))
205
206 # Test for staged copies of the scripts
207 # For these, since we explicitly know if we're running a debug versus
208 # release variant, target only the relevant directory
209 if dmatch('build-root/install-vpp_debug-native/vpp/bin'):
210 srcdir = os.path.sep.join(localdir_s[:-4])
211 dirs.append(sdir(srcdir, '_debug'))
212 if dmatch('build-root/install-vpp-native/vpp/bin'):
213 srcdir = os.path.sep.join(localdir_s[:-4])
214 dirs.append(sdir(srcdir, ''))
215
216 # finally, try the location system packages typically install into
217 dirs.append(os.path.sep.join(('', 'usr', 'share', 'vpp', 'api')))
218
Paul Vinciguerra19542292019-03-17 17:34:46 -0700219 # check the directories for existence; first one wins
Chris Luke52bf22e2017-11-03 23:32:38 -0400220 for dir in dirs:
221 if os.path.isdir(dir):
222 return dir
223
224 return None
225
226 @classmethod
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500227 def find_api_files(cls, api_dir=None, patterns='*'): # -> list
Chris Luke52bf22e2017-11-03 23:32:38 -0400228 """Find API definition files from the given directory tree with the
229 given pattern. If no directory is given then find_api_dir() is used
230 to locate one. If no pattern is given then all definition files found
231 in the directory tree are used.
232
233 :param api_dir: A directory tree in which to locate API definition
234 files; subdirectories are descended into.
235 If this is None then find_api_dir() is called to discover it.
236 :param patterns: A list of patterns to use in each visited directory
237 when looking for files.
238 This can be a list/tuple object or a comma-separated string of
239 patterns. Each value in the list will have leading/trialing
240 whitespace stripped.
241 The pattern specifies the first part of the filename, '.api.json'
242 is appended.
243 The results are de-duplicated, thus overlapping patterns are fine.
244 If this is None it defaults to '*' meaning "all API files".
245 :returns: A list of file paths for the API files found.
246 """
247 if api_dir is None:
Ole Troanedfe2c02019-07-30 15:38:13 +0200248 api_dir = cls.find_api_dir([])
Chris Luke52bf22e2017-11-03 23:32:38 -0400249 if api_dir is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800250 raise VPPApiError("api_dir cannot be located")
Chris Luke52bf22e2017-11-03 23:32:38 -0400251
252 if isinstance(patterns, list) or isinstance(patterns, tuple):
253 patterns = [p.strip() + '.api.json' for p in patterns]
254 else:
255 patterns = [p.strip() + '.api.json' for p in patterns.split(",")]
256
257 api_files = []
258 for root, dirnames, files in os.walk(api_dir):
259 # iterate all given patterns and de-dup the result
260 files = set(sum([fnmatch.filter(files, p) for p in patterns], []))
261 for filename in files:
262 api_files.append(os.path.join(root, filename))
263
264 return api_files
265
Ole Troanedfe2c02019-07-30 15:38:13 +0200266 @classmethod
267 def process_json_file(self, apidef_file):
268 api = json.load(apidef_file)
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500269 return self._process_json(api)
270
271 @classmethod
272 def process_json_str(self, json_str):
273 api = json.loads(json_str)
274 return self._process_json(api)
275
276 @staticmethod
277 def _process_json(api): # -> Tuple[Dict, Dict]
Ole Troanedfe2c02019-07-30 15:38:13 +0200278 types = {}
279 services = {}
280 messages = {}
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500281 try:
282 for t in api['enums']:
283 t[0] = 'vl_api_' + t[0] + '_t'
284 types[t[0]] = {'type': 'enum', 'data': t}
285 except KeyError:
286 pass
Paul Vinciguerra3825d932020-12-03 21:06:28 -0500287 try:
288 for t in api['enumflags']:
289 t[0] = 'vl_api_' + t[0] + '_t'
290 types[t[0]] = {'type': 'enum', 'data': t}
291 except KeyError:
292 pass
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500293 try:
294 for t in api['unions']:
295 t[0] = 'vl_api_' + t[0] + '_t'
296 types[t[0]] = {'type': 'union', 'data': t}
297 except KeyError:
298 pass
299
300 try:
301 for t in api['types']:
302 t[0] = 'vl_api_' + t[0] + '_t'
303 types[t[0]] = {'type': 'type', 'data': t}
304 except KeyError:
305 pass
306
307 try:
308 for t, v in api['aliases'].items():
309 types['vl_api_' + t + '_t'] = {'type': 'alias', 'data': v}
310 except KeyError:
311 pass
312
313 try:
314 services.update(api['services'])
315 except KeyError:
316 pass
Ole Troanedfe2c02019-07-30 15:38:13 +0200317
318 i = 0
319 while True:
320 unresolved = {}
321 for k, v in types.items():
322 t = v['data']
323 if not vpp_get_type(k):
324 if v['type'] == 'enum':
325 try:
326 VPPEnumType(t[0], t[1:])
327 except ValueError:
328 unresolved[k] = v
Paul Vinciguerra3825d932020-12-03 21:06:28 -0500329 if not vpp_get_type(k):
330 if v['type'] == 'enumflag':
331 try:
332 VPPEnumFlagType(t[0], t[1:])
333 except ValueError:
334 unresolved[k] = v
Ole Troanedfe2c02019-07-30 15:38:13 +0200335 elif v['type'] == 'union':
336 try:
337 VPPUnionType(t[0], t[1:])
338 except ValueError:
339 unresolved[k] = v
340 elif v['type'] == 'type':
341 try:
342 VPPType(t[0], t[1:])
343 except ValueError:
344 unresolved[k] = v
345 elif v['type'] == 'alias':
346 try:
347 VPPTypeAlias(k, t)
348 except ValueError:
349 unresolved[k] = v
350 if len(unresolved) == 0:
351 break
352 if i > 3:
353 raise VPPValueError('Unresolved type definitions {}'
354 .format(unresolved))
355 types = unresolved
356 i += 1
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500357 try:
358 for m in api['messages']:
359 try:
360 messages[m[0]] = VPPMessage(m[0], m[1:])
361 except VPPNotImplementedError:
362 ### OLE FIXME
363 logger.error('Not implemented error for {}'.format(m[0]))
364 except KeyError:
365 pass
Ole Troanedfe2c02019-07-30 15:38:13 +0200366 return messages, services
367
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400368
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500369class VPPApiClient:
Ole Troanedfe2c02019-07-30 15:38:13 +0200370 """VPP interface.
371
372 This class provides the APIs to VPP. The APIs are loaded
373 from provided .api.json files and makes functions accordingly.
374 These functions are documented in the VPP .api files, as they
375 are dynamically created.
376
377 Additionally, VPP can send callback messages; this class
378 provides a means to register a callback function to receive
379 these messages in a background thread.
380 """
381 apidir = None
382 VPPApiError = VPPApiError
383 VPPRuntimeError = VPPRuntimeError
384 VPPValueError = VPPValueError
385 VPPNotImplementedError = VPPNotImplementedError
386 VPPIOError = VPPIOError
387
388
389 def __init__(self, apifiles=None, testmode=False, async_thread=True,
390 logger=None, loglevel=None,
391 read_timeout=5, use_socket=False,
392 server_address='/run/vpp/api.sock'):
393 """Create a VPP API object.
394
395 apifiles is a list of files containing API
396 descriptions that will be loaded - methods will be
397 dynamically created reflecting these APIs. If not
398 provided this will load the API files from VPP's
399 default install location.
400
401 logger, if supplied, is the logging logger object to log to.
402 loglevel, if supplied, is the log level this logger is set
403 to report at (from the loglevels in the logging module).
404 """
405 if logger is None:
406 logger = logging.getLogger(
407 "{}.{}".format(__name__, self.__class__.__name__))
408 if loglevel is not None:
409 logger.setLevel(loglevel)
410 self.logger = logger
411
412 self.messages = {}
413 self.services = {}
414 self.id_names = []
415 self.id_msgdef = []
416 self.header = VPPType('header', [['u16', 'msgid'],
417 ['u32', 'client_index']])
418 self.apifiles = []
419 self.event_callback = None
420 self.message_queue = queue.Queue()
421 self.read_timeout = read_timeout
422 self.async_thread = async_thread
423 self.event_thread = None
424 self.testmode = testmode
425 self.use_socket = use_socket
426 self.server_address = server_address
427 self._apifiles = apifiles
Ole Troanfd574082019-11-27 23:12:48 +0100428 self.stats = {}
Ole Troanedfe2c02019-07-30 15:38:13 +0200429
430 if use_socket:
431 from . vpp_transport_socket import VppTransport
432 else:
433 from . vpp_transport_shmem import VppTransport
434
435 if not apifiles:
436 # Pick up API definitions from default directory
437 try:
438 apifiles = VPPApiJSONFiles.find_api_files(self.apidir)
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500439 except (RuntimeError, VPPApiError):
Ole Troanedfe2c02019-07-30 15:38:13 +0200440 # In test mode we don't care that we can't find the API files
441 if testmode:
442 apifiles = []
443 else:
444 raise VPPRuntimeError
445
446 for file in apifiles:
447 with open(file) as apidef_file:
448 m, s = VPPApiJSONFiles.process_json_file(apidef_file)
449 self.messages.update(m)
450 self.services.update(s)
451
452 self.apifiles = apifiles
453
454 # Basic sanity check
455 if len(self.messages) == 0 and not testmode:
456 raise VPPValueError(1, 'Missing JSON message definitions')
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400457 if not(verify_enum_hint(VppEnum.vl_api_address_family_t)):
458 raise VPPRuntimeError("Invalid address family hints. "
459 "Cannot continue.")
Ole Troanedfe2c02019-07-30 15:38:13 +0200460
461 self.transport = VppTransport(self, read_timeout=read_timeout,
462 server_address=server_address)
463 # Make sure we allow VPP to clean up the message rings.
464 atexit.register(vpp_atexit, weakref.ref(self))
465
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400466 add_convenience_methods()
467
Ole Troanedfe2c02019-07-30 15:38:13 +0200468 def get_function(self, name):
469 return getattr(self._api, name)
470
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500471 class ContextId:
Ole Troanedfe2c02019-07-30 15:38:13 +0200472 """Multiprocessing-safe provider of unique context IDs."""
473 def __init__(self):
474 self.context = mp.Value(ctypes.c_uint, 0)
475 self.lock = mp.Lock()
476
477 def __call__(self):
478 """Get a new unique (or, at least, not recently used) context."""
479 with self.lock:
480 self.context.value += 1
481 return self.context.value
482 get_context = ContextId()
483
484 def get_type(self, name):
485 return vpp_get_type(name)
486
Klement Sekera7112c542017-03-01 09:53:19 +0100487 @property
488 def api(self):
489 if not hasattr(self, "_api"):
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800490 raise VPPApiError("Not connected, api definitions not available")
Klement Sekera7112c542017-03-01 09:53:19 +0100491 return self._api
492
Ole Troaneabd6072018-08-09 12:50:55 +0200493 def make_function(self, msg, i, multipart, do_async):
494 if (do_async):
Ole Troana7564e82018-06-12 21:06:44 +0200495 def f(**kwargs):
496 return self._call_vpp_async(i, msg, **kwargs)
497 else:
498 def f(**kwargs):
499 return self._call_vpp(i, msg, multipart, **kwargs)
500
501 f.__name__ = str(msg.name)
502 f.__doc__ = ", ".join(["%s %s" %
503 (msg.fieldtypes[j], k)
504 for j, k in enumerate(msg.fields)])
Ole Troanf159f582019-02-28 20:20:47 +0100505 f.msg = msg
506
Ole Troana7564e82018-06-12 21:06:44 +0200507 return f
508
Ole Troaneabd6072018-08-09 12:50:55 +0200509 def _register_functions(self, do_async=False):
Ole Troana03f4ef2016-12-02 12:53:55 +0100510 self.id_names = [None] * (self.vpp_dictionary_maxid + 1)
511 self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1)
Klement Sekera8aedf5e2018-07-06 11:07:21 +0200512 self._api = VppApiDynamicMethodHolder()
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500513 for name, msg in self.messages.items():
Ole Troana7564e82018-06-12 21:06:44 +0200514 n = name + '_' + msg.crc[2:]
Ole Troandaa4bff2019-08-28 14:12:02 +0200515 i = self.transport.get_msg_index(n)
Ole Troan3cc49712017-03-08 12:02:24 +0100516 if i > 0:
Ole Troana7564e82018-06-12 21:06:44 +0200517 self.id_msgdef[i] = msg
Ole Troana03f4ef2016-12-02 12:53:55 +0100518 self.id_names[i] = name
Ole Troandfb984d2018-12-07 14:31:16 +0100519
520 # Create function for client side messages.
521 if name in self.services:
Ole Troanf5db3712020-05-20 15:47:06 +0200522 f = self.make_function(msg, i, self.services[name], do_async)
Ole Troandfb984d2018-12-07 14:31:16 +0100523 setattr(self._api, name, FuncWrapper(f))
Ole Troan3cc49712017-03-08 12:02:24 +0100524 else:
Ole Troan4df97162017-07-07 16:06:08 +0200525 self.logger.debug(
526 'No such message type or failed CRC checksum: %s', n)
Ole Troana03f4ef2016-12-02 12:53:55 +0100527
Ole Troan4df97162017-07-07 16:06:08 +0200528 def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200529 do_async):
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700530 pfx = chroot_prefix.encode('utf-8') if chroot_prefix else None
Ole Troan94495f22018-08-02 11:58:12 +0200531
Ole Troandaa4bff2019-08-28 14:12:02 +0200532 rv = self.transport.connect(name, pfx,
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700533 msg_handler, rx_qlen)
Ole Troana03f4ef2016-12-02 12:53:55 +0100534 if rv != 0:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800535 raise VPPIOError(2, 'Connect failed')
Ole Troan94495f22018-08-02 11:58:12 +0200536 self.vpp_dictionary_maxid = self.transport.msg_table_max_index()
Ole Troaneabd6072018-08-09 12:50:55 +0200537 self._register_functions(do_async=do_async)
Ole Troana03f4ef2016-12-02 12:53:55 +0100538
539 # Initialise control ping
Ole Troana7564e82018-06-12 21:06:44 +0200540 crc = self.messages['control_ping'].crc
Ole Troan94495f22018-08-02 11:58:12 +0200541 self.control_ping_index = self.transport.get_msg_index(
Ole Troandaa4bff2019-08-28 14:12:02 +0200542 ('control_ping' + '_' + crc[2:]))
Ole Troana03f4ef2016-12-02 12:53:55 +0100543 self.control_ping_msgdef = self.messages['control_ping']
Klement Sekera180402d2018-02-17 10:58:37 +0100544 if self.async_thread:
545 self.event_thread = threading.Thread(
546 target=self.thread_msg_handler)
547 self.event_thread.daemon = True
548 self.event_thread.start()
Vratko Polak94e45312019-05-27 18:36:23 +0200549 else:
550 self.event_thread = None
Ole Troan4df97162017-07-07 16:06:08 +0200551 return rv
Ole Troana03f4ef2016-12-02 12:53:55 +0100552
Ole Troaneabd6072018-08-09 12:50:55 +0200553 def connect(self, name, chroot_prefix=None, do_async=False, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100554 """Attach to VPP.
555
556 name - the name of the client.
557 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
Ole Troaneabd6072018-08-09 12:50:55 +0200558 do_async - if true, messages are sent without waiting for a reply
Ole Troandfc9b7c2017-03-06 23:51:57 +0100559 rx_qlen - the length of the VPP message receive queue between
560 client and server.
561 """
Ole Troan94495f22018-08-02 11:58:12 +0200562 msg_handler = self.transport.get_callback(do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100563 return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200564 do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100565
Ole Troan6bf177c2017-08-17 10:34:32 +0200566 def connect_sync(self, name, chroot_prefix=None, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100567 """Attach to VPP in synchronous mode. Application must poll for events.
568
569 name - the name of the client.
570 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
571 rx_qlen - the length of the VPP message receive queue between
572 client and server.
573 """
574
Ole Troan94495f22018-08-02 11:58:12 +0200575 return self.connect_internal(name, None, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200576 do_async=False)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100577
Ole Troana03f4ef2016-12-02 12:53:55 +0100578 def disconnect(self):
Ole Troan5016f992017-01-19 09:44:44 +0100579 """Detach from VPP."""
Ole Troan94495f22018-08-02 11:58:12 +0200580 rv = self.transport.disconnect()
Vratko Polak94e45312019-05-27 18:36:23 +0200581 if self.event_thread is not None:
582 self.message_queue.put("terminate event thread")
Ole Troana03f4ef2016-12-02 12:53:55 +0100583 return rv
584
Ole Troan5016f992017-01-19 09:44:44 +0100585 def msg_handler_sync(self, msg):
586 """Process an incoming message from VPP in sync mode.
587
588 The message may be a reply or it may be an async notification.
589 """
590 r = self.decode_incoming_msg(msg)
591 if r is None:
Ole Troana03f4ef2016-12-02 12:53:55 +0100592 return
593
Ole Troan5016f992017-01-19 09:44:44 +0100594 # If we have a context, then use the context to find any
595 # request waiting for a reply
596 context = 0
597 if hasattr(r, 'context') and r.context > 0:
598 context = r.context
Ole Troan5f9dcff2016-08-01 04:59:13 +0200599
Ole Troan5016f992017-01-19 09:44:44 +0100600 if context == 0:
601 # No context -> async notification that we feed to the callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100602 self.message_queue.put_nowait(r)
Ole Troana03f4ef2016-12-02 12:53:55 +0100603 else:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800604 raise VPPIOError(2, 'RPC reply message received in event handler')
Ole Troan5016f992017-01-19 09:44:44 +0100605
Ole Troan413f4a52018-11-28 11:36:05 +0100606 def has_context(self, msg):
607 if len(msg) < 10:
608 return False
609
610 header = VPPType('header_with_context', [['u16', 'msgid'],
611 ['u32', 'client_index'],
612 ['u32', 'context']])
613
614 (i, ci, context), size = header.unpack(msg, 0)
615 if self.id_names[i] == 'rx_thread_exit':
616 return
617
618 #
619 # Decode message and returns a tuple.
620 #
621 msgobj = self.id_msgdef[i]
622 if 'context' in msgobj.field_by_name and context >= 0:
623 return True
624 return False
625
Ole Troan0bcad322018-12-11 13:04:01 +0100626 def decode_incoming_msg(self, msg, no_type_conversion=False):
Ole Troan5016f992017-01-19 09:44:44 +0100627 if not msg:
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -0500628 logger.warning('vpp_api.read failed')
Ole Troan5016f992017-01-19 09:44:44 +0100629 return
Ole Troan413f4a52018-11-28 11:36:05 +0100630
Ole Troanc84cbad2018-09-06 22:58:05 +0200631 (i, ci), size = self.header.unpack(msg, 0)
Ole Troan5016f992017-01-19 09:44:44 +0100632 if self.id_names[i] == 'rx_thread_exit':
633 return
634
635 #
636 # Decode message and returns a tuple.
637 #
Ole Troana7564e82018-06-12 21:06:44 +0200638 msgobj = self.id_msgdef[i]
639 if not msgobj:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800640 raise VPPIOError(2, 'Reply message undefined')
Ole Troan5016f992017-01-19 09:44:44 +0100641
Ole Troan0bcad322018-12-11 13:04:01 +0100642 r, size = msgobj.unpack(msg, ntc=no_type_conversion)
Ole Troana03f4ef2016-12-02 12:53:55 +0100643 return r
644
Ole Troan5016f992017-01-19 09:44:44 +0100645 def msg_handler_async(self, msg):
646 """Process a message from VPP in async mode.
647
648 In async mode, all messages are returned to the callback.
649 """
650 r = self.decode_incoming_msg(msg)
651 if r is None:
652 return
653
654 msgname = type(r).__name__
655
Ole Troan4df97162017-07-07 16:06:08 +0200656 if self.event_callback:
657 self.event_callback(msgname, r)
Ole Troan5016f992017-01-19 09:44:44 +0100658
659 def _control_ping(self, context):
660 """Send a ping command."""
661 self._call_vpp_async(self.control_ping_index,
Ole Troan4df97162017-07-07 16:06:08 +0200662 self.control_ping_msgdef,
Ole Troan5016f992017-01-19 09:44:44 +0100663 context=context)
664
Ole Troana7564e82018-06-12 21:06:44 +0200665 def validate_args(self, msg, kwargs):
666 d = set(kwargs.keys()) - set(msg.field_by_name.keys())
667 if d:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800668 raise VPPValueError('Invalid argument {} to {}'
Ole Troan0bcad322018-12-11 13:04:01 +0100669 .format(list(d), msg.name))
Ole Troana7564e82018-06-12 21:06:44 +0200670
Ole Troanfd574082019-11-27 23:12:48 +0100671 def _add_stat(self, name, ms):
672 if not name in self.stats:
673 self.stats[name] = {'max': ms, 'count': 1, 'avg': ms}
674 else:
675 if ms > self.stats[name]['max']:
676 self.stats[name]['max'] = ms
677 self.stats[name]['count'] += 1
678 n = self.stats[name]['count']
679 self.stats[name]['avg'] = self.stats[name]['avg'] * (n - 1) / n + ms / n
680
681 def get_stats(self):
682 s = '\n=== API PAPI STATISTICS ===\n'
683 s += '{:<30} {:>4} {:>6} {:>6}\n'.format('message', 'cnt', 'avg', 'max')
684 for n in sorted(self.stats.items(), key=lambda v: v[1]['avg'], reverse=True):
685 s += '{:<30} {:>4} {:>6.2f} {:>6.2f}\n'.format(n[0], n[1]['count'],
686 n[1]['avg'], n[1]['max'])
687 return s
688
Paul Vinciguerra65315142020-04-26 22:04:32 -0400689 def get_field_options(self, msg, fld_name):
690 # when there is an option, the msgdef has 3 elements.
691 # ['u32', 'ring_size', {'default': 1024}]
692 for _def in self.messages[msg].msgdef:
693 if isinstance(_def, list) and \
694 len(_def) == 3 and \
695 _def[1] == fld_name:
696 return _def[2]
697
Ole Troanf5db3712020-05-20 15:47:06 +0200698 def _call_vpp(self, i, msgdef, service, **kwargs):
Ole Troan5016f992017-01-19 09:44:44 +0100699 """Given a message, send the message and await a reply.
700
701 msgdef - the message packing definition
702 i - the message type index
703 multipart - True if the message returns multiple
704 messages in return.
705 context - context number - chosen at random if not
706 supplied.
707 The remainder of the kwargs are the arguments to the API call.
708
709 The return value is the message or message array containing
710 the response. It will raise an IOError exception if there was
711 no response within the timeout window.
712 """
Ole Troanfd574082019-11-27 23:12:48 +0100713 ts = time.time()
Ole Troan4df97162017-07-07 16:06:08 +0200714 if 'context' not in kwargs:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100715 context = self.get_context()
716 kwargs['context'] = context
717 else:
718 context = kwargs['context']
719 kwargs['_vl_msg_id'] = i
Ole Troan5016f992017-01-19 09:44:44 +0100720
Ole Troan0bcad322018-12-11 13:04:01 +0100721 no_type_conversion = kwargs.pop('_no_type_conversion', False)
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500722 timeout = kwargs.pop('_timeout', None)
Ole Troan0bcad322018-12-11 13:04:01 +0100723
Ole Troan94495f22018-08-02 11:58:12 +0200724 try:
725 if self.transport.socket_index:
726 kwargs['client_index'] = self.transport.socket_index
727 except AttributeError:
728 pass
Ole Troan413f4a52018-11-28 11:36:05 +0100729 self.validate_args(msgdef, kwargs)
730
Vratko Polakb6590202019-07-16 14:32:55 +0200731 s = 'Calling {}({})'.format(msgdef.name,
732 ','.join(['{!r}:{!r}'.format(k, v) for k, v in kwargs.items()]))
733 self.logger.debug(s)
Ole Troan413f4a52018-11-28 11:36:05 +0100734
735 b = msgdef.pack(kwargs)
Ole Troan94495f22018-08-02 11:58:12 +0200736 self.transport.suspend()
737
738 self.transport.write(b)
Ole Troan5016f992017-01-19 09:44:44 +0100739
Ole Troanf5db3712020-05-20 15:47:06 +0200740 msgreply = service['reply']
741 stream = True if 'stream' in service else False
742 if stream:
743 if 'stream_msg' in service:
744 # New service['reply'] = _reply and service['stream_message'] = _details
745 stream_message = service['stream_msg']
746 modern =True
747 else:
748 # Old service['reply'] = _details
749 stream_message = msgreply
750 msgreply = 'control_ping_reply'
751 modern = False
752 # Send a ping after the request - we use its response
753 # to detect that we have seen all results.
754 self._control_ping(context)
Ole Troan5016f992017-01-19 09:44:44 +0100755
756 # Block until we get a reply.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100757 rl = []
758 while (True):
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500759 r = self.read_blocking(no_type_conversion, timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200760 if r is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800761 raise VPPIOError(2, 'VPP API client: read failed')
Ole Troandfc9b7c2017-03-06 23:51:57 +0100762 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200763 if context not in r or r.context == 0 or context != r.context:
Ole Troan94495f22018-08-02 11:58:12 +0200764 # Message being queued
Ole Troandfc9b7c2017-03-06 23:51:57 +0100765 self.message_queue.put_nowait(r)
766 continue
Ole Troanf5db3712020-05-20 15:47:06 +0200767 if msgname != msgreply and (stream and (msgname != stream_message)):
768 print('REPLY MISMATCH', msgreply, msgname, stream_message, stream)
769 if not stream:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100770 rl = r
771 break
Ole Troanf5db3712020-05-20 15:47:06 +0200772 if msgname == msgreply:
773 if modern: # Return both reply and list
774 rl = r, rl
Ole Troandfc9b7c2017-03-06 23:51:57 +0100775 break
776
777 rl.append(r)
778
Ole Troan94495f22018-08-02 11:58:12 +0200779 self.transport.resume()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100780
Klement Sekera5e2f84d2019-09-12 09:01:06 +0000781 s = 'Return value: {!r}'.format(r)
782 if len(s) > 80:
783 s = s[:80] + "..."
784 self.logger.debug(s)
Ole Troanfd574082019-11-27 23:12:48 +0100785 te = time.time()
786 self._add_stat(msgdef.name, (te - ts) * 1000)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100787 return rl
Ole Troan5016f992017-01-19 09:44:44 +0100788
Ole Troana7564e82018-06-12 21:06:44 +0200789 def _call_vpp_async(self, i, msg, **kwargs):
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200790 """Given a message, send the message and return the context.
Ole Troan5016f992017-01-19 09:44:44 +0100791
792 msgdef - the message packing definition
793 i - the message type index
794 context - context number - chosen at random if not
795 supplied.
796 The remainder of the kwargs are the arguments to the API call.
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200797
798 The reply message(s) will be delivered later to the registered callback.
799 The returned context will help with assigning which call
800 the reply belongs to.
Ole Troan5016f992017-01-19 09:44:44 +0100801 """
Ole Troan4df97162017-07-07 16:06:08 +0200802 if 'context' not in kwargs:
Ole Troan7e3a8752016-12-05 10:27:09 +0100803 context = self.get_context()
804 kwargs['context'] = context
805 else:
806 context = kwargs['context']
Ole Troan94495f22018-08-02 11:58:12 +0200807 try:
808 if self.transport.socket_index:
809 kwargs['client_index'] = self.transport.socket_index
810 except AttributeError:
811 kwargs['client_index'] = 0
Ole Troan7e3a8752016-12-05 10:27:09 +0100812 kwargs['_vl_msg_id'] = i
Ole Troana7564e82018-06-12 21:06:44 +0200813 b = msg.pack(kwargs)
Ole Troan7e3a8752016-12-05 10:27:09 +0100814
Ole Troan94495f22018-08-02 11:58:12 +0200815 self.transport.write(b)
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200816 return context
Ole Troan7e3a8752016-12-05 10:27:09 +0100817
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500818 def read_blocking(self, no_type_conversion=False, timeout=None):
Vratko Polak09385472019-09-10 13:35:11 +0200819 """Get next received message from transport within timeout, decoded.
820
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500821 Note that notifications have context zero
Vratko Polak09385472019-09-10 13:35:11 +0200822 and are not put into receive queue (at least for socket transport),
823 use async_thread with registered callback for processing them.
824
825 If no message appears in the queue within timeout, return None.
826
827 Optionally, type conversion can be skipped,
828 as some of conversions are into less precise types.
829
830 When r is the return value of this, the caller can get message name as:
831 msgname = type(r).__name__
832 and context number (type long) as:
833 context = r.context
834
835 :param no_type_conversion: If false, type conversions are applied.
836 :type no_type_conversion: bool
837 :returns: Decoded message, or None if no message (within timeout).
838 :rtype: Whatever VPPType.unpack returns, depends on no_type_conversion.
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500839 :raises VppTransportShmemIOError if timed out.
Vratko Polak09385472019-09-10 13:35:11 +0200840 """
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500841 msg = self.transport.read(timeout=timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200842 if not msg:
843 return None
844 return self.decode_incoming_msg(msg, no_type_conversion)
845
Ole Troana03f4ef2016-12-02 12:53:55 +0100846 def register_event_callback(self, callback):
Ole Troan5016f992017-01-19 09:44:44 +0100847 """Register a callback for async messages.
Ole Troana03f4ef2016-12-02 12:53:55 +0100848
Ole Troan5016f992017-01-19 09:44:44 +0100849 This will be called for async notifications in sync mode,
850 and all messages in async mode. In sync mode, replies to
851 requests will not come here.
852
853 callback is a fn(msg_type_name, msg_type) that will be
854 called when a message comes in. While this function is
855 executing, note that (a) you are in a background thread and
856 may wish to use threading.Lock to protect your datastructures,
857 and (b) message processing from VPP will stop (so if you take
858 a long while about it you may provoke reply timeouts or cause
859 VPP to fill the RX buffer). Passing None will disable the
860 callback.
861 """
862 self.event_callback = callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100863
864 def thread_msg_handler(self):
Ole Troan94495f22018-08-02 11:58:12 +0200865 """Python thread calling the user registered message handler.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100866
867 This is to emulate the old style event callback scheme. Modern
868 clients should provide their own thread to poll the event
869 queue.
870 """
871 while True:
872 r = self.message_queue.get()
Klement Sekera180402d2018-02-17 10:58:37 +0100873 if r == "terminate event thread":
874 break
Ole Troandfc9b7c2017-03-06 23:51:57 +0100875 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200876 if self.event_callback:
877 self.event_callback(msgname, r)
Chris Luke52bf22e2017-11-03 23:32:38 -0400878
Ole Troanc046d702019-10-14 23:07:06 +0200879 def validate_message_table(self, namecrctable):
880 """Take a dictionary of name_crc message names
881 and returns an array of missing messages"""
882
883 missing_table = []
884 for name_crc in namecrctable:
885 i = self.transport.get_msg_index(name_crc)
886 if i <= 0:
887 missing_table.append(name_crc)
888 return missing_table
889
890 def dump_message_table(self):
891 """Return VPPs API message table as name_crc dictionary"""
892 return self.transport.message_table
893
894 def dump_message_table_filtered(self, msglist):
895 """Return VPPs API message table as name_crc dictionary,
896 filtered by message name list."""
897
898 replies = [self.services[n]['reply'] for n in msglist]
899 message_table_filtered = {}
900 for name in msglist + replies:
901 for k,v in self.transport.message_table.items():
902 if k.startswith(name):
903 message_table_filtered[k] = v
904 break
905 return message_table_filtered
906
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400907 def __repr__(self):
908 return "<VPPApiClient apifiles=%s, testmode=%s, async_thread=%s, " \
909 "logger=%s, read_timeout=%s, use_socket=%s, " \
910 "server_address='%s'>" % (
911 self._apifiles, self.testmode, self.async_thread,
912 self.logger, self.read_timeout, self.use_socket,
913 self.server_address)
914
Ole Troanf5db3712020-05-20 15:47:06 +0200915 def details_iter(self, f, **kwargs):
916 cursor = 0
917 while True:
918 kwargs['cursor'] = cursor
919 rv, details = f(**kwargs)
920 #
921 # Convert to yield from details when we only support python 3
922 #
923 for d in details:
924 yield d
925 if rv.retval == 0 or rv.retval != -165:
926 break
927 cursor = rv.cursor
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400928
Chris Luke52bf22e2017-11-03 23:32:38 -0400929# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4