import struct
import json
import threading
-import glob
+import fnmatch
+import weakref
import atexit
from cffi import FFI
import cffi
# Barfs on failure, no need to check success.
vpp_api = ffi.dlopen('libvppapiclient.so')
-def vpp_atexit(self):
+
+def vpp_atexit(vpp_weakref):
"""Clean up VPP connection on shutdown."""
- if self.connected:
- self.logger.debug('Cleaning up VPP on exit')
- self.disconnect()
+ vpp_instance = vpp_weakref()
+ if vpp_instance.connected:
+ vpp_instance.logger.debug('Cleaning up VPP on exit')
+ vpp_instance.disconnect()
+
vpp_object = None
these messages in a background thread.
"""
def __init__(self, apifiles=None, testmode=False, async_thread=True,
- logger=logging.getLogger('vpp_papi'), loglevel='debug'):
+ logger=None, loglevel=None,
+ read_timeout=0):
"""Create a VPP API object.
apifiles is a list of files containing API
dynamically created reflecting these APIs. If not
provided this will load the API files from VPP's
default install location.
+
+ logger, if supplied, is the logging logger object to log to.
+ loglevel, if supplied, is the log level this logger is set
+ to report at (from the loglevels in the logging module).
"""
global vpp_object
vpp_object = self
+
+ if logger is None:
+ logger = logging.getLogger(__name__)
+ if loglevel is not None:
+ logger.setLevel(loglevel)
+
self.logger = logger
- logging.basicConfig(level=getattr(logging, loglevel.upper()))
self.messages = {}
self.id_names = []
self.apifiles = []
self.event_callback = None
self.message_queue = queue.Queue()
- self.read_timeout = 0
+ self.read_timeout = read_timeout
self.vpp_api = vpp_api
- if async_thread:
- self.event_thread = threading.Thread(
- target=self.thread_msg_handler)
- self.event_thread.daemon = True
- self.event_thread.start()
+ self.async_thread = async_thread
if not apifiles:
# Pick up API definitions from default directory
- apifiles = glob.glob('/usr/share/vpp/api/*.api.json')
+ try:
+ apifiles = self.find_api_files()
+ except RuntimeError:
+ # In test mode we don't care that we can't find the API files
+ if testmode:
+ apifiles = []
+ else:
+ raise
for file in apifiles:
with open(file) as apidef_file:
raise ValueError(1, 'Missing JSON message definitions')
# Make sure we allow VPP to clean up the message rings.
- atexit.register(vpp_atexit, self)
+ atexit.register(vpp_atexit, weakref.ref(self))
# Register error handler
vpp_api.vac_set_error_handler(vac_error_handler)
# Support legacy CFFI
# from_buffer supported from 1.8.0
- (major, minor, patch) = [int(s) for s in cffi.__version__.split('.', 3)]
+ (major, minor, patch) = [int(s) for s in
+ cffi.__version__.split('.', 3)]
if major >= 1 and minor >= 8:
self._write = self._write_new_cffi
else:
return self.context
get_context = ContextId()
+ @classmethod
+ def find_api_dir(cls):
+ """Attempt to find the best directory in which API definition
+ files may reside. If the value VPP_API_DIR exists in the environment
+ then it is first on the search list. If we're inside a recognized
+ location in a VPP source tree (src/scripts and src/vpp-api/python)
+ then entries from there to the likely locations in build-root are
+ added. Finally the location used by system packages is added.
+
+ :returns: A single directory name, or None if no such directory
+ could be found.
+ """
+ dirs = []
+
+ if 'VPP_API_DIR' in os.environ:
+ dirs.append(os.environ['VPP_API_DIR'])
+
+ # perhaps we're in the 'src/scripts' or 'src/vpp-api/python' dir;
+ # in which case, plot a course to likely places in the src tree
+ import __main__ as main
+ if hasattr(main, '__file__'):
+ # get the path of the calling script
+ localdir = os.path.dirname(os.path.realpath(main.__file__))
+ else:
+ # use cwd if there is no calling script
+ localdir = os.getcwd()
+ localdir_s = localdir.split(os.path.sep)
+
+ def dmatch(dir):
+ """Match dir against right-hand components of the script dir"""
+ d = dir.split('/') # param 'dir' assumes a / separator
+ length = len(d)
+ return len(localdir_s) > length and localdir_s[-length:] == d
+
+ def sdir(srcdir, variant):
+ """Build a path from srcdir to the staged API files of
+ 'variant' (typically '' or '_debug')"""
+ # Since 'core' and 'plugin' files are staged
+ # in separate directories, we target the parent dir.
+ return os.path.sep.join((
+ srcdir,
+ 'build-root',
+ 'install-vpp%s-native' % variant,
+ 'vpp',
+ 'share',
+ 'vpp',
+ 'api',
+ ))
+
+ srcdir = None
+ if dmatch('src/scripts'):
+ srcdir = os.path.sep.join(localdir_s[:-2])
+ elif dmatch('src/vpp-api/python'):
+ srcdir = os.path.sep.join(localdir_s[:-3])
+ elif dmatch('test'):
+ # we're apparently running tests
+ srcdir = os.path.sep.join(localdir_s[:-1])
+
+ if srcdir:
+ # we're in the source tree, try both the debug and release
+ # variants.
+ dirs.append(sdir(srcdir, '_debug'))
+ dirs.append(sdir(srcdir, ''))
+
+ # Test for staged copies of the scripts
+ # For these, since we explicitly know if we're running a debug versus
+ # release variant, target only the relevant directory
+ if dmatch('build-root/install-vpp_debug-native/vpp/bin'):
+ srcdir = os.path.sep.join(localdir_s[:-4])
+ dirs.append(sdir(srcdir, '_debug'))
+ if dmatch('build-root/install-vpp-native/vpp/bin'):
+ srcdir = os.path.sep.join(localdir_s[:-4])
+ dirs.append(sdir(srcdir, ''))
+
+ # finally, try the location system packages typically install into
+ dirs.append(os.path.sep.join(('', 'usr', 'share', 'vpp', 'api')))
+
+ # check the directories for existance; first one wins
+ for dir in dirs:
+ if os.path.isdir(dir):
+ return dir
+
+ return None
+
+ @classmethod
+ def find_api_files(cls, api_dir=None, patterns='*'):
+ """Find API definition files from the given directory tree with the
+ given pattern. If no directory is given then find_api_dir() is used
+ to locate one. If no pattern is given then all definition files found
+ in the directory tree are used.
+
+ :param api_dir: A directory tree in which to locate API definition
+ files; subdirectories are descended into.
+ If this is None then find_api_dir() is called to discover it.
+ :param patterns: A list of patterns to use in each visited directory
+ when looking for files.
+ This can be a list/tuple object or a comma-separated string of
+ patterns. Each value in the list will have leading/trialing
+ whitespace stripped.
+ The pattern specifies the first part of the filename, '.api.json'
+ is appended.
+ The results are de-duplicated, thus overlapping patterns are fine.
+ If this is None it defaults to '*' meaning "all API files".
+ :returns: A list of file paths for the API files found.
+ """
+ if api_dir is None:
+ api_dir = cls.find_api_dir()
+ if api_dir is None:
+ raise RuntimeError("api_dir cannot be located")
+
+ if isinstance(patterns, list) or isinstance(patterns, tuple):
+ patterns = [p.strip() + '.api.json' for p in patterns]
+ else:
+ patterns = [p.strip() + '.api.json' for p in patterns.split(",")]
+
+ api_files = []
+ for root, dirnames, files in os.walk(api_dir):
+ # iterate all given patterns and de-dup the result
+ files = set(sum([fnmatch.filter(files, p) for p in patterns], []))
+ for filename in files:
+ api_files.append(os.path.join(root, filename))
+
+ return api_files
+
def status(self):
"""Debug function: report current VPP API status to stdout."""
print('Connected') if self.connected else print('Not Connected')
'i32': 'i',
'u64': 'Q',
'f64': 'd', }
- pack = None
+
if t in base_types:
- pack = base_types[t]
if not vl:
if e > 0 and t == 'u8':
# Fixed byte array
for k in kwargs:
if k not in msgdef['args']:
- raise ValueError(1,'Non existing argument [' + k + ']' + \
- ' used in call to: ' + \
- self.id_names[kwargs['_vl_msg_id']] + '()' )
+ raise ValueError(1, 'Non existing argument [' + k + ']' +
+ ' used in call to: ' +
+ self.id_names[kwargs['_vl_msg_id']] + '()')
for k, v in vpp_iterator(msgdef['args']):
off += size
if callable(v[1]):
e = kwargs[v[0]] if v[0] in kwargs else v[0]
if e != len(kwargs[k]):
- raise (ValueError(1, 'Input list length mismatch: %s (%s != %s)' % (k, e, len(kwargs[k]))))
+ raise (ValueError(1,
+ 'Input list length mismatch: '
+ '%s (%s != %s)' %
+ (k, e, len(kwargs[k]))))
size = 0
for i in range(e):
size += v[1](self, True, buf, off + size,
kwargs[k][i])
else:
if v[0] in kwargs:
- l = kwargs[v[0]]
- if l != len(kwargs[k]):
- raise ValueError(1, 'Input list length mistmatch: %s (%s != %s)' % (k, l, len(kwargs[k])))
+ kwargslen = kwargs[v[0]]
+ if kwargslen != len(kwargs[k]):
+ raise ValueError(1,
+ 'Input list length mismatch:'
+ ' %s (%s != %s)' %
+ (k, kwargslen,
+ len(kwargs[k])))
else:
- l = len(kwargs[k])
+ kwargslen = len(kwargs[k])
if v[1].size == 1:
- buf[off:off + l] = bytearray(kwargs[k])
- size = l
+ buf[off:off + kwargslen] = bytearray(kwargs[k])
+ size = kwargslen
else:
size = 0
for i in kwargs[k]:
size = v(self, True, buf, off, kwargs[k])
else:
if type(kwargs[k]) is str and v.size < len(kwargs[k]):
- raise ValueError(1, 'Input list length mistmatch: %s (%s < %s)' % (k, v.size, len(kwargs[k])))
+ raise ValueError(1,
+ 'Input list length mismatch: '
+ '%s (%s < %s)' %
+ (k, v.size, len(kwargs[k])))
v.pack_into(buf, off, kwargs[k])
size = v.size
else:
def encode(self, msgdef, kwargs):
# Make suitably large buffer
- size = self.get_size(msgdef['sizes'], kwargs)
+ size = self.get_size(msgdef['sizes'], kwargs)
buf = bytearray(size)
offset = 0
size = self.__struct_type(True, msgdef, buf, offset, kwargs)
return self.messages[name]['return_tuple']
return None
+ def duplicate_check_ok(self, name, msgdef):
+ crc = None
+ for c in msgdef:
+ if type(c) is dict and 'crc' in c:
+ crc = c['crc']
+ break
+ if crc:
+ # We can get duplicates because of imports
+ if crc == self.messages[name]['crc']:
+ return True
+ return False
+
def add_message(self, name, msgdef, typeonly=False):
if name in self.messages:
+ if typeonly:
+ if self.duplicate_check_ok(name, msgdef):
+ return
raise ValueError('Duplicate message name: ' + name)
args = collections.OrderedDict()
raise ValueError('Variable Length Array must be last: ' + name)
size, s = self.__struct(*f)
args[field_name] = s
- if type(s) == list and type(s[0]) == int and type(s[1]) == struct.Struct:
+ if type(s) == list and type(s[0]) == int and \
+ type(s[1]) == struct.Struct:
if s[0] < 0:
sizes[field_name] = size
else:
def make_function(self, name, i, msgdef, multipart, async):
if (async):
- f = lambda **kwargs: (self._call_vpp_async(i, msgdef, **kwargs))
+ def f(**kwargs):
+ return self._call_vpp_async(i, msgdef, **kwargs)
else:
- f = lambda **kwargs: (self._call_vpp(i, msgdef, multipart,
- **kwargs))
+ def f(**kwargs):
+ return self._call_vpp(i, msgdef, multipart, **kwargs)
args = self.messages[name]['args']
argtypes = self.messages[name]['argtypes']
f.__name__ = str(name)
multipart = True if name.find('_dump') > 0 else False
f = self.make_function(name, i, msgdef, multipart, async)
setattr(self._api, name, FuncWrapper(f))
-
- # old API stuff starts here - will be removed in 17.07
- if hasattr(self, name):
- raise NameError(
- 3, "Conflicting name in JSON definition: `%s'" % name)
- setattr(self, name, f)
- # old API stuff ends here
else:
self.logger.debug(
'No such message type or failed CRC checksum: %s', n)
"""Send a binary-packed message to VPP."""
if not self.connected:
raise IOError(1, 'Not connected')
- return vpp_api.vac_write(str(buf), len(buf))
+ return vpp_api.vac_write(bytes(buf), len(buf))
def _read(self):
if not self.connected:
self.control_ping_index = vpp_api.vac_get_msg_index(
('control_ping' + '_' + crc[2:]).encode())
self.control_ping_msgdef = self.messages['control_ping']
+ if self.async_thread:
+ self.event_thread = threading.Thread(
+ target=self.thread_msg_handler)
+ self.event_thread.daemon = True
+ self.event_thread.start()
return rv
def connect(self, name, chroot_prefix=None, async=False, rx_qlen=32):
"""Detach from VPP."""
rv = vpp_api.vac_disconnect()
self.connected = False
+ self.message_queue.put("terminate event thread")
return rv
def msg_handler_sync(self, msg):
if hasattr(r, 'context') and r.context > 0:
context = r.context
- msgname = type(r).__name__
-
if context == 0:
# No context -> async notification that we feed to the callback
self.message_queue.put_nowait(r)
"""
while True:
r = self.message_queue.get()
+ if r == "terminate event thread":
+ break
msgname = type(r).__name__
if self.event_callback:
self.event_callback(msgname, r)
+
+
+# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4