blob: edbe479c54cd2d9ff9b4fb7f762e8ed91cd148a7 [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 Vinciguerraae8819f2019-06-07 13:35:37 -040050__all__ = ('FuncWrapper', 'VPP', '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
Ole Troanf5db3712020-05-20 15:47:06 +0200689 def _call_vpp(self, i, msgdef, service, **kwargs):
Ole Troan5016f992017-01-19 09:44:44 +0100690 """Given a message, send the message and await a reply.
691
692 msgdef - the message packing definition
693 i - the message type index
694 multipart - True if the message returns multiple
695 messages in return.
696 context - context number - chosen at random if not
697 supplied.
698 The remainder of the kwargs are the arguments to the API call.
699
700 The return value is the message or message array containing
701 the response. It will raise an IOError exception if there was
702 no response within the timeout window.
703 """
Ole Troanfd574082019-11-27 23:12:48 +0100704 ts = time.time()
Ole Troan4df97162017-07-07 16:06:08 +0200705 if 'context' not in kwargs:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100706 context = self.get_context()
707 kwargs['context'] = context
708 else:
709 context = kwargs['context']
710 kwargs['_vl_msg_id'] = i
Ole Troan5016f992017-01-19 09:44:44 +0100711
Ole Troan0bcad322018-12-11 13:04:01 +0100712 no_type_conversion = kwargs.pop('_no_type_conversion', False)
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500713 timeout = kwargs.pop('_timeout', None)
Ole Troan0bcad322018-12-11 13:04:01 +0100714
Ole Troan94495f22018-08-02 11:58:12 +0200715 try:
716 if self.transport.socket_index:
717 kwargs['client_index'] = self.transport.socket_index
718 except AttributeError:
719 pass
Ole Troan413f4a52018-11-28 11:36:05 +0100720 self.validate_args(msgdef, kwargs)
721
Vratko Polakb6590202019-07-16 14:32:55 +0200722 s = 'Calling {}({})'.format(msgdef.name,
723 ','.join(['{!r}:{!r}'.format(k, v) for k, v in kwargs.items()]))
724 self.logger.debug(s)
Ole Troan413f4a52018-11-28 11:36:05 +0100725
726 b = msgdef.pack(kwargs)
Ole Troan94495f22018-08-02 11:58:12 +0200727 self.transport.suspend()
728
729 self.transport.write(b)
Ole Troan5016f992017-01-19 09:44:44 +0100730
Ole Troanf5db3712020-05-20 15:47:06 +0200731 msgreply = service['reply']
732 stream = True if 'stream' in service else False
733 if stream:
734 if 'stream_msg' in service:
735 # New service['reply'] = _reply and service['stream_message'] = _details
736 stream_message = service['stream_msg']
737 modern =True
738 else:
739 # Old service['reply'] = _details
740 stream_message = msgreply
741 msgreply = 'control_ping_reply'
742 modern = False
743 # Send a ping after the request - we use its response
744 # to detect that we have seen all results.
745 self._control_ping(context)
Ole Troan5016f992017-01-19 09:44:44 +0100746
747 # Block until we get a reply.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100748 rl = []
749 while (True):
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500750 r = self.read_blocking(no_type_conversion, timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200751 if r is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800752 raise VPPIOError(2, 'VPP API client: read failed')
Ole Troandfc9b7c2017-03-06 23:51:57 +0100753 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200754 if context not in r or r.context == 0 or context != r.context:
Ole Troan94495f22018-08-02 11:58:12 +0200755 # Message being queued
Ole Troandfc9b7c2017-03-06 23:51:57 +0100756 self.message_queue.put_nowait(r)
757 continue
Ole Troanf5db3712020-05-20 15:47:06 +0200758 if msgname != msgreply and (stream and (msgname != stream_message)):
759 print('REPLY MISMATCH', msgreply, msgname, stream_message, stream)
760 if not stream:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100761 rl = r
762 break
Ole Troanf5db3712020-05-20 15:47:06 +0200763 if msgname == msgreply:
764 if modern: # Return both reply and list
765 rl = r, rl
Ole Troandfc9b7c2017-03-06 23:51:57 +0100766 break
767
768 rl.append(r)
769
Ole Troan94495f22018-08-02 11:58:12 +0200770 self.transport.resume()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100771
Klement Sekera5e2f84d2019-09-12 09:01:06 +0000772 s = 'Return value: {!r}'.format(r)
773 if len(s) > 80:
774 s = s[:80] + "..."
775 self.logger.debug(s)
Ole Troanfd574082019-11-27 23:12:48 +0100776 te = time.time()
777 self._add_stat(msgdef.name, (te - ts) * 1000)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100778 return rl
Ole Troan5016f992017-01-19 09:44:44 +0100779
Ole Troana7564e82018-06-12 21:06:44 +0200780 def _call_vpp_async(self, i, msg, **kwargs):
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200781 """Given a message, send the message and return the context.
Ole Troan5016f992017-01-19 09:44:44 +0100782
783 msgdef - the message packing definition
784 i - the message type index
785 context - context number - chosen at random if not
786 supplied.
787 The remainder of the kwargs are the arguments to the API call.
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200788
789 The reply message(s) will be delivered later to the registered callback.
790 The returned context will help with assigning which call
791 the reply belongs to.
Ole Troan5016f992017-01-19 09:44:44 +0100792 """
Ole Troan4df97162017-07-07 16:06:08 +0200793 if 'context' not in kwargs:
Ole Troan7e3a8752016-12-05 10:27:09 +0100794 context = self.get_context()
795 kwargs['context'] = context
796 else:
797 context = kwargs['context']
Ole Troan94495f22018-08-02 11:58:12 +0200798 try:
799 if self.transport.socket_index:
800 kwargs['client_index'] = self.transport.socket_index
801 except AttributeError:
802 kwargs['client_index'] = 0
Ole Troan7e3a8752016-12-05 10:27:09 +0100803 kwargs['_vl_msg_id'] = i
Ole Troana7564e82018-06-12 21:06:44 +0200804 b = msg.pack(kwargs)
Ole Troan7e3a8752016-12-05 10:27:09 +0100805
Ole Troan94495f22018-08-02 11:58:12 +0200806 self.transport.write(b)
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200807 return context
Ole Troan7e3a8752016-12-05 10:27:09 +0100808
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500809 def read_blocking(self, no_type_conversion=False, timeout=None):
Vratko Polak09385472019-09-10 13:35:11 +0200810 """Get next received message from transport within timeout, decoded.
811
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500812 Note that notifications have context zero
Vratko Polak09385472019-09-10 13:35:11 +0200813 and are not put into receive queue (at least for socket transport),
814 use async_thread with registered callback for processing them.
815
816 If no message appears in the queue within timeout, return None.
817
818 Optionally, type conversion can be skipped,
819 as some of conversions are into less precise types.
820
821 When r is the return value of this, the caller can get message name as:
822 msgname = type(r).__name__
823 and context number (type long) as:
824 context = r.context
825
826 :param no_type_conversion: If false, type conversions are applied.
827 :type no_type_conversion: bool
828 :returns: Decoded message, or None if no message (within timeout).
829 :rtype: Whatever VPPType.unpack returns, depends on no_type_conversion.
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500830 :raises VppTransportShmemIOError if timed out.
Vratko Polak09385472019-09-10 13:35:11 +0200831 """
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500832 msg = self.transport.read(timeout=timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200833 if not msg:
834 return None
835 return self.decode_incoming_msg(msg, no_type_conversion)
836
Ole Troana03f4ef2016-12-02 12:53:55 +0100837 def register_event_callback(self, callback):
Ole Troan5016f992017-01-19 09:44:44 +0100838 """Register a callback for async messages.
Ole Troana03f4ef2016-12-02 12:53:55 +0100839
Ole Troan5016f992017-01-19 09:44:44 +0100840 This will be called for async notifications in sync mode,
841 and all messages in async mode. In sync mode, replies to
842 requests will not come here.
843
844 callback is a fn(msg_type_name, msg_type) that will be
845 called when a message comes in. While this function is
846 executing, note that (a) you are in a background thread and
847 may wish to use threading.Lock to protect your datastructures,
848 and (b) message processing from VPP will stop (so if you take
849 a long while about it you may provoke reply timeouts or cause
850 VPP to fill the RX buffer). Passing None will disable the
851 callback.
852 """
853 self.event_callback = callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100854
855 def thread_msg_handler(self):
Ole Troan94495f22018-08-02 11:58:12 +0200856 """Python thread calling the user registered message handler.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100857
858 This is to emulate the old style event callback scheme. Modern
859 clients should provide their own thread to poll the event
860 queue.
861 """
862 while True:
863 r = self.message_queue.get()
Klement Sekera180402d2018-02-17 10:58:37 +0100864 if r == "terminate event thread":
865 break
Ole Troandfc9b7c2017-03-06 23:51:57 +0100866 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200867 if self.event_callback:
868 self.event_callback(msgname, r)
Chris Luke52bf22e2017-11-03 23:32:38 -0400869
Ole Troanc046d702019-10-14 23:07:06 +0200870 def validate_message_table(self, namecrctable):
871 """Take a dictionary of name_crc message names
872 and returns an array of missing messages"""
873
874 missing_table = []
875 for name_crc in namecrctable:
876 i = self.transport.get_msg_index(name_crc)
877 if i <= 0:
878 missing_table.append(name_crc)
879 return missing_table
880
881 def dump_message_table(self):
882 """Return VPPs API message table as name_crc dictionary"""
883 return self.transport.message_table
884
885 def dump_message_table_filtered(self, msglist):
886 """Return VPPs API message table as name_crc dictionary,
887 filtered by message name list."""
888
889 replies = [self.services[n]['reply'] for n in msglist]
890 message_table_filtered = {}
891 for name in msglist + replies:
892 for k,v in self.transport.message_table.items():
893 if k.startswith(name):
894 message_table_filtered[k] = v
895 break
896 return message_table_filtered
897
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400898 def __repr__(self):
899 return "<VPPApiClient apifiles=%s, testmode=%s, async_thread=%s, " \
900 "logger=%s, read_timeout=%s, use_socket=%s, " \
901 "server_address='%s'>" % (
902 self._apifiles, self.testmode, self.async_thread,
903 self.logger, self.read_timeout, self.use_socket,
904 self.server_address)
905
Ole Troanf5db3712020-05-20 15:47:06 +0200906 def details_iter(self, f, **kwargs):
907 cursor = 0
908 while True:
909 kwargs['cursor'] = cursor
910 rv, details = f(**kwargs)
911 #
912 # Convert to yield from details when we only support python 3
913 #
914 for d in details:
915 yield d
916 if rv.retval == 0 or rv.retval != -165:
917 break
918 cursor = rv.cursor
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400919
Chris Luke52bf22e2017-11-03 23:32:38 -0400920# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4