blob: e22b88bc3f68541cdc38153298234b78cfe37b4b [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 Vinciguerraae8819f2019-06-07 13:35:37 -040034from . vpp_serializer import VPPType, VPPEnumType, 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',
51 'VppEnum', 'VppEnumType',
52 '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
Klement Sekera180402d2018-02-17 10:58:37 +010075def vpp_atexit(vpp_weakref):
Ole Troan5016f992017-01-19 09:44:44 +010076 """Clean up VPP connection on shutdown."""
Klement Sekera180402d2018-02-17 10:58:37 +010077 vpp_instance = vpp_weakref()
Ole Troan94495f22018-08-02 11:58:12 +020078 if vpp_instance and vpp_instance.transport.connected:
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -050079 logger.debug('Cleaning up VPP on exit')
Klement Sekera180402d2018-02-17 10:58:37 +010080 vpp_instance.disconnect()
Ole Troan5016f992017-01-19 09:44:44 +010081
Ole Troan8006c6a2018-12-17 12:02:26 +010082
Ole Troan4df97162017-07-07 16:06:08 +020083
84
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -040085def add_convenience_methods():
86 # provide convenience methods to IP[46]Address.vapi_af
87 def _vapi_af(self):
88 if 6 == self._version:
89 return VppEnum.vl_api_address_family_t.ADDRESS_IP6.value
90 if 4 == self._version:
91 return VppEnum.vl_api_address_family_t.ADDRESS_IP4.value
92 raise ValueError("Invalid _version.")
93
94 def _vapi_af_name(self):
95 if 6 == self._version:
96 return 'ip6'
97 if 4 == self._version:
98 return 'ip4'
99 raise ValueError("Invalid _version.")
100
101 ipaddress._IPAddressBase.vapi_af = property(_vapi_af)
102 ipaddress._IPAddressBase.vapi_af_name = property(_vapi_af_name)
103
104
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500105class VppApiDynamicMethodHolder:
Klement Sekera7112c542017-03-01 09:53:19 +0100106 pass
107
108
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500109class FuncWrapper:
Klement Sekera7112c542017-03-01 09:53:19 +0100110 def __init__(self, func):
111 self._func = func
112 self.__name__ = func.__name__
Paul Vinciguerrab8daa252019-03-19 15:04:17 -0700113 self.__doc__ = func.__doc__
Klement Sekera7112c542017-03-01 09:53:19 +0100114
115 def __call__(self, **kwargs):
116 return self._func(**kwargs)
117
Paul Vinciguerra48664592019-06-19 22:19:02 -0400118 def __repr__(self):
119 return '<FuncWrapper(func=<%s(%s)>)>' % (self.__name__, self.__doc__)
120
Klement Sekera7112c542017-03-01 09:53:19 +0100121
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800122class VPPApiError(Exception):
123 pass
124
125
126class VPPNotImplementedError(NotImplementedError):
127 pass
128
129
130class VPPIOError(IOError):
131 pass
132
133
134class VPPRuntimeError(RuntimeError):
135 pass
136
137
138class VPPValueError(ValueError):
139 pass
140
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400141
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500142class VPPApiJSONFiles:
Chris Luke52bf22e2017-11-03 23:32:38 -0400143 @classmethod
Ole Troanedfe2c02019-07-30 15:38:13 +0200144 def find_api_dir(cls, dirs):
Chris Luke52bf22e2017-11-03 23:32:38 -0400145 """Attempt to find the best directory in which API definition
146 files may reside. If the value VPP_API_DIR exists in the environment
147 then it is first on the search list. If we're inside a recognized
148 location in a VPP source tree (src/scripts and src/vpp-api/python)
149 then entries from there to the likely locations in build-root are
150 added. Finally the location used by system packages is added.
151
152 :returns: A single directory name, or None if no such directory
153 could be found.
154 """
Chris Luke52bf22e2017-11-03 23:32:38 -0400155
156 # perhaps we're in the 'src/scripts' or 'src/vpp-api/python' dir;
157 # in which case, plot a course to likely places in the src tree
158 import __main__ as main
159 if hasattr(main, '__file__'):
160 # get the path of the calling script
161 localdir = os.path.dirname(os.path.realpath(main.__file__))
162 else:
163 # use cwd if there is no calling script
Andrey "Zed" Zaikin68e2ffb2018-04-24 14:50:02 +0300164 localdir = os.getcwd()
Chris Luke52bf22e2017-11-03 23:32:38 -0400165 localdir_s = localdir.split(os.path.sep)
166
167 def dmatch(dir):
168 """Match dir against right-hand components of the script dir"""
169 d = dir.split('/') # param 'dir' assumes a / separator
Ole Troanafddd832018-02-28 14:55:20 +0100170 length = len(d)
171 return len(localdir_s) > length and localdir_s[-length:] == d
Chris Luke52bf22e2017-11-03 23:32:38 -0400172
173 def sdir(srcdir, variant):
174 """Build a path from srcdir to the staged API files of
175 'variant' (typically '' or '_debug')"""
176 # Since 'core' and 'plugin' files are staged
177 # in separate directories, we target the parent dir.
178 return os.path.sep.join((
179 srcdir,
180 'build-root',
181 'install-vpp%s-native' % variant,
182 'vpp',
183 'share',
184 'vpp',
185 'api',
186 ))
187
188 srcdir = None
189 if dmatch('src/scripts'):
190 srcdir = os.path.sep.join(localdir_s[:-2])
191 elif dmatch('src/vpp-api/python'):
192 srcdir = os.path.sep.join(localdir_s[:-3])
193 elif dmatch('test'):
194 # we're apparently running tests
195 srcdir = os.path.sep.join(localdir_s[:-1])
196
197 if srcdir:
198 # we're in the source tree, try both the debug and release
199 # variants.
Chris Luke52bf22e2017-11-03 23:32:38 -0400200 dirs.append(sdir(srcdir, '_debug'))
201 dirs.append(sdir(srcdir, ''))
202
203 # Test for staged copies of the scripts
204 # For these, since we explicitly know if we're running a debug versus
205 # release variant, target only the relevant directory
206 if dmatch('build-root/install-vpp_debug-native/vpp/bin'):
207 srcdir = os.path.sep.join(localdir_s[:-4])
208 dirs.append(sdir(srcdir, '_debug'))
209 if dmatch('build-root/install-vpp-native/vpp/bin'):
210 srcdir = os.path.sep.join(localdir_s[:-4])
211 dirs.append(sdir(srcdir, ''))
212
213 # finally, try the location system packages typically install into
214 dirs.append(os.path.sep.join(('', 'usr', 'share', 'vpp', 'api')))
215
Paul Vinciguerra19542292019-03-17 17:34:46 -0700216 # check the directories for existence; first one wins
Chris Luke52bf22e2017-11-03 23:32:38 -0400217 for dir in dirs:
218 if os.path.isdir(dir):
219 return dir
220
221 return None
222
223 @classmethod
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500224 def find_api_files(cls, api_dir=None, patterns='*'): # -> list
Chris Luke52bf22e2017-11-03 23:32:38 -0400225 """Find API definition files from the given directory tree with the
226 given pattern. If no directory is given then find_api_dir() is used
227 to locate one. If no pattern is given then all definition files found
228 in the directory tree are used.
229
230 :param api_dir: A directory tree in which to locate API definition
231 files; subdirectories are descended into.
232 If this is None then find_api_dir() is called to discover it.
233 :param patterns: A list of patterns to use in each visited directory
234 when looking for files.
235 This can be a list/tuple object or a comma-separated string of
236 patterns. Each value in the list will have leading/trialing
237 whitespace stripped.
238 The pattern specifies the first part of the filename, '.api.json'
239 is appended.
240 The results are de-duplicated, thus overlapping patterns are fine.
241 If this is None it defaults to '*' meaning "all API files".
242 :returns: A list of file paths for the API files found.
243 """
244 if api_dir is None:
Ole Troanedfe2c02019-07-30 15:38:13 +0200245 api_dir = cls.find_api_dir([])
Chris Luke52bf22e2017-11-03 23:32:38 -0400246 if api_dir is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800247 raise VPPApiError("api_dir cannot be located")
Chris Luke52bf22e2017-11-03 23:32:38 -0400248
249 if isinstance(patterns, list) or isinstance(patterns, tuple):
250 patterns = [p.strip() + '.api.json' for p in patterns]
251 else:
252 patterns = [p.strip() + '.api.json' for p in patterns.split(",")]
253
254 api_files = []
255 for root, dirnames, files in os.walk(api_dir):
256 # iterate all given patterns and de-dup the result
257 files = set(sum([fnmatch.filter(files, p) for p in patterns], []))
258 for filename in files:
259 api_files.append(os.path.join(root, filename))
260
261 return api_files
262
Ole Troanedfe2c02019-07-30 15:38:13 +0200263 @classmethod
264 def process_json_file(self, apidef_file):
265 api = json.load(apidef_file)
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500266 return self._process_json(api)
267
268 @classmethod
269 def process_json_str(self, json_str):
270 api = json.loads(json_str)
271 return self._process_json(api)
272
273 @staticmethod
274 def _process_json(api): # -> Tuple[Dict, Dict]
Ole Troanedfe2c02019-07-30 15:38:13 +0200275 types = {}
276 services = {}
277 messages = {}
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500278 try:
279 for t in api['enums']:
280 t[0] = 'vl_api_' + t[0] + '_t'
281 types[t[0]] = {'type': 'enum', 'data': t}
282 except KeyError:
283 pass
284
285 try:
286 for t in api['unions']:
287 t[0] = 'vl_api_' + t[0] + '_t'
288 types[t[0]] = {'type': 'union', 'data': t}
289 except KeyError:
290 pass
291
292 try:
293 for t in api['types']:
294 t[0] = 'vl_api_' + t[0] + '_t'
295 types[t[0]] = {'type': 'type', 'data': t}
296 except KeyError:
297 pass
298
299 try:
300 for t, v in api['aliases'].items():
301 types['vl_api_' + t + '_t'] = {'type': 'alias', 'data': v}
302 except KeyError:
303 pass
304
305 try:
306 services.update(api['services'])
307 except KeyError:
308 pass
Ole Troanedfe2c02019-07-30 15:38:13 +0200309
310 i = 0
311 while True:
312 unresolved = {}
313 for k, v in types.items():
314 t = v['data']
315 if not vpp_get_type(k):
316 if v['type'] == 'enum':
317 try:
318 VPPEnumType(t[0], t[1:])
319 except ValueError:
320 unresolved[k] = v
321 elif v['type'] == 'union':
322 try:
323 VPPUnionType(t[0], t[1:])
324 except ValueError:
325 unresolved[k] = v
326 elif v['type'] == 'type':
327 try:
328 VPPType(t[0], t[1:])
329 except ValueError:
330 unresolved[k] = v
331 elif v['type'] == 'alias':
332 try:
333 VPPTypeAlias(k, t)
334 except ValueError:
335 unresolved[k] = v
336 if len(unresolved) == 0:
337 break
338 if i > 3:
339 raise VPPValueError('Unresolved type definitions {}'
340 .format(unresolved))
341 types = unresolved
342 i += 1
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500343 try:
344 for m in api['messages']:
345 try:
346 messages[m[0]] = VPPMessage(m[0], m[1:])
347 except VPPNotImplementedError:
348 ### OLE FIXME
349 logger.error('Not implemented error for {}'.format(m[0]))
350 except KeyError:
351 pass
Ole Troanedfe2c02019-07-30 15:38:13 +0200352 return messages, services
353
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400354
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500355class VPPApiClient:
Ole Troanedfe2c02019-07-30 15:38:13 +0200356 """VPP interface.
357
358 This class provides the APIs to VPP. The APIs are loaded
359 from provided .api.json files and makes functions accordingly.
360 These functions are documented in the VPP .api files, as they
361 are dynamically created.
362
363 Additionally, VPP can send callback messages; this class
364 provides a means to register a callback function to receive
365 these messages in a background thread.
366 """
367 apidir = None
368 VPPApiError = VPPApiError
369 VPPRuntimeError = VPPRuntimeError
370 VPPValueError = VPPValueError
371 VPPNotImplementedError = VPPNotImplementedError
372 VPPIOError = VPPIOError
373
374
375 def __init__(self, apifiles=None, testmode=False, async_thread=True,
376 logger=None, loglevel=None,
377 read_timeout=5, use_socket=False,
378 server_address='/run/vpp/api.sock'):
379 """Create a VPP API object.
380
381 apifiles is a list of files containing API
382 descriptions that will be loaded - methods will be
383 dynamically created reflecting these APIs. If not
384 provided this will load the API files from VPP's
385 default install location.
386
387 logger, if supplied, is the logging logger object to log to.
388 loglevel, if supplied, is the log level this logger is set
389 to report at (from the loglevels in the logging module).
390 """
391 if logger is None:
392 logger = logging.getLogger(
393 "{}.{}".format(__name__, self.__class__.__name__))
394 if loglevel is not None:
395 logger.setLevel(loglevel)
396 self.logger = logger
397
398 self.messages = {}
399 self.services = {}
400 self.id_names = []
401 self.id_msgdef = []
402 self.header = VPPType('header', [['u16', 'msgid'],
403 ['u32', 'client_index']])
404 self.apifiles = []
405 self.event_callback = None
406 self.message_queue = queue.Queue()
407 self.read_timeout = read_timeout
408 self.async_thread = async_thread
409 self.event_thread = None
410 self.testmode = testmode
411 self.use_socket = use_socket
412 self.server_address = server_address
413 self._apifiles = apifiles
Ole Troanfd574082019-11-27 23:12:48 +0100414 self.stats = {}
Ole Troanedfe2c02019-07-30 15:38:13 +0200415
416 if use_socket:
417 from . vpp_transport_socket import VppTransport
418 else:
419 from . vpp_transport_shmem import VppTransport
420
421 if not apifiles:
422 # Pick up API definitions from default directory
423 try:
424 apifiles = VPPApiJSONFiles.find_api_files(self.apidir)
Paul Vinciguerra46d68642020-12-01 02:00:35 -0500425 except (RuntimeError, VPPApiError):
Ole Troanedfe2c02019-07-30 15:38:13 +0200426 # In test mode we don't care that we can't find the API files
427 if testmode:
428 apifiles = []
429 else:
430 raise VPPRuntimeError
431
432 for file in apifiles:
433 with open(file) as apidef_file:
434 m, s = VPPApiJSONFiles.process_json_file(apidef_file)
435 self.messages.update(m)
436 self.services.update(s)
437
438 self.apifiles = apifiles
439
440 # Basic sanity check
441 if len(self.messages) == 0 and not testmode:
442 raise VPPValueError(1, 'Missing JSON message definitions')
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400443 if not(verify_enum_hint(VppEnum.vl_api_address_family_t)):
444 raise VPPRuntimeError("Invalid address family hints. "
445 "Cannot continue.")
Ole Troanedfe2c02019-07-30 15:38:13 +0200446
447 self.transport = VppTransport(self, read_timeout=read_timeout,
448 server_address=server_address)
449 # Make sure we allow VPP to clean up the message rings.
450 atexit.register(vpp_atexit, weakref.ref(self))
451
Paul Vinciguerrae64e5ff2020-04-28 00:27:38 -0400452 add_convenience_methods()
453
Ole Troanedfe2c02019-07-30 15:38:13 +0200454 def get_function(self, name):
455 return getattr(self._api, name)
456
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500457 class ContextId:
Ole Troanedfe2c02019-07-30 15:38:13 +0200458 """Multiprocessing-safe provider of unique context IDs."""
459 def __init__(self):
460 self.context = mp.Value(ctypes.c_uint, 0)
461 self.lock = mp.Lock()
462
463 def __call__(self):
464 """Get a new unique (or, at least, not recently used) context."""
465 with self.lock:
466 self.context.value += 1
467 return self.context.value
468 get_context = ContextId()
469
470 def get_type(self, name):
471 return vpp_get_type(name)
472
Klement Sekera7112c542017-03-01 09:53:19 +0100473 @property
474 def api(self):
475 if not hasattr(self, "_api"):
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800476 raise VPPApiError("Not connected, api definitions not available")
Klement Sekera7112c542017-03-01 09:53:19 +0100477 return self._api
478
Ole Troaneabd6072018-08-09 12:50:55 +0200479 def make_function(self, msg, i, multipart, do_async):
480 if (do_async):
Ole Troana7564e82018-06-12 21:06:44 +0200481 def f(**kwargs):
482 return self._call_vpp_async(i, msg, **kwargs)
483 else:
484 def f(**kwargs):
485 return self._call_vpp(i, msg, multipart, **kwargs)
486
487 f.__name__ = str(msg.name)
488 f.__doc__ = ", ".join(["%s %s" %
489 (msg.fieldtypes[j], k)
490 for j, k in enumerate(msg.fields)])
Ole Troanf159f582019-02-28 20:20:47 +0100491 f.msg = msg
492
Ole Troana7564e82018-06-12 21:06:44 +0200493 return f
494
Ole Troaneabd6072018-08-09 12:50:55 +0200495 def _register_functions(self, do_async=False):
Ole Troana03f4ef2016-12-02 12:53:55 +0100496 self.id_names = [None] * (self.vpp_dictionary_maxid + 1)
497 self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1)
Klement Sekera8aedf5e2018-07-06 11:07:21 +0200498 self._api = VppApiDynamicMethodHolder()
Paul Vinciguerrad7adc292020-12-02 14:34:27 -0500499 for name, msg in self.messages.items():
Ole Troana7564e82018-06-12 21:06:44 +0200500 n = name + '_' + msg.crc[2:]
Ole Troandaa4bff2019-08-28 14:12:02 +0200501 i = self.transport.get_msg_index(n)
Ole Troan3cc49712017-03-08 12:02:24 +0100502 if i > 0:
Ole Troana7564e82018-06-12 21:06:44 +0200503 self.id_msgdef[i] = msg
Ole Troana03f4ef2016-12-02 12:53:55 +0100504 self.id_names[i] = name
Ole Troandfb984d2018-12-07 14:31:16 +0100505
506 # Create function for client side messages.
507 if name in self.services:
Ole Troanf5db3712020-05-20 15:47:06 +0200508 f = self.make_function(msg, i, self.services[name], do_async)
Ole Troandfb984d2018-12-07 14:31:16 +0100509 setattr(self._api, name, FuncWrapper(f))
Ole Troan3cc49712017-03-08 12:02:24 +0100510 else:
Ole Troan4df97162017-07-07 16:06:08 +0200511 self.logger.debug(
512 'No such message type or failed CRC checksum: %s', n)
Ole Troana03f4ef2016-12-02 12:53:55 +0100513
Ole Troan4df97162017-07-07 16:06:08 +0200514 def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200515 do_async):
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700516 pfx = chroot_prefix.encode('utf-8') if chroot_prefix else None
Ole Troan94495f22018-08-02 11:58:12 +0200517
Ole Troandaa4bff2019-08-28 14:12:02 +0200518 rv = self.transport.connect(name, pfx,
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700519 msg_handler, rx_qlen)
Ole Troana03f4ef2016-12-02 12:53:55 +0100520 if rv != 0:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800521 raise VPPIOError(2, 'Connect failed')
Ole Troan94495f22018-08-02 11:58:12 +0200522 self.vpp_dictionary_maxid = self.transport.msg_table_max_index()
Ole Troaneabd6072018-08-09 12:50:55 +0200523 self._register_functions(do_async=do_async)
Ole Troana03f4ef2016-12-02 12:53:55 +0100524
525 # Initialise control ping
Ole Troana7564e82018-06-12 21:06:44 +0200526 crc = self.messages['control_ping'].crc
Ole Troan94495f22018-08-02 11:58:12 +0200527 self.control_ping_index = self.transport.get_msg_index(
Ole Troandaa4bff2019-08-28 14:12:02 +0200528 ('control_ping' + '_' + crc[2:]))
Ole Troana03f4ef2016-12-02 12:53:55 +0100529 self.control_ping_msgdef = self.messages['control_ping']
Klement Sekera180402d2018-02-17 10:58:37 +0100530 if self.async_thread:
531 self.event_thread = threading.Thread(
532 target=self.thread_msg_handler)
533 self.event_thread.daemon = True
534 self.event_thread.start()
Vratko Polak94e45312019-05-27 18:36:23 +0200535 else:
536 self.event_thread = None
Ole Troan4df97162017-07-07 16:06:08 +0200537 return rv
Ole Troana03f4ef2016-12-02 12:53:55 +0100538
Ole Troaneabd6072018-08-09 12:50:55 +0200539 def connect(self, name, chroot_prefix=None, do_async=False, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100540 """Attach to VPP.
541
542 name - the name of the client.
543 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
Ole Troaneabd6072018-08-09 12:50:55 +0200544 do_async - if true, messages are sent without waiting for a reply
Ole Troandfc9b7c2017-03-06 23:51:57 +0100545 rx_qlen - the length of the VPP message receive queue between
546 client and server.
547 """
Ole Troan94495f22018-08-02 11:58:12 +0200548 msg_handler = self.transport.get_callback(do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100549 return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200550 do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100551
Ole Troan6bf177c2017-08-17 10:34:32 +0200552 def connect_sync(self, name, chroot_prefix=None, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100553 """Attach to VPP in synchronous mode. Application must poll for events.
554
555 name - the name of the client.
556 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
557 rx_qlen - the length of the VPP message receive queue between
558 client and server.
559 """
560
Ole Troan94495f22018-08-02 11:58:12 +0200561 return self.connect_internal(name, None, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200562 do_async=False)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100563
Ole Troana03f4ef2016-12-02 12:53:55 +0100564 def disconnect(self):
Ole Troan5016f992017-01-19 09:44:44 +0100565 """Detach from VPP."""
Ole Troan94495f22018-08-02 11:58:12 +0200566 rv = self.transport.disconnect()
Vratko Polak94e45312019-05-27 18:36:23 +0200567 if self.event_thread is not None:
568 self.message_queue.put("terminate event thread")
Ole Troana03f4ef2016-12-02 12:53:55 +0100569 return rv
570
Ole Troan5016f992017-01-19 09:44:44 +0100571 def msg_handler_sync(self, msg):
572 """Process an incoming message from VPP in sync mode.
573
574 The message may be a reply or it may be an async notification.
575 """
576 r = self.decode_incoming_msg(msg)
577 if r is None:
Ole Troana03f4ef2016-12-02 12:53:55 +0100578 return
579
Ole Troan5016f992017-01-19 09:44:44 +0100580 # If we have a context, then use the context to find any
581 # request waiting for a reply
582 context = 0
583 if hasattr(r, 'context') and r.context > 0:
584 context = r.context
Ole Troan5f9dcff2016-08-01 04:59:13 +0200585
Ole Troan5016f992017-01-19 09:44:44 +0100586 if context == 0:
587 # No context -> async notification that we feed to the callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100588 self.message_queue.put_nowait(r)
Ole Troana03f4ef2016-12-02 12:53:55 +0100589 else:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800590 raise VPPIOError(2, 'RPC reply message received in event handler')
Ole Troan5016f992017-01-19 09:44:44 +0100591
Ole Troan413f4a52018-11-28 11:36:05 +0100592 def has_context(self, msg):
593 if len(msg) < 10:
594 return False
595
596 header = VPPType('header_with_context', [['u16', 'msgid'],
597 ['u32', 'client_index'],
598 ['u32', 'context']])
599
600 (i, ci, context), size = header.unpack(msg, 0)
601 if self.id_names[i] == 'rx_thread_exit':
602 return
603
604 #
605 # Decode message and returns a tuple.
606 #
607 msgobj = self.id_msgdef[i]
608 if 'context' in msgobj.field_by_name and context >= 0:
609 return True
610 return False
611
Ole Troan0bcad322018-12-11 13:04:01 +0100612 def decode_incoming_msg(self, msg, no_type_conversion=False):
Ole Troan5016f992017-01-19 09:44:44 +0100613 if not msg:
Paul Vinciguerra5395c6a2020-12-02 17:43:59 -0500614 logger.warning('vpp_api.read failed')
Ole Troan5016f992017-01-19 09:44:44 +0100615 return
Ole Troan413f4a52018-11-28 11:36:05 +0100616
Ole Troanc84cbad2018-09-06 22:58:05 +0200617 (i, ci), size = self.header.unpack(msg, 0)
Ole Troan5016f992017-01-19 09:44:44 +0100618 if self.id_names[i] == 'rx_thread_exit':
619 return
620
621 #
622 # Decode message and returns a tuple.
623 #
Ole Troana7564e82018-06-12 21:06:44 +0200624 msgobj = self.id_msgdef[i]
625 if not msgobj:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800626 raise VPPIOError(2, 'Reply message undefined')
Ole Troan5016f992017-01-19 09:44:44 +0100627
Ole Troan0bcad322018-12-11 13:04:01 +0100628 r, size = msgobj.unpack(msg, ntc=no_type_conversion)
Ole Troana03f4ef2016-12-02 12:53:55 +0100629 return r
630
Ole Troan5016f992017-01-19 09:44:44 +0100631 def msg_handler_async(self, msg):
632 """Process a message from VPP in async mode.
633
634 In async mode, all messages are returned to the callback.
635 """
636 r = self.decode_incoming_msg(msg)
637 if r is None:
638 return
639
640 msgname = type(r).__name__
641
Ole Troan4df97162017-07-07 16:06:08 +0200642 if self.event_callback:
643 self.event_callback(msgname, r)
Ole Troan5016f992017-01-19 09:44:44 +0100644
645 def _control_ping(self, context):
646 """Send a ping command."""
647 self._call_vpp_async(self.control_ping_index,
Ole Troan4df97162017-07-07 16:06:08 +0200648 self.control_ping_msgdef,
Ole Troan5016f992017-01-19 09:44:44 +0100649 context=context)
650
Ole Troana7564e82018-06-12 21:06:44 +0200651 def validate_args(self, msg, kwargs):
652 d = set(kwargs.keys()) - set(msg.field_by_name.keys())
653 if d:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800654 raise VPPValueError('Invalid argument {} to {}'
Ole Troan0bcad322018-12-11 13:04:01 +0100655 .format(list(d), msg.name))
Ole Troana7564e82018-06-12 21:06:44 +0200656
Ole Troanfd574082019-11-27 23:12:48 +0100657 def _add_stat(self, name, ms):
658 if not name in self.stats:
659 self.stats[name] = {'max': ms, 'count': 1, 'avg': ms}
660 else:
661 if ms > self.stats[name]['max']:
662 self.stats[name]['max'] = ms
663 self.stats[name]['count'] += 1
664 n = self.stats[name]['count']
665 self.stats[name]['avg'] = self.stats[name]['avg'] * (n - 1) / n + ms / n
666
667 def get_stats(self):
668 s = '\n=== API PAPI STATISTICS ===\n'
669 s += '{:<30} {:>4} {:>6} {:>6}\n'.format('message', 'cnt', 'avg', 'max')
670 for n in sorted(self.stats.items(), key=lambda v: v[1]['avg'], reverse=True):
671 s += '{:<30} {:>4} {:>6.2f} {:>6.2f}\n'.format(n[0], n[1]['count'],
672 n[1]['avg'], n[1]['max'])
673 return s
674
Ole Troanf5db3712020-05-20 15:47:06 +0200675 def _call_vpp(self, i, msgdef, service, **kwargs):
Ole Troan5016f992017-01-19 09:44:44 +0100676 """Given a message, send the message and await a reply.
677
678 msgdef - the message packing definition
679 i - the message type index
680 multipart - True if the message returns multiple
681 messages in return.
682 context - context number - chosen at random if not
683 supplied.
684 The remainder of the kwargs are the arguments to the API call.
685
686 The return value is the message or message array containing
687 the response. It will raise an IOError exception if there was
688 no response within the timeout window.
689 """
Ole Troanfd574082019-11-27 23:12:48 +0100690 ts = time.time()
Ole Troan4df97162017-07-07 16:06:08 +0200691 if 'context' not in kwargs:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100692 context = self.get_context()
693 kwargs['context'] = context
694 else:
695 context = kwargs['context']
696 kwargs['_vl_msg_id'] = i
Ole Troan5016f992017-01-19 09:44:44 +0100697
Ole Troan0bcad322018-12-11 13:04:01 +0100698 no_type_conversion = kwargs.pop('_no_type_conversion', False)
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500699 timeout = kwargs.pop('_timeout', None)
Ole Troan0bcad322018-12-11 13:04:01 +0100700
Ole Troan94495f22018-08-02 11:58:12 +0200701 try:
702 if self.transport.socket_index:
703 kwargs['client_index'] = self.transport.socket_index
704 except AttributeError:
705 pass
Ole Troan413f4a52018-11-28 11:36:05 +0100706 self.validate_args(msgdef, kwargs)
707
Vratko Polakb6590202019-07-16 14:32:55 +0200708 s = 'Calling {}({})'.format(msgdef.name,
709 ','.join(['{!r}:{!r}'.format(k, v) for k, v in kwargs.items()]))
710 self.logger.debug(s)
Ole Troan413f4a52018-11-28 11:36:05 +0100711
712 b = msgdef.pack(kwargs)
Ole Troan94495f22018-08-02 11:58:12 +0200713 self.transport.suspend()
714
715 self.transport.write(b)
Ole Troan5016f992017-01-19 09:44:44 +0100716
Ole Troanf5db3712020-05-20 15:47:06 +0200717 msgreply = service['reply']
718 stream = True if 'stream' in service else False
719 if stream:
720 if 'stream_msg' in service:
721 # New service['reply'] = _reply and service['stream_message'] = _details
722 stream_message = service['stream_msg']
723 modern =True
724 else:
725 # Old service['reply'] = _details
726 stream_message = msgreply
727 msgreply = 'control_ping_reply'
728 modern = False
729 # Send a ping after the request - we use its response
730 # to detect that we have seen all results.
731 self._control_ping(context)
Ole Troan5016f992017-01-19 09:44:44 +0100732
733 # Block until we get a reply.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100734 rl = []
735 while (True):
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500736 r = self.read_blocking(no_type_conversion, timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200737 if r is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800738 raise VPPIOError(2, 'VPP API client: read failed')
Ole Troandfc9b7c2017-03-06 23:51:57 +0100739 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200740 if context not in r or r.context == 0 or context != r.context:
Ole Troan94495f22018-08-02 11:58:12 +0200741 # Message being queued
Ole Troandfc9b7c2017-03-06 23:51:57 +0100742 self.message_queue.put_nowait(r)
743 continue
Ole Troanf5db3712020-05-20 15:47:06 +0200744 if msgname != msgreply and (stream and (msgname != stream_message)):
745 print('REPLY MISMATCH', msgreply, msgname, stream_message, stream)
746 if not stream:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100747 rl = r
748 break
Ole Troanf5db3712020-05-20 15:47:06 +0200749 if msgname == msgreply:
750 if modern: # Return both reply and list
751 rl = r, rl
Ole Troandfc9b7c2017-03-06 23:51:57 +0100752 break
753
754 rl.append(r)
755
Ole Troan94495f22018-08-02 11:58:12 +0200756 self.transport.resume()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100757
Klement Sekera5e2f84d2019-09-12 09:01:06 +0000758 s = 'Return value: {!r}'.format(r)
759 if len(s) > 80:
760 s = s[:80] + "..."
761 self.logger.debug(s)
Ole Troanfd574082019-11-27 23:12:48 +0100762 te = time.time()
763 self._add_stat(msgdef.name, (te - ts) * 1000)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100764 return rl
Ole Troan5016f992017-01-19 09:44:44 +0100765
Ole Troana7564e82018-06-12 21:06:44 +0200766 def _call_vpp_async(self, i, msg, **kwargs):
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200767 """Given a message, send the message and return the context.
Ole Troan5016f992017-01-19 09:44:44 +0100768
769 msgdef - the message packing definition
770 i - the message type index
771 context - context number - chosen at random if not
772 supplied.
773 The remainder of the kwargs are the arguments to the API call.
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200774
775 The reply message(s) will be delivered later to the registered callback.
776 The returned context will help with assigning which call
777 the reply belongs to.
Ole Troan5016f992017-01-19 09:44:44 +0100778 """
Ole Troan4df97162017-07-07 16:06:08 +0200779 if 'context' not in kwargs:
Ole Troan7e3a8752016-12-05 10:27:09 +0100780 context = self.get_context()
781 kwargs['context'] = context
782 else:
783 context = kwargs['context']
Ole Troan94495f22018-08-02 11:58:12 +0200784 try:
785 if self.transport.socket_index:
786 kwargs['client_index'] = self.transport.socket_index
787 except AttributeError:
788 kwargs['client_index'] = 0
Ole Troan7e3a8752016-12-05 10:27:09 +0100789 kwargs['_vl_msg_id'] = i
Ole Troana7564e82018-06-12 21:06:44 +0200790 b = msg.pack(kwargs)
Ole Troan7e3a8752016-12-05 10:27:09 +0100791
Ole Troan94495f22018-08-02 11:58:12 +0200792 self.transport.write(b)
Vratko Polak2f6e0c62019-09-06 15:20:07 +0200793 return context
Ole Troan7e3a8752016-12-05 10:27:09 +0100794
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500795 def read_blocking(self, no_type_conversion=False, timeout=None):
Vratko Polak09385472019-09-10 13:35:11 +0200796 """Get next received message from transport within timeout, decoded.
797
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500798 Note that notifications have context zero
Vratko Polak09385472019-09-10 13:35:11 +0200799 and are not put into receive queue (at least for socket transport),
800 use async_thread with registered callback for processing them.
801
802 If no message appears in the queue within timeout, return None.
803
804 Optionally, type conversion can be skipped,
805 as some of conversions are into less precise types.
806
807 When r is the return value of this, the caller can get message name as:
808 msgname = type(r).__name__
809 and context number (type long) as:
810 context = r.context
811
812 :param no_type_conversion: If false, type conversions are applied.
813 :type no_type_conversion: bool
814 :returns: Decoded message, or None if no message (within timeout).
815 :rtype: Whatever VPPType.unpack returns, depends on no_type_conversion.
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500816 :raises VppTransportShmemIOError if timed out.
Vratko Polak09385472019-09-10 13:35:11 +0200817 """
Paul Vinciguerrae2ccdf02019-12-02 13:40:33 -0500818 msg = self.transport.read(timeout=timeout)
Vratko Polak09385472019-09-10 13:35:11 +0200819 if not msg:
820 return None
821 return self.decode_incoming_msg(msg, no_type_conversion)
822
Ole Troana03f4ef2016-12-02 12:53:55 +0100823 def register_event_callback(self, callback):
Ole Troan5016f992017-01-19 09:44:44 +0100824 """Register a callback for async messages.
Ole Troana03f4ef2016-12-02 12:53:55 +0100825
Ole Troan5016f992017-01-19 09:44:44 +0100826 This will be called for async notifications in sync mode,
827 and all messages in async mode. In sync mode, replies to
828 requests will not come here.
829
830 callback is a fn(msg_type_name, msg_type) that will be
831 called when a message comes in. While this function is
832 executing, note that (a) you are in a background thread and
833 may wish to use threading.Lock to protect your datastructures,
834 and (b) message processing from VPP will stop (so if you take
835 a long while about it you may provoke reply timeouts or cause
836 VPP to fill the RX buffer). Passing None will disable the
837 callback.
838 """
839 self.event_callback = callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100840
841 def thread_msg_handler(self):
Ole Troan94495f22018-08-02 11:58:12 +0200842 """Python thread calling the user registered message handler.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100843
844 This is to emulate the old style event callback scheme. Modern
845 clients should provide their own thread to poll the event
846 queue.
847 """
848 while True:
849 r = self.message_queue.get()
Klement Sekera180402d2018-02-17 10:58:37 +0100850 if r == "terminate event thread":
851 break
Ole Troandfc9b7c2017-03-06 23:51:57 +0100852 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200853 if self.event_callback:
854 self.event_callback(msgname, r)
Chris Luke52bf22e2017-11-03 23:32:38 -0400855
Ole Troanc046d702019-10-14 23:07:06 +0200856 def validate_message_table(self, namecrctable):
857 """Take a dictionary of name_crc message names
858 and returns an array of missing messages"""
859
860 missing_table = []
861 for name_crc in namecrctable:
862 i = self.transport.get_msg_index(name_crc)
863 if i <= 0:
864 missing_table.append(name_crc)
865 return missing_table
866
867 def dump_message_table(self):
868 """Return VPPs API message table as name_crc dictionary"""
869 return self.transport.message_table
870
871 def dump_message_table_filtered(self, msglist):
872 """Return VPPs API message table as name_crc dictionary,
873 filtered by message name list."""
874
875 replies = [self.services[n]['reply'] for n in msglist]
876 message_table_filtered = {}
877 for name in msglist + replies:
878 for k,v in self.transport.message_table.items():
879 if k.startswith(name):
880 message_table_filtered[k] = v
881 break
882 return message_table_filtered
883
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400884 def __repr__(self):
885 return "<VPPApiClient apifiles=%s, testmode=%s, async_thread=%s, " \
886 "logger=%s, read_timeout=%s, use_socket=%s, " \
887 "server_address='%s'>" % (
888 self._apifiles, self.testmode, self.async_thread,
889 self.logger, self.read_timeout, self.use_socket,
890 self.server_address)
891
Ole Troanf5db3712020-05-20 15:47:06 +0200892 def details_iter(self, f, **kwargs):
893 cursor = 0
894 while True:
895 kwargs['cursor'] = cursor
896 rv, details = f(**kwargs)
897 #
898 # Convert to yield from details when we only support python 3
899 #
900 for d in details:
901 yield d
902 if rv.retval == 0 or rv.retval != -165:
903 break
904 cursor = rv.cursor
Paul Vinciguerra8a2fa3b2019-06-16 21:23:31 -0400905
Paul Vinciguerra19542292019-03-17 17:34:46 -0700906# Provide the old name for backward compatibility.
907VPP = VPPApiClient
Chris Luke52bf22e2017-11-03 23:32:38 -0400908
909# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4