blob: 9849d2e487c7c2b3286ac50cea9a201381ef3582 [file] [log] [blame]
Ole Troana03f4ef2016-12-02 12:53:55 +01001#!/usr/bin/env python
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
Ole Troan4df97162017-07-07 16:06:08 +020019import sys
20import os
21import logging
22import collections
23import struct
Paul Vinciguerra5fced042019-02-26 20:39:44 -080024import functools
Ole Troan4df97162017-07-07 16:06:08 +020025import json
26import threading
Chris Luke52bf22e2017-11-03 23:32:38 -040027import fnmatch
Klement Sekera180402d2018-02-17 10:58:37 +010028import weakref
Ole Troan4df97162017-07-07 16:06:08 +020029import atexit
Ole Troana7564e82018-06-12 21:06:44 +020030from . vpp_serializer import VPPType, VPPEnumType, VPPUnionType, BaseTypes
Ole Troan53fffa12018-11-13 12:36:56 +010031from . vpp_serializer import VPPMessage, vpp_get_type, VPPTypeAlias
Ole Troan8006c6a2018-12-17 12:02:26 +010032from . macaddress import MACAddress, mac_pton, mac_ntop
Ole Troan4df97162017-07-07 16:06:08 +020033
Ole Troan413f4a52018-11-28 11:36:05 +010034logger = logging.getLogger(__name__)
35
Ole Troan4df97162017-07-07 16:06:08 +020036if sys.version[0] == '2':
37 import Queue as queue
38else:
39 import queue as queue
40
Ole Troanafddd832018-02-28 14:55:20 +010041
Paul Vinciguerra5fced042019-02-26 20:39:44 -080042def metaclass(metaclass):
43 @functools.wraps(metaclass)
44 def wrapper(cls):
45 return metaclass(cls.__name__, cls.__bases__, cls.__dict__.copy())
46
47 return wrapper
48
49
Ole Troan0685da42018-10-16 14:42:50 +020050class VppEnumType(type):
51 def __getattr__(cls, name):
52 t = vpp_get_type(name)
53 return t.enum
54
55
Paul Vinciguerra5fced042019-02-26 20:39:44 -080056@metaclass(VppEnumType)
Paul Vinciguerra7e713f12018-11-26 12:04:48 -080057class VppEnum(object):
Paul Vinciguerra5fced042019-02-26 20:39:44 -080058 pass
Ole Troan0685da42018-10-16 14:42:50 +020059
60
Klement Sekera180402d2018-02-17 10:58:37 +010061def vpp_atexit(vpp_weakref):
Ole Troan5016f992017-01-19 09:44:44 +010062 """Clean up VPP connection on shutdown."""
Klement Sekera180402d2018-02-17 10:58:37 +010063 vpp_instance = vpp_weakref()
Ole Troan94495f22018-08-02 11:58:12 +020064 if vpp_instance and vpp_instance.transport.connected:
Klement Sekera180402d2018-02-17 10:58:37 +010065 vpp_instance.logger.debug('Cleaning up VPP on exit')
66 vpp_instance.disconnect()
Ole Troan5016f992017-01-19 09:44:44 +010067
Ole Troan8006c6a2018-12-17 12:02:26 +010068
Ole Troan0bcad322018-12-11 13:04:01 +010069if sys.version[0] == '2':
70 def vpp_iterator(d):
Ole Troan4df97162017-07-07 16:06:08 +020071 return d.iteritems()
Ole Troan0bcad322018-12-11 13:04:01 +010072else:
73 def vpp_iterator(d):
Ole Troan4df97162017-07-07 16:06:08 +020074 return d.items()
75
76
Ole Troan413f4a52018-11-28 11:36:05 +010077def call_logger(msgdef, kwargs):
78 s = 'Calling {}('.format(msgdef.name)
79 for k, v in kwargs.items():
80 s += '{}:{} '.format(k, v)
81 s += ')'
82 return s
83
84
85def return_logger(r):
86 s = 'Return from {}'.format(r)
87 return s
88
89
Klement Sekera8aedf5e2018-07-06 11:07:21 +020090class VppApiDynamicMethodHolder(object):
Klement Sekera7112c542017-03-01 09:53:19 +010091 pass
92
93
94class FuncWrapper(object):
95 def __init__(self, func):
96 self._func = func
97 self.__name__ = func.__name__
Paul Vinciguerrab8daa252019-03-19 15:04:17 -070098 self.__doc__ = func.__doc__
Klement Sekera7112c542017-03-01 09:53:19 +010099
100 def __call__(self, **kwargs):
101 return self._func(**kwargs)
102
103
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800104class VPPApiError(Exception):
105 pass
106
107
108class VPPNotImplementedError(NotImplementedError):
109 pass
110
111
112class VPPIOError(IOError):
113 pass
114
115
116class VPPRuntimeError(RuntimeError):
117 pass
118
119
120class VPPValueError(ValueError):
121 pass
122
123
Paul Vinciguerra7e713f12018-11-26 12:04:48 -0800124class VPP(object):
Ole Troan5016f992017-01-19 09:44:44 +0100125 """VPP interface.
126
127 This class provides the APIs to VPP. The APIs are loaded
128 from provided .api.json files and makes functions accordingly.
129 These functions are documented in the VPP .api files, as they
130 are dynamically created.
131
132 Additionally, VPP can send callback messages; this class
133 provides a means to register a callback function to receive
134 these messages in a background thread.
135 """
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800136 VPPApiError = VPPApiError
137 VPPRuntimeError = VPPRuntimeError
138 VPPValueError = VPPValueError
139 VPPNotImplementedError = VPPNotImplementedError
140 VPPIOError = VPPIOError
Ole Troana7564e82018-06-12 21:06:44 +0200141
142 def process_json_file(self, apidef_file):
143 api = json.load(apidef_file)
144 types = {}
145 for t in api['enums']:
146 t[0] = 'vl_api_' + t[0] + '_t'
147 types[t[0]] = {'type': 'enum', 'data': t}
148 for t in api['unions']:
149 t[0] = 'vl_api_' + t[0] + '_t'
150 types[t[0]] = {'type': 'union', 'data': t}
151 for t in api['types']:
152 t[0] = 'vl_api_' + t[0] + '_t'
153 types[t[0]] = {'type': 'type', 'data': t}
Ole Troan53fffa12018-11-13 12:36:56 +0100154 for t, v in api['aliases'].items():
155 types['vl_api_' + t + '_t'] = {'type': 'alias', 'data': v}
Ole Troandfb984d2018-12-07 14:31:16 +0100156 self.services.update(api['services'])
Ole Troana7564e82018-06-12 21:06:44 +0200157
158 i = 0
159 while True:
160 unresolved = {}
161 for k, v in types.items():
162 t = v['data']
Ole Troan53fffa12018-11-13 12:36:56 +0100163 if not vpp_get_type(k):
Ole Troan0685da42018-10-16 14:42:50 +0200164 if v['type'] == 'enum':
165 try:
166 VPPEnumType(t[0], t[1:])
167 except ValueError:
168 unresolved[k] = v
169 elif v['type'] == 'union':
170 try:
171 VPPUnionType(t[0], t[1:])
172 except ValueError:
173 unresolved[k] = v
174 elif v['type'] == 'type':
175 try:
176 VPPType(t[0], t[1:])
177 except ValueError:
178 unresolved[k] = v
Ole Troan53fffa12018-11-13 12:36:56 +0100179 elif v['type'] == 'alias':
180 try:
181 VPPTypeAlias(k, t)
182 except ValueError:
183 unresolved[k] = v
Ole Troana7564e82018-06-12 21:06:44 +0200184 if len(unresolved) == 0:
185 break
186 if i > 3:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800187 raise VPPValueError('Unresolved type definitions {}'
188 .format(unresolved))
Ole Troana7564e82018-06-12 21:06:44 +0200189 types = unresolved
190 i += 1
191
192 for m in api['messages']:
193 try:
194 self.messages[m[0]] = VPPMessage(m[0], m[1:])
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800195 except VPPNotImplementedError:
Ole Troana7564e82018-06-12 21:06:44 +0200196 self.logger.error('Not implemented error for {}'.format(m[0]))
197
Ole Troan4df97162017-07-07 16:06:08 +0200198 def __init__(self, apifiles=None, testmode=False, async_thread=True,
Igor Mikhailov (imichail)5efd14f2018-10-30 12:17:49 -0700199 logger=None, loglevel=None,
Ole Troan94495f22018-08-02 11:58:12 +0200200 read_timeout=5, use_socket=False,
201 server_address='/run/vpp-api.sock'):
Ole Troan5016f992017-01-19 09:44:44 +0100202 """Create a VPP API object.
203
204 apifiles is a list of files containing API
205 descriptions that will be loaded - methods will be
206 dynamically created reflecting these APIs. If not
207 provided this will load the API files from VPP's
208 default install location.
Ian Wellsd0e812f2018-06-06 14:12:27 +0100209
210 logger, if supplied, is the logging logger object to log to.
211 loglevel, if supplied, is the log level this logger is set
212 to report at (from the loglevels in the logging module).
Ole Troan5016f992017-01-19 09:44:44 +0100213 """
Ian Wellsd0e812f2018-06-06 14:12:27 +0100214 if logger is None:
215 logger = logging.getLogger(__name__)
216 if loglevel is not None:
217 logger.setLevel(loglevel)
Ole Troan3cc49712017-03-08 12:02:24 +0100218 self.logger = logger
Ole Troan3cc49712017-03-08 12:02:24 +0100219
Ole Troana03f4ef2016-12-02 12:53:55 +0100220 self.messages = {}
Ole Troandfb984d2018-12-07 14:31:16 +0100221 self.services = {}
Ole Troana03f4ef2016-12-02 12:53:55 +0100222 self.id_names = []
223 self.id_msgdef = []
Ole Troana7564e82018-06-12 21:06:44 +0200224 self.header = VPPType('header', [['u16', 'msgid'],
225 ['u32', 'client_index']])
Ole Troan5016f992017-01-19 09:44:44 +0100226 self.apifiles = []
Ole Troan3d31f002017-01-26 11:13:00 +0100227 self.event_callback = None
Ole Troan4df97162017-07-07 16:06:08 +0200228 self.message_queue = queue.Queue()
dongjuan84937522017-11-09 14:46:36 +0800229 self.read_timeout = read_timeout
Klement Sekera180402d2018-02-17 10:58:37 +0100230 self.async_thread = async_thread
Ole Troan5f9dcff2016-08-01 04:59:13 +0200231
Ole Troan94495f22018-08-02 11:58:12 +0200232 if use_socket:
233 from . vpp_transport_socket import VppTransport
234 else:
235 from . vpp_transport_shmem import VppTransport
236
Ole Troanf5984bd2016-12-18 13:15:08 +0100237 if not apifiles:
238 # Pick up API definitions from default directory
Chris Luke52bf22e2017-11-03 23:32:38 -0400239 try:
240 apifiles = self.find_api_files()
241 except RuntimeError:
242 # In test mode we don't care that we can't find the API files
243 if testmode:
244 apifiles = []
245 else:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800246 raise VPPRuntimeError
Ole Troanf5984bd2016-12-18 13:15:08 +0100247
Ole Troana03f4ef2016-12-02 12:53:55 +0100248 for file in apifiles:
Ole Troana03f4ef2016-12-02 12:53:55 +0100249 with open(file) as apidef_file:
Ole Troana7564e82018-06-12 21:06:44 +0200250 self.process_json_file(apidef_file)
Ole Troan5f9dcff2016-08-01 04:59:13 +0200251
Ole Troan4df97162017-07-07 16:06:08 +0200252 self.apifiles = apifiles
Ole Troan5f9dcff2016-08-01 04:59:13 +0200253
Ole Troana03f4ef2016-12-02 12:53:55 +0100254 # Basic sanity check
Ole Troanf5984bd2016-12-18 13:15:08 +0100255 if len(self.messages) == 0 and not testmode:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800256 raise VPPValueError(1, 'Missing JSON message definitions')
Ole Troan5f9dcff2016-08-01 04:59:13 +0200257
Ole Troan94495f22018-08-02 11:58:12 +0200258 self.transport = VppTransport(self, read_timeout=read_timeout,
259 server_address=server_address)
Ole Troan5016f992017-01-19 09:44:44 +0100260 # Make sure we allow VPP to clean up the message rings.
Klement Sekera180402d2018-02-17 10:58:37 +0100261 atexit.register(vpp_atexit, weakref.ref(self))
Ole Troan5f9dcff2016-08-01 04:59:13 +0200262
Ole Troana03f4ef2016-12-02 12:53:55 +0100263 class ContextId(object):
Ole Troan5016f992017-01-19 09:44:44 +0100264 """Thread-safe provider of unique context IDs."""
Ole Troana03f4ef2016-12-02 12:53:55 +0100265 def __init__(self):
266 self.context = 0
Ole Troan4df97162017-07-07 16:06:08 +0200267 self.lock = threading.Lock()
268
Ole Troana03f4ef2016-12-02 12:53:55 +0100269 def __call__(self):
Ole Troan5016f992017-01-19 09:44:44 +0100270 """Get a new unique (or, at least, not recently used) context."""
Ole Troan4df97162017-07-07 16:06:08 +0200271 with self.lock:
272 self.context += 1
273 return self.context
Ole Troana03f4ef2016-12-02 12:53:55 +0100274 get_context = ContextId()
Ole Troan5f9dcff2016-08-01 04:59:13 +0200275
Ole Troan0685da42018-10-16 14:42:50 +0200276 def get_type(self, name):
277 return vpp_get_type(name)
278
Chris Luke52bf22e2017-11-03 23:32:38 -0400279 @classmethod
280 def find_api_dir(cls):
281 """Attempt to find the best directory in which API definition
282 files may reside. If the value VPP_API_DIR exists in the environment
283 then it is first on the search list. If we're inside a recognized
284 location in a VPP source tree (src/scripts and src/vpp-api/python)
285 then entries from there to the likely locations in build-root are
286 added. Finally the location used by system packages is added.
287
288 :returns: A single directory name, or None if no such directory
289 could be found.
290 """
291 dirs = []
292
293 if 'VPP_API_DIR' in os.environ:
294 dirs.append(os.environ['VPP_API_DIR'])
295
296 # perhaps we're in the 'src/scripts' or 'src/vpp-api/python' dir;
297 # in which case, plot a course to likely places in the src tree
298 import __main__ as main
299 if hasattr(main, '__file__'):
300 # get the path of the calling script
301 localdir = os.path.dirname(os.path.realpath(main.__file__))
302 else:
303 # use cwd if there is no calling script
Andrey "Zed" Zaikin68e2ffb2018-04-24 14:50:02 +0300304 localdir = os.getcwd()
Chris Luke52bf22e2017-11-03 23:32:38 -0400305 localdir_s = localdir.split(os.path.sep)
306
307 def dmatch(dir):
308 """Match dir against right-hand components of the script dir"""
309 d = dir.split('/') # param 'dir' assumes a / separator
Ole Troanafddd832018-02-28 14:55:20 +0100310 length = len(d)
311 return len(localdir_s) > length and localdir_s[-length:] == d
Chris Luke52bf22e2017-11-03 23:32:38 -0400312
313 def sdir(srcdir, variant):
314 """Build a path from srcdir to the staged API files of
315 'variant' (typically '' or '_debug')"""
316 # Since 'core' and 'plugin' files are staged
317 # in separate directories, we target the parent dir.
318 return os.path.sep.join((
319 srcdir,
320 'build-root',
321 'install-vpp%s-native' % variant,
322 'vpp',
323 'share',
324 'vpp',
325 'api',
326 ))
327
328 srcdir = None
329 if dmatch('src/scripts'):
330 srcdir = os.path.sep.join(localdir_s[:-2])
331 elif dmatch('src/vpp-api/python'):
332 srcdir = os.path.sep.join(localdir_s[:-3])
333 elif dmatch('test'):
334 # we're apparently running tests
335 srcdir = os.path.sep.join(localdir_s[:-1])
336
337 if srcdir:
338 # we're in the source tree, try both the debug and release
339 # variants.
Chris Luke52bf22e2017-11-03 23:32:38 -0400340 dirs.append(sdir(srcdir, '_debug'))
341 dirs.append(sdir(srcdir, ''))
342
343 # Test for staged copies of the scripts
344 # For these, since we explicitly know if we're running a debug versus
345 # release variant, target only the relevant directory
346 if dmatch('build-root/install-vpp_debug-native/vpp/bin'):
347 srcdir = os.path.sep.join(localdir_s[:-4])
348 dirs.append(sdir(srcdir, '_debug'))
349 if dmatch('build-root/install-vpp-native/vpp/bin'):
350 srcdir = os.path.sep.join(localdir_s[:-4])
351 dirs.append(sdir(srcdir, ''))
352
353 # finally, try the location system packages typically install into
354 dirs.append(os.path.sep.join(('', 'usr', 'share', 'vpp', 'api')))
355
356 # check the directories for existance; first one wins
357 for dir in dirs:
358 if os.path.isdir(dir):
359 return dir
360
361 return None
362
363 @classmethod
364 def find_api_files(cls, api_dir=None, patterns='*'):
365 """Find API definition files from the given directory tree with the
366 given pattern. If no directory is given then find_api_dir() is used
367 to locate one. If no pattern is given then all definition files found
368 in the directory tree are used.
369
370 :param api_dir: A directory tree in which to locate API definition
371 files; subdirectories are descended into.
372 If this is None then find_api_dir() is called to discover it.
373 :param patterns: A list of patterns to use in each visited directory
374 when looking for files.
375 This can be a list/tuple object or a comma-separated string of
376 patterns. Each value in the list will have leading/trialing
377 whitespace stripped.
378 The pattern specifies the first part of the filename, '.api.json'
379 is appended.
380 The results are de-duplicated, thus overlapping patterns are fine.
381 If this is None it defaults to '*' meaning "all API files".
382 :returns: A list of file paths for the API files found.
383 """
384 if api_dir is None:
385 api_dir = cls.find_api_dir()
386 if api_dir is None:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800387 raise VPPApiError("api_dir cannot be located")
Chris Luke52bf22e2017-11-03 23:32:38 -0400388
389 if isinstance(patterns, list) or isinstance(patterns, tuple):
390 patterns = [p.strip() + '.api.json' for p in patterns]
391 else:
392 patterns = [p.strip() + '.api.json' for p in patterns.split(",")]
393
394 api_files = []
395 for root, dirnames, files in os.walk(api_dir):
396 # iterate all given patterns and de-dup the result
397 files = set(sum([fnmatch.filter(files, p) for p in patterns], []))
398 for filename in files:
399 api_files.append(os.path.join(root, filename))
400
401 return api_files
402
Klement Sekera7112c542017-03-01 09:53:19 +0100403 @property
404 def api(self):
405 if not hasattr(self, "_api"):
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800406 raise VPPApiError("Not connected, api definitions not available")
Klement Sekera7112c542017-03-01 09:53:19 +0100407 return self._api
408
Ole Troaneabd6072018-08-09 12:50:55 +0200409 def make_function(self, msg, i, multipart, do_async):
410 if (do_async):
Ole Troana7564e82018-06-12 21:06:44 +0200411 def f(**kwargs):
412 return self._call_vpp_async(i, msg, **kwargs)
413 else:
414 def f(**kwargs):
415 return self._call_vpp(i, msg, multipart, **kwargs)
416
417 f.__name__ = str(msg.name)
418 f.__doc__ = ", ".join(["%s %s" %
419 (msg.fieldtypes[j], k)
420 for j, k in enumerate(msg.fields)])
Ole Troanf159f582019-02-28 20:20:47 +0100421 f.msg = msg
422
Ole Troana7564e82018-06-12 21:06:44 +0200423 return f
424
Ole Troaneabd6072018-08-09 12:50:55 +0200425 def _register_functions(self, do_async=False):
Ole Troana03f4ef2016-12-02 12:53:55 +0100426 self.id_names = [None] * (self.vpp_dictionary_maxid + 1)
427 self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1)
Klement Sekera8aedf5e2018-07-06 11:07:21 +0200428 self._api = VppApiDynamicMethodHolder()
Ole Troana7564e82018-06-12 21:06:44 +0200429 for name, msg in vpp_iterator(self.messages):
430 n = name + '_' + msg.crc[2:]
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700431 i = self.transport.get_msg_index(n.encode('utf-8'))
Ole Troan3cc49712017-03-08 12:02:24 +0100432 if i > 0:
Ole Troana7564e82018-06-12 21:06:44 +0200433 self.id_msgdef[i] = msg
Ole Troana03f4ef2016-12-02 12:53:55 +0100434 self.id_names[i] = name
Ole Troandfb984d2018-12-07 14:31:16 +0100435
436 # Create function for client side messages.
437 if name in self.services:
Ole Troan0bcad322018-12-11 13:04:01 +0100438 if 'stream' in self.services[name] and \
439 self.services[name]['stream']:
Ole Troandfb984d2018-12-07 14:31:16 +0100440 multipart = True
441 else:
442 multipart = False
443 f = self.make_function(msg, i, multipart, do_async)
444 setattr(self._api, name, FuncWrapper(f))
Ole Troan3cc49712017-03-08 12:02:24 +0100445 else:
Ole Troan4df97162017-07-07 16:06:08 +0200446 self.logger.debug(
447 'No such message type or failed CRC checksum: %s', n)
Ole Troana03f4ef2016-12-02 12:53:55 +0100448
Ole Troan4df97162017-07-07 16:06:08 +0200449 def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200450 do_async):
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700451 pfx = chroot_prefix.encode('utf-8') if chroot_prefix else None
Ole Troan94495f22018-08-02 11:58:12 +0200452
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700453 rv = self.transport.connect(name.encode('utf-8'), pfx,
454 msg_handler, rx_qlen)
Ole Troana03f4ef2016-12-02 12:53:55 +0100455 if rv != 0:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800456 raise VPPIOError(2, 'Connect failed')
Ole Troan94495f22018-08-02 11:58:12 +0200457 self.vpp_dictionary_maxid = self.transport.msg_table_max_index()
Ole Troaneabd6072018-08-09 12:50:55 +0200458 self._register_functions(do_async=do_async)
Ole Troana03f4ef2016-12-02 12:53:55 +0100459
460 # Initialise control ping
Ole Troana7564e82018-06-12 21:06:44 +0200461 crc = self.messages['control_ping'].crc
Ole Troan94495f22018-08-02 11:58:12 +0200462 self.control_ping_index = self.transport.get_msg_index(
Paul Vinciguerra3d04e332019-03-19 17:32:39 -0700463 ('control_ping' + '_' + crc[2:]).encode('utf-8'))
Ole Troana03f4ef2016-12-02 12:53:55 +0100464 self.control_ping_msgdef = self.messages['control_ping']
Klement Sekera180402d2018-02-17 10:58:37 +0100465 if self.async_thread:
466 self.event_thread = threading.Thread(
467 target=self.thread_msg_handler)
468 self.event_thread.daemon = True
469 self.event_thread.start()
Ole Troan4df97162017-07-07 16:06:08 +0200470 return rv
Ole Troana03f4ef2016-12-02 12:53:55 +0100471
Ole Troaneabd6072018-08-09 12:50:55 +0200472 def connect(self, name, chroot_prefix=None, do_async=False, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100473 """Attach to VPP.
474
475 name - the name of the client.
476 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
Ole Troaneabd6072018-08-09 12:50:55 +0200477 do_async - if true, messages are sent without waiting for a reply
Ole Troandfc9b7c2017-03-06 23:51:57 +0100478 rx_qlen - the length of the VPP message receive queue between
479 client and server.
480 """
Ole Troan94495f22018-08-02 11:58:12 +0200481 msg_handler = self.transport.get_callback(do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100482 return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200483 do_async)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100484
Ole Troan6bf177c2017-08-17 10:34:32 +0200485 def connect_sync(self, name, chroot_prefix=None, rx_qlen=32):
Ole Troandfc9b7c2017-03-06 23:51:57 +0100486 """Attach to VPP in synchronous mode. Application must poll for events.
487
488 name - the name of the client.
489 chroot_prefix - if VPP is chroot'ed, the prefix of the jail
490 rx_qlen - the length of the VPP message receive queue between
491 client and server.
492 """
493
Ole Troan94495f22018-08-02 11:58:12 +0200494 return self.connect_internal(name, None, chroot_prefix, rx_qlen,
Ole Troaneabd6072018-08-09 12:50:55 +0200495 do_async=False)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100496
Ole Troana03f4ef2016-12-02 12:53:55 +0100497 def disconnect(self):
Ole Troan5016f992017-01-19 09:44:44 +0100498 """Detach from VPP."""
Ole Troan94495f22018-08-02 11:58:12 +0200499 rv = self.transport.disconnect()
Klement Sekera180402d2018-02-17 10:58:37 +0100500 self.message_queue.put("terminate event thread")
Ole Troana03f4ef2016-12-02 12:53:55 +0100501 return rv
502
Ole Troan5016f992017-01-19 09:44:44 +0100503 def msg_handler_sync(self, msg):
504 """Process an incoming message from VPP in sync mode.
505
506 The message may be a reply or it may be an async notification.
507 """
508 r = self.decode_incoming_msg(msg)
509 if r is None:
Ole Troana03f4ef2016-12-02 12:53:55 +0100510 return
511
Ole Troan5016f992017-01-19 09:44:44 +0100512 # If we have a context, then use the context to find any
513 # request waiting for a reply
514 context = 0
515 if hasattr(r, 'context') and r.context > 0:
516 context = r.context
Ole Troan5f9dcff2016-08-01 04:59:13 +0200517
Ole Troan5016f992017-01-19 09:44:44 +0100518 if context == 0:
519 # No context -> async notification that we feed to the callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100520 self.message_queue.put_nowait(r)
Ole Troana03f4ef2016-12-02 12:53:55 +0100521 else:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800522 raise VPPIOError(2, 'RPC reply message received in event handler')
Ole Troan5016f992017-01-19 09:44:44 +0100523
Ole Troan413f4a52018-11-28 11:36:05 +0100524 def has_context(self, msg):
525 if len(msg) < 10:
526 return False
527
528 header = VPPType('header_with_context', [['u16', 'msgid'],
529 ['u32', 'client_index'],
530 ['u32', 'context']])
531
532 (i, ci, context), size = header.unpack(msg, 0)
533 if self.id_names[i] == 'rx_thread_exit':
534 return
535
536 #
537 # Decode message and returns a tuple.
538 #
539 msgobj = self.id_msgdef[i]
540 if 'context' in msgobj.field_by_name and context >= 0:
541 return True
542 return False
543
Ole Troan0bcad322018-12-11 13:04:01 +0100544 def decode_incoming_msg(self, msg, no_type_conversion=False):
Ole Troan5016f992017-01-19 09:44:44 +0100545 if not msg:
Ole Troan3cc49712017-03-08 12:02:24 +0100546 self.logger.warning('vpp_api.read failed')
Ole Troan5016f992017-01-19 09:44:44 +0100547 return
Ole Troan413f4a52018-11-28 11:36:05 +0100548
Ole Troanc84cbad2018-09-06 22:58:05 +0200549 (i, ci), size = self.header.unpack(msg, 0)
Ole Troan5016f992017-01-19 09:44:44 +0100550 if self.id_names[i] == 'rx_thread_exit':
551 return
552
553 #
554 # Decode message and returns a tuple.
555 #
Ole Troana7564e82018-06-12 21:06:44 +0200556 msgobj = self.id_msgdef[i]
557 if not msgobj:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800558 raise VPPIOError(2, 'Reply message undefined')
Ole Troan5016f992017-01-19 09:44:44 +0100559
Ole Troan0bcad322018-12-11 13:04:01 +0100560 r, size = msgobj.unpack(msg, ntc=no_type_conversion)
Ole Troana03f4ef2016-12-02 12:53:55 +0100561 return r
562
Ole Troan5016f992017-01-19 09:44:44 +0100563 def msg_handler_async(self, msg):
564 """Process a message from VPP in async mode.
565
566 In async mode, all messages are returned to the callback.
567 """
568 r = self.decode_incoming_msg(msg)
569 if r is None:
570 return
571
572 msgname = type(r).__name__
573
Ole Troan4df97162017-07-07 16:06:08 +0200574 if self.event_callback:
575 self.event_callback(msgname, r)
Ole Troan5016f992017-01-19 09:44:44 +0100576
577 def _control_ping(self, context):
578 """Send a ping command."""
579 self._call_vpp_async(self.control_ping_index,
Ole Troan4df97162017-07-07 16:06:08 +0200580 self.control_ping_msgdef,
Ole Troan5016f992017-01-19 09:44:44 +0100581 context=context)
582
Ole Troana7564e82018-06-12 21:06:44 +0200583 def validate_args(self, msg, kwargs):
584 d = set(kwargs.keys()) - set(msg.field_by_name.keys())
585 if d:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800586 raise VPPValueError('Invalid argument {} to {}'
Ole Troan0bcad322018-12-11 13:04:01 +0100587 .format(list(d), msg.name))
Ole Troana7564e82018-06-12 21:06:44 +0200588
Ole Troan413f4a52018-11-28 11:36:05 +0100589 def _call_vpp(self, i, msgdef, multipart, **kwargs):
Ole Troan5016f992017-01-19 09:44:44 +0100590 """Given a message, send the message and await a reply.
591
592 msgdef - the message packing definition
593 i - the message type index
594 multipart - True if the message returns multiple
595 messages in return.
596 context - context number - chosen at random if not
597 supplied.
598 The remainder of the kwargs are the arguments to the API call.
599
600 The return value is the message or message array containing
601 the response. It will raise an IOError exception if there was
602 no response within the timeout window.
603 """
604
Ole Troan4df97162017-07-07 16:06:08 +0200605 if 'context' not in kwargs:
Ole Troandfc9b7c2017-03-06 23:51:57 +0100606 context = self.get_context()
607 kwargs['context'] = context
608 else:
609 context = kwargs['context']
610 kwargs['_vl_msg_id'] = i
Ole Troan5016f992017-01-19 09:44:44 +0100611
Ole Troan0bcad322018-12-11 13:04:01 +0100612 no_type_conversion = kwargs.pop('_no_type_conversion', False)
613
Ole Troan94495f22018-08-02 11:58:12 +0200614 try:
615 if self.transport.socket_index:
616 kwargs['client_index'] = self.transport.socket_index
617 except AttributeError:
618 pass
Ole Troan413f4a52018-11-28 11:36:05 +0100619 self.validate_args(msgdef, kwargs)
620
621 logging.debug(call_logger(msgdef, kwargs))
622
623 b = msgdef.pack(kwargs)
Ole Troan94495f22018-08-02 11:58:12 +0200624 self.transport.suspend()
625
626 self.transport.write(b)
Ole Troan5016f992017-01-19 09:44:44 +0100627
628 if multipart:
629 # Send a ping after the request - we use its response
630 # to detect that we have seen all results.
631 self._control_ping(context)
632
633 # Block until we get a reply.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100634 rl = []
635 while (True):
Ole Troan94495f22018-08-02 11:58:12 +0200636 msg = self.transport.read()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100637 if not msg:
Paul Vinciguerra6ccc6e92018-11-27 08:15:22 -0800638 raise VPPIOError(2, 'VPP API client: read failed')
Ole Troan0bcad322018-12-11 13:04:01 +0100639 r = self.decode_incoming_msg(msg, no_type_conversion)
Ole Troandfc9b7c2017-03-06 23:51:57 +0100640 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200641 if context not in r or r.context == 0 or context != r.context:
Ole Troan94495f22018-08-02 11:58:12 +0200642 # Message being queued
Ole Troandfc9b7c2017-03-06 23:51:57 +0100643 self.message_queue.put_nowait(r)
644 continue
645
646 if not multipart:
647 rl = r
648 break
649 if msgname == 'control_ping_reply':
650 break
651
652 rl.append(r)
653
Ole Troan94495f22018-08-02 11:58:12 +0200654 self.transport.resume()
Ole Troandfc9b7c2017-03-06 23:51:57 +0100655
Ole Troan413f4a52018-11-28 11:36:05 +0100656 logger.debug(return_logger(rl))
Ole Troandfc9b7c2017-03-06 23:51:57 +0100657 return rl
Ole Troan5016f992017-01-19 09:44:44 +0100658
Ole Troana7564e82018-06-12 21:06:44 +0200659 def _call_vpp_async(self, i, msg, **kwargs):
Ole Troan5016f992017-01-19 09:44:44 +0100660 """Given a message, send the message and await a reply.
661
662 msgdef - the message packing definition
663 i - the message type index
664 context - context number - chosen at random if not
665 supplied.
666 The remainder of the kwargs are the arguments to the API call.
667 """
Ole Troan4df97162017-07-07 16:06:08 +0200668 if 'context' not in kwargs:
Ole Troan7e3a8752016-12-05 10:27:09 +0100669 context = self.get_context()
670 kwargs['context'] = context
671 else:
672 context = kwargs['context']
Ole Troan94495f22018-08-02 11:58:12 +0200673 try:
674 if self.transport.socket_index:
675 kwargs['client_index'] = self.transport.socket_index
676 except AttributeError:
677 kwargs['client_index'] = 0
Ole Troan7e3a8752016-12-05 10:27:09 +0100678 kwargs['_vl_msg_id'] = i
Ole Troana7564e82018-06-12 21:06:44 +0200679 b = msg.pack(kwargs)
Ole Troan7e3a8752016-12-05 10:27:09 +0100680
Ole Troan94495f22018-08-02 11:58:12 +0200681 self.transport.write(b)
Ole Troan7e3a8752016-12-05 10:27:09 +0100682
Ole Troana03f4ef2016-12-02 12:53:55 +0100683 def register_event_callback(self, callback):
Ole Troan5016f992017-01-19 09:44:44 +0100684 """Register a callback for async messages.
Ole Troana03f4ef2016-12-02 12:53:55 +0100685
Ole Troan5016f992017-01-19 09:44:44 +0100686 This will be called for async notifications in sync mode,
687 and all messages in async mode. In sync mode, replies to
688 requests will not come here.
689
690 callback is a fn(msg_type_name, msg_type) that will be
691 called when a message comes in. While this function is
692 executing, note that (a) you are in a background thread and
693 may wish to use threading.Lock to protect your datastructures,
694 and (b) message processing from VPP will stop (so if you take
695 a long while about it you may provoke reply timeouts or cause
696 VPP to fill the RX buffer). Passing None will disable the
697 callback.
698 """
699 self.event_callback = callback
Ole Troandfc9b7c2017-03-06 23:51:57 +0100700
701 def thread_msg_handler(self):
Ole Troan94495f22018-08-02 11:58:12 +0200702 """Python thread calling the user registered message handler.
Ole Troandfc9b7c2017-03-06 23:51:57 +0100703
704 This is to emulate the old style event callback scheme. Modern
705 clients should provide their own thread to poll the event
706 queue.
707 """
708 while True:
709 r = self.message_queue.get()
Klement Sekera180402d2018-02-17 10:58:37 +0100710 if r == "terminate event thread":
711 break
Ole Troandfc9b7c2017-03-06 23:51:57 +0100712 msgname = type(r).__name__
Ole Troan4df97162017-07-07 16:06:08 +0200713 if self.event_callback:
714 self.event_callback(msgname, r)
Chris Luke52bf22e2017-11-03 23:32:38 -0400715
716
717# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4