diff options
author | Ole Troan <ot@cisco.com> | 2018-06-12 21:06:44 +0200 |
---|---|---|
committer | Florin Coras <florin.coras@gmail.com> | 2018-06-22 03:00:45 +0000 |
commit | a7564e8004fd6d9a63eb0605f752f27a71403645 (patch) | |
tree | 15a97b1b7852dfbdee28c56e0d58218689126c1b /src/vpp-api/python/vpp_papi | |
parent | d600ffe13a0f8f5d11fdea6ab7b24a364c968347 (diff) |
Python API: Add enum and union support.
As well as a rewrite of the encoders/decoders to make it more readable and extensible.
(Re-commit after fix to verify build.)
Change-Id: Ic244d3cebe070bb2570491f8a24f4a1e203f889a
Signed-off-by: Ole Troan <ot@cisco.com>
Diffstat (limited to 'src/vpp-api/python/vpp_papi')
-rw-r--r-- | src/vpp-api/python/vpp_papi/__init__.py | 1 | ||||
-rw-r--r-- | src/vpp-api/python/vpp_papi/vpp_papi.py | 679 | ||||
-rw-r--r-- | src/vpp-api/python/vpp_papi/vpp_serializer.py | 343 |
3 files changed, 1023 insertions, 0 deletions
diff --git a/src/vpp-api/python/vpp_papi/__init__.py b/src/vpp-api/python/vpp_papi/__init__.py new file mode 100644 index 00000000000..f9afcf17f29 --- /dev/null +++ b/src/vpp-api/python/vpp_papi/__init__.py @@ -0,0 +1 @@ +from .vpp_papi import * diff --git a/src/vpp-api/python/vpp_papi/vpp_papi.py b/src/vpp-api/python/vpp_papi/vpp_papi.py new file mode 100644 index 00000000000..edc0c6ab364 --- /dev/null +++ b/src/vpp-api/python/vpp_papi/vpp_papi.py @@ -0,0 +1,679 @@ +#!/usr/bin/env python +# +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import print_function +from __future__ import absolute_import +import sys +import os +import logging +import collections +import struct +import json +import threading +import fnmatch +import weakref +import atexit +from cffi import FFI +import cffi +from . vpp_serializer import VPPType, VPPEnumType, VPPUnionType, BaseTypes +from . vpp_serializer import VPPMessage + +if sys.version[0] == '2': + import Queue as queue +else: + import queue as queue + +ffi = FFI() +ffi.cdef(""" +typedef void (*vac_callback_t)(unsigned char * data, int len); +typedef void (*vac_error_callback_t)(void *, unsigned char *, int); +int vac_connect(char * name, char * chroot_prefix, vac_callback_t cb, + int rx_qlen); +int vac_disconnect(void); +int vac_read(char **data, int *l, unsigned short timeout); +int vac_write(char *data, int len); +void vac_free(void * msg); + +int vac_get_msg_index(unsigned char * name); +int vac_msg_table_size(void); +int vac_msg_table_max_index(void); + +void vac_rx_suspend (void); +void vac_rx_resume (void); +void vac_set_error_handler(vac_error_callback_t); + """) + +# Barfs on failure, no need to check success. +vpp_api = ffi.dlopen('libvppapiclient.so') + + +def vpp_atexit(vpp_weakref): + """Clean up VPP connection on shutdown.""" + vpp_instance = vpp_weakref() + if vpp_instance.connected: + vpp_instance.logger.debug('Cleaning up VPP on exit') + vpp_instance.disconnect() + + +vpp_object = None + + +def vpp_iterator(d): + if sys.version[0] == '2': + return d.iteritems() + else: + return d.items() + + +@ffi.callback("void(unsigned char *, int)") +def vac_callback_sync(data, len): + vpp_object.msg_handler_sync(ffi.buffer(data, len)) + + +@ffi.callback("void(unsigned char *, int)") +def vac_callback_async(data, len): + vpp_object.msg_handler_async(ffi.buffer(data, len)) + + +@ffi.callback("void(void *, unsigned char *, int)") +def vac_error_handler(arg, msg, msg_len): + vpp_object.logger.warning("VPP API client:: %s", ffi.string(msg, msg_len)) + + +class Empty(object): + pass + + +class FuncWrapper(object): + def __init__(self, func): + self._func = func + self.__name__ = func.__name__ + + def __call__(self, **kwargs): + return self._func(**kwargs) + + +class VPP(): + """VPP interface. + + This class provides the APIs to VPP. The APIs are loaded + from provided .api.json files and makes functions accordingly. + These functions are documented in the VPP .api files, as they + are dynamically created. + + Additionally, VPP can send callback messages; this class + provides a means to register a callback function to receive + these messages in a background thread. + """ + + def process_json_file(self, apidef_file): + api = json.load(apidef_file) + types = {} + for t in api['enums']: + t[0] = 'vl_api_' + t[0] + '_t' + types[t[0]] = {'type': 'enum', 'data': t} + for t in api['unions']: + t[0] = 'vl_api_' + t[0] + '_t' + types[t[0]] = {'type': 'union', 'data': t} + for t in api['types']: + t[0] = 'vl_api_' + t[0] + '_t' + types[t[0]] = {'type': 'type', 'data': t} + + i = 0 + while True: + unresolved = {} + for k, v in types.items(): + t = v['data'] + if v['type'] == 'enum': + try: + VPPEnumType(t[0], t[1:]) + except ValueError: + unresolved[k] = v + elif v['type'] == 'union': + try: + VPPUnionType(t[0], t[1:]) + except ValueError: + unresolved[k] = v + elif v['type'] == 'type': + try: + VPPType(t[0], t[1:]) + except ValueError: + unresolved[k] = v + if len(unresolved) == 0: + break + if i > 3: + raise ValueError('Unresolved type definitions {}' + .format(unresolved)) + types = unresolved + i += 1 + + for m in api['messages']: + try: + self.messages[m[0]] = VPPMessage(m[0], m[1:]) + except NotImplementedError: + self.logger.error('Not implemented error for {}'.format(m[0])) + + def __init__(self, apifiles=None, testmode=False, async_thread=True, + logger=logging.getLogger('vpp_papi'), loglevel='debug', + read_timeout=0): + """Create a VPP API object. + + apifiles is a list of files containing API + descriptions that will be loaded - methods will be + 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 + + self.messages = {} + self.id_names = [] + self.id_msgdef = [] + self.connected = False + self.header = VPPType('header', [['u16', 'msgid'], + ['u32', 'client_index']]) + self.apifiles = [] + self.event_callback = None + self.message_queue = queue.Queue() + self.read_timeout = read_timeout + self.vpp_api = vpp_api + self.async_thread = async_thread + + if not apifiles: + # Pick up API definitions from default directory + 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: + self.process_json_file(apidef_file) + + self.apifiles = apifiles + + # Basic sanity check + if len(self.messages) == 0 and not testmode: + raise ValueError(1, 'Missing JSON message definitions') + + # Make sure we allow VPP to clean up the message rings. + atexit.register(vpp_atexit, weakref.ref(self)) + + # Register error handler + if not testmode: + 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)] + if major >= 1 and minor >= 8: + self._write = self._write_new_cffi + else: + self._write = self._write_legacy_cffi + + class ContextId(object): + """Thread-safe provider of unique context IDs.""" + def __init__(self): + self.context = 0 + self.lock = threading.Lock() + + def __call__(self): + """Get a new unique (or, at least, not recently used) context.""" + with self.lock: + self.context += 1 + 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') + print('Read API definitions from', ', '.join(self.apifiles)) + + @property + def api(self): + if not hasattr(self, "_api"): + raise Exception("Not connected, api definitions not available") + return self._api + + def make_function(self, msg, i, multipart, async): + if (async): + def f(**kwargs): + return self._call_vpp_async(i, msg, **kwargs) + else: + def f(**kwargs): + return self._call_vpp(i, msg, multipart, **kwargs) + + f.__name__ = str(msg.name) + f.__doc__ = ", ".join(["%s %s" % + (msg.fieldtypes[j], k) + for j, k in enumerate(msg.fields)]) + return f + + def _register_functions(self, async=False): + self.id_names = [None] * (self.vpp_dictionary_maxid + 1) + self.id_msgdef = [None] * (self.vpp_dictionary_maxid + 1) + self._api = Empty() + for name, msg in vpp_iterator(self.messages): + n = name + '_' + msg.crc[2:] + i = vpp_api.vac_get_msg_index(n.encode()) + if i > 0: + self.id_msgdef[i] = msg + self.id_names[i] = name + # TODO: Fix multipart (use services) + multipart = True if name.find('_dump') > 0 else False + f = self.make_function(msg, i, multipart, async) + setattr(self._api, name, FuncWrapper(f)) + else: + self.logger.debug( + 'No such message type or failed CRC checksum: %s', n) + + def _write_new_cffi(self, buf): + """Send a binary-packed message to VPP.""" + if not self.connected: + raise IOError(1, 'Not connected') + return vpp_api.vac_write(ffi.from_buffer(buf), len(buf)) + + def _write_legacy_cffi(self, buf): + """Send a binary-packed message to VPP.""" + if not self.connected: + raise IOError(1, 'Not connected') + return vpp_api.vac_write(bytes(buf), len(buf)) + + def _read(self): + if not self.connected: + raise IOError(1, 'Not connected') + mem = ffi.new("char **") + size = ffi.new("int *") + rv = vpp_api.vac_read(mem, size, self.read_timeout) + if rv: + raise IOError(rv, 'vac_read failed') + msg = bytes(ffi.buffer(mem[0], size[0])) + vpp_api.vac_free(mem[0]) + return msg + + def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen, + async): + pfx = chroot_prefix.encode() if chroot_prefix else ffi.NULL + rv = vpp_api.vac_connect(name.encode(), pfx, msg_handler, rx_qlen) + if rv != 0: + raise IOError(2, 'Connect failed') + self.connected = True + self.vpp_dictionary_maxid = vpp_api.vac_msg_table_max_index() + self._register_functions(async=async) + + # Initialise control ping + crc = self.messages['control_ping'].crc + 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): + """Attach to VPP. + + name - the name of the client. + chroot_prefix - if VPP is chroot'ed, the prefix of the jail + async - if true, messages are sent without waiting for a reply + rx_qlen - the length of the VPP message receive queue between + client and server. + """ + msg_handler = vac_callback_sync if not async else vac_callback_async + return self.connect_internal(name, msg_handler, chroot_prefix, rx_qlen, + async) + + def connect_sync(self, name, chroot_prefix=None, rx_qlen=32): + """Attach to VPP in synchronous mode. Application must poll for events. + + name - the name of the client. + chroot_prefix - if VPP is chroot'ed, the prefix of the jail + rx_qlen - the length of the VPP message receive queue between + client and server. + """ + + return self.connect_internal(name, ffi.NULL, chroot_prefix, rx_qlen, + async=False) + + def disconnect(self): + """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): + """Process an incoming message from VPP in sync mode. + + The message may be a reply or it may be an async notification. + """ + r = self.decode_incoming_msg(msg) + if r is None: + return + + # If we have a context, then use the context to find any + # request waiting for a reply + context = 0 + if hasattr(r, 'context') and r.context > 0: + context = r.context + + if context == 0: + # No context -> async notification that we feed to the callback + self.message_queue.put_nowait(r) + else: + raise IOError(2, 'RPC reply message received in event handler') + + def decode_incoming_msg(self, msg): + if not msg: + self.logger.warning('vpp_api.read failed') + return + + i, ci = self.header.unpack(msg, 0) + if self.id_names[i] == 'rx_thread_exit': + return + + # + # Decode message and returns a tuple. + # + msgobj = self.id_msgdef[i] + if not msgobj: + raise IOError(2, 'Reply message undefined') + + r = msgobj.unpack(msg) + + return r + + def msg_handler_async(self, msg): + """Process a message from VPP in async mode. + + In async mode, all messages are returned to the callback. + """ + r = self.decode_incoming_msg(msg) + if r is None: + return + + msgname = type(r).__name__ + + if self.event_callback: + self.event_callback(msgname, r) + + def _control_ping(self, context): + """Send a ping command.""" + self._call_vpp_async(self.control_ping_index, + self.control_ping_msgdef, + context=context) + + def validate_args(self, msg, kwargs): + d = set(kwargs.keys()) - set(msg.field_by_name.keys()) + if d: + raise ValueError('Invalid argument {} to {}' + .format(list(d), msg.name)) + + def _call_vpp(self, i, msg, multipart, **kwargs): + """Given a message, send the message and await a reply. + + msgdef - the message packing definition + i - the message type index + multipart - True if the message returns multiple + messages in return. + context - context number - chosen at random if not + supplied. + The remainder of the kwargs are the arguments to the API call. + + The return value is the message or message array containing + the response. It will raise an IOError exception if there was + no response within the timeout window. + """ + + if 'context' not in kwargs: + context = self.get_context() + kwargs['context'] = context + else: + context = kwargs['context'] + kwargs['_vl_msg_id'] = i + + self.validate_args(msg, kwargs) + b = msg.pack(kwargs) + vpp_api.vac_rx_suspend() + self._write(b) + + if multipart: + # Send a ping after the request - we use its response + # to detect that we have seen all results. + self._control_ping(context) + + # Block until we get a reply. + rl = [] + while (True): + msg = self._read() + if not msg: + raise IOError(2, 'VPP API client: read failed') + r = self.decode_incoming_msg(msg) + msgname = type(r).__name__ + if context not in r or r.context == 0 or context != r.context: + self.message_queue.put_nowait(r) + continue + + if not multipart: + rl = r + break + if msgname == 'control_ping_reply': + break + + rl.append(r) + + vpp_api.vac_rx_resume() + + return rl + + def _call_vpp_async(self, i, msg, **kwargs): + """Given a message, send the message and await a reply. + + msgdef - the message packing definition + i - the message type index + context - context number - chosen at random if not + supplied. + The remainder of the kwargs are the arguments to the API call. + """ + if 'context' not in kwargs: + context = self.get_context() + kwargs['context'] = context + else: + context = kwargs['context'] + kwargs['client_index'] = 0 + kwargs['_vl_msg_id'] = i + b = msg.pack(kwargs) + + self._write(b) + + def register_event_callback(self, callback): + """Register a callback for async messages. + + This will be called for async notifications in sync mode, + and all messages in async mode. In sync mode, replies to + requests will not come here. + + callback is a fn(msg_type_name, msg_type) that will be + called when a message comes in. While this function is + executing, note that (a) you are in a background thread and + may wish to use threading.Lock to protect your datastructures, + and (b) message processing from VPP will stop (so if you take + a long while about it you may provoke reply timeouts or cause + VPP to fill the RX buffer). Passing None will disable the + callback. + """ + self.event_callback = callback + + def thread_msg_handler(self): + """Python thread calling the user registerd message handler. + + This is to emulate the old style event callback scheme. Modern + clients should provide their own thread to poll the event + queue. + """ + 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 diff --git a/src/vpp-api/python/vpp_papi/vpp_serializer.py b/src/vpp-api/python/vpp_papi/vpp_serializer.py new file mode 100644 index 00000000000..7f5c5ace2dc --- /dev/null +++ b/src/vpp-api/python/vpp_papi/vpp_serializer.py @@ -0,0 +1,343 @@ +# +# Copyright (c) 2018 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import struct +import collections +from enum import IntEnum +import logging + +# +# Set log-level in application by doing e.g.: +# logger = logging.getLogger('vpp_serializer') +# logger.setLevel(logging.DEBUG) +# +logger = logging.getLogger(__name__) + + +class BaseTypes(): + def __init__(self, type, elements=0): + base_types = {'u8': '>B', + 'u16': '>H', + 'u32': '>I', + 'i32': '>i', + 'u64': '>Q', + 'f64': '>d', + 'header': '>HI'} + + if elements > 0 and type == 'u8': + self.packer = struct.Struct('>%ss' % elements) + else: + self.packer = struct.Struct(base_types[type]) + self.size = self.packer.size + logger.debug('Adding {} with format: {}' + .format(type, base_types[type])) + + def pack(self, data, kwargs=None): + logger.debug("Data: {} Format: {}".format(data, self.packer.format)) + return self.packer.pack(data) + + def unpack(self, data, offset, result=None): + logger.debug("@ {} Format: {}".format(offset, self.packer.format)) + return self.packer.unpack_from(data, offset)[0] + + +types = {} +types['u8'] = BaseTypes('u8') +types['u16'] = BaseTypes('u16') +types['u32'] = BaseTypes('u32') +types['i32'] = BaseTypes('i32') +types['u64'] = BaseTypes('u64') +types['f64'] = BaseTypes('f64') + + +class FixedList_u8(): + def __init__(self, name, field_type, num): + self.name = name + self.num = num + self.packer = BaseTypes(field_type, num) + self.size = self.packer.size + + def pack(self, list, kwargs): + """Packs a fixed length bytestring. Left-pads with zeros + if input data is too short.""" + logger.debug("Data: {}".format(list)) + + if len(list) > self.num: + raise ValueError('Fixed list length error for "{}", got: {}' + ' expected: {}' + .format(self.name, len(list), self.num)) + return self.packer.pack(list) + + def unpack(self, data, offset=0, result=None): + if len(data[offset:]) < self.num: + raise ValueError('Invalid array length for "{}" got {}' + ' expected {}' + .format(self.name, len(data), self.num)) + return self.packer.unpack(data, offset) + + +class FixedList(): + def __init__(self, name, field_type, num): + self.num = num + self.packer = types[field_type] + self.size = self.packer.size * num + + def pack(self, list, kwargs): + logger.debug("Data: {}".format(list)) + + if len(list) != self.num: + raise ValueError('Fixed list length error, got: {} expected: {}' + .format(len(list), self.num)) + b = bytes() + for e in list: + b += self.packer.pack(e) + return b + + def unpack(self, data, offset=0, result=None): + # Return a list of arguments + result = [] + for e in range(self.num): + x = self.packer.unpack(data, offset) + result.append(x) + offset += self.packer.size + return result + + +class VLAList(): + def __init__(self, name, field_type, len_field_name, index): + self.index = index + self.packer = types[field_type] + self.size = self.packer.size + self.length_field = len_field_name + + def pack(self, list, kwargs=None): + logger.debug("Data: {}".format(list)) + if len(list) != kwargs[self.length_field]: + raise ValueError('Variable length error, got: {} expected: {}' + .format(len(list), kwargs[self.length_field])) + b = bytes() + + # u8 array + if self.packer.size == 1: + return bytearray(list) + + for e in list: + b += self.packer.pack(e) + return b + + def unpack(self, data, offset=0, result=None): + logger.debug("Data: {} @ {} Result: {}" + .format(list, offset, result[self.index])) + # Return a list of arguments + + # u8 array + if self.packer.size == 1: + if result[self.index] == 0: + return b'' + p = BaseTypes('u8', result[self.index]) + r = p.unpack(data, offset) + return r + + r = [] + for e in range(result[self.index]): + x = self.packer.unpack(data, offset) + r.append(x) + offset += self.packer.size + return r + + +class VLAList_legacy(): + def __init__(self, name, field_type): + self.packer = types[field_type] + self.size = self.packer.size + + def pack(self, list, kwargs=None): + logger.debug("Data: {}".format(list)) + + if self.packer.size == 1: + return bytes(list) + + b = bytes() + for e in list: + b += self.packer.pack(e) + return b + + def unpack(self, data, offset=0, result=None): + # Return a list of arguments + if (len(data) - offset) % self.packer.size: + raise ValueError('Legacy Variable Length Array length mismatch.') + elements = int((len(data) - offset) / self.packer.size) + r = [] + logger.debug("Legacy VLA: {} elements of size {}" + .format(elements, self.packer.size)) + for e in range(elements): + x = self.packer.unpack(data, offset) + r.append(x) + offset += self.packer.size + return r + + +class VPPEnumType(): + def __init__(self, name, msgdef): + self.size = types['u32'].size + e_hash = {} + for f in msgdef: + if type(f) is dict and 'enumtype' in f: + if f['enumtype'] != 'u32': + raise NotImplementedError + continue + ename, evalue = f + e_hash[ename] = evalue + self.enum = IntEnum(name, e_hash) + types[name] = self + logger.debug('Adding enum {}'.format(name)) + + def __getattr__(self, name): + return self.enum[name] + + def pack(self, data, kwargs=None): + logger.debug("Data: {}".format(data)) + return types['u32'].pack(data, kwargs) + + def unpack(self, data, offset=0, result=None): + x = types['u32'].unpack(data, offset) + return self.enum(x) + + +class VPPUnionType(): + def __init__(self, name, msgdef): + self.name = name + self.size = 0 + self.maxindex = 0 + fields = [] + self.packers = collections.OrderedDict() + for i, f in enumerate(msgdef): + if type(f) is dict and 'crc' in f: + self.crc = f['crc'] + continue + f_type, f_name = f + if f_type not in types: + logger.debug('Unknown union type {}'.format(f_type)) + raise ValueError('Unknown message type {}'.format(f_type)) + fields.append(f_name) + size = types[f_type].size + self.packers[f_name] = types[f_type] + if size > self.size: + self.size = size + self.maxindex = i + + types[name] = self + self.tuple = collections.namedtuple(name, fields, rename=True) + logger.debug('Adding union {}'.format(name)) + + def pack(self, data, kwargs=None): + logger.debug("Data: {}".format(data)) + for k, v in data.items(): + logger.debug("Key: {} Value: {}".format(k, v)) + b = self.packers[k].pack(v, kwargs) + offset = self.size - self.packers[k].size + break + r = bytearray(self.size) + r[offset:] = b + return r + + def unpack(self, data, offset=0, result=None): + r = [] + for k, p in self.packers.items(): + union_offset = self.size - p.size + r.append(p.unpack(data, offset + union_offset)) + return self.tuple._make(r) + + +class VPPType(): + # Set everything up to be able to pack / unpack + def __init__(self, name, msgdef): + self.name = name + self.msgdef = msgdef + self.packers = [] + self.fields = [] + self.fieldtypes = [] + self.field_by_name = {} + size = 0 + for i, f in enumerate(msgdef): + if type(f) is dict and 'crc' in f: + self.crc = f['crc'] + continue + f_type, f_name = f[:2] + self.fields.append(f_name) + self.field_by_name[f_name] = None + self.fieldtypes.append(f_type) + if f_type not in types: + logger.debug('Unknown type {}'.format(f_type)) + raise ValueError('Unknown message type {}'.format(f_type)) + if len(f) == 3: # list + list_elements = f[2] + if list_elements == 0: + p = VLAList_legacy(f_name, f_type) + self.packers.append(p) + elif f_type == 'u8': + p = FixedList_u8(f_name, f_type, list_elements) + self.packers.append(p) + size += p.size + else: + p = FixedList(f_name, f_type, list_elements) + self.packers.append(p) + size += p.size + elif len(f) == 4: # Variable length list + # Find index of length field + length_index = self.fields.index(f[3]) + p = VLAList(f_name, f_type, f[3], length_index) + self.packers.append(p) + else: + self.packers.append(types[f_type]) + size += types[f_type].size + + self.size = size + self.tuple = collections.namedtuple(name, self.fields, rename=True) + types[name] = self + logger.debug('Adding type {}'.format(name)) + + def pack(self, data, kwargs=None): + if not kwargs: + kwargs = data + logger.debug("Data: {}".format(data)) + b = bytes() + for i, a in enumerate(self.fields): + if a not in data: + logger.debug("Argument {} not given, defaulting to 0" + .format(a)) + b += b'\x00' * self.packers[i].size + continue + + if isinstance(self.packers[i], VPPType): + b += self.packers[i].pack(data[a], kwargs[a]) + else: + b += self.packers[i].pack(data[a], kwargs) + return b + + def unpack(self, data, offset=0, result=None): + # Return a list of arguments + result = [] + for p in self.packers: + x = p.unpack(data, offset, result) + if type(x) is tuple and len(x) == 1: + x = x[0] + result.append(x) + offset += p.size + return self.tuple._make(result) + + +class VPPMessage(VPPType): + pass |