From 8d6777df09c3dc441013a31f21cc50ab3b0f42a3 Mon Sep 17 00:00:00 2001 From: Billy O'Mahony Date: Fri, 29 May 2015 15:24:03 +0100 Subject: framework: Add reworked framework to repo This commit adds the vSwitch Integration Test Framework whose design, based off TOIT, is outlined in the HLD previously made availiable to the community for review. The design of this framework allows developers to add different implementations of components, specifically vSwitches, Traffic Generators, Metrics Collectors and VNFs, easily. The goal of this design is that all testcases should run regardless of what is "under the hood". This commit adds support for running the framework for a phy to phy RFC2544 testcase only. More testcases will be added by the community. vSwitches supported at this time: * Intel DPDK (r) accelerated OpenvSwitch Traffic Generators supported at this time: * IxNet - IxNetwork Implementation * Ixia - IxExplorer Implementation * Dummy - Manual Implementation Metrics Collectors supported at this time: * Linux Metrics No VNFs are supported at this time but the framework outlines how they should be integrated and provides APIs for them to adhere to. JIRA: VSPERF-27 Change-Id: I312e1a1199487ffee8f824be06cd97d4f793eee0 Signed-off-by: Stephen Finucane Signed-off-by: Meghan Halton Signed-off-by: Christopher Nolan Signed-off-by: Maryam Tahhan Signed-off-by: Ciara Loftus Signed-off-by: Mark Kavanagh Signed-off-by: Cian Ferriter Signed-off-by: Timo Puha Signed-off-by: Billy O'Mahony Signed-off-by: Michal Weglicki Signed-off-by: Rory Sexton Signed-off-by: Ian Stokes Signed-off-by: Kevin Traynor Signed-off-by: Dino Simeon Madarang Reviewed-by: Eugene Snider Reviewed-by: Aihua Li --- src/__init__.py | 21 +++ src/dpdk/__init__.py | 21 +++ src/dpdk/dpdk.py | 377 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/ovs/__init__.py | 25 ++++ src/ovs/daemon.py | 142 +++++++++++++++++++ src/ovs/ofctl.py | 316 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 902 insertions(+) create mode 100644 src/__init__.py create mode 100644 src/dpdk/__init__.py create mode 100644 src/dpdk/dpdk.py create mode 100644 src/ovs/__init__.py create mode 100644 src/ovs/daemon.py create mode 100644 src/ovs/ofctl.py (limited to 'src') diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..9293b4f8 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2015 Intel Corporation. +# +# 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. + +"""High level package for VSPERF dependencies + +No functionality is expected for this package and its purpose is just to +keep Python package structure intact without extra requirements for +PYTHONPATH. +""" + diff --git a/src/dpdk/__init__.py b/src/dpdk/__init__.py new file mode 100644 index 00000000..4be1e215 --- /dev/null +++ b/src/dpdk/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2015 Intel Corporation. +# +# 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. + +"""A collection of functions for automating the DPDK setup and teardown. + +These automation tasks include mounting/unmounting hugepages, inserting +and removing drivers and binding/unbinding NICs. +""" + +from src.dpdk.dpdk import * diff --git a/src/dpdk/dpdk.py b/src/dpdk/dpdk.py new file mode 100644 index 00000000..9b3d1385 --- /dev/null +++ b/src/dpdk/dpdk.py @@ -0,0 +1,377 @@ +# Copyright 2015 Intel Corporation. +# +# 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. + +"""Automation of system configuration for DPDK use. + +Parts of this based on ``tools/pci_unbind.py`` script from Intel(R) DPDK. +""" + +from sys import platform as _platform + +import os +import re +import subprocess +import logging +import locale + +from tools import tasks +from conf import settings + +_LOGGER = logging.getLogger(__name__) +RTE_PCI_TOOL = os.path.join( + settings.getValue('RTE_SDK'), 'tools', 'dpdk_nic_bind.py') + +# +# system management +# + + +def init(): + """Setup system for DPDK. + """ + if not _is_linux(): + _LOGGER.error('Not running on a compatible Linux version. Exiting...') + return + + _mount_hugepages() + _insert_modules() + _remove_vhost_net() + _bind_nics() + _copy_dpdk_for_guest() + + +def cleanup(): + """Setup system for DPDK. + """ + if not _is_linux(): + _LOGGER.error('Not running on a compatible Linux version. Exiting...') + return + + _unbind_nics() + _remove_modules() + _umount_hugepages() + _vhost_user_cleanup() + + +# +# vhost specific modules management +# + + +def insert_vhost_modules(): + """Inserts VHOST related kernel modules + """ + mod_path_prefix = os.path.join(settings.getValue('RTE_SDK'), + 'lib', + 'librte_vhost') + _insert_module_group('VHOST_MODULE', mod_path_prefix) + + +def remove_vhost_modules(): + """Removes all VHOST related kernel modules + """ + _remove_module_group('VHOST_MODULE') + +# +# basic compatibility test +# + + +def _is_linux(): + """Check if running on Linux. + + Many of the functions in this file rely on features commonly found + only on Linux (i.e. ``/proc`` is not present on FreeBSD). Hence, this + check is important to ensure someone doesn't run this on an incompatible + OS or distro. + """ + return _platform.startswith('linux') and os.path.isdir('/proc') + +# +# hugepage management +# + + +def _is_hugepage_available(): + """Check if hugepages are available on the system. + """ + hugepage_re = re.compile(r'^HugePages_Free:\s+(?P\d+)$') + + # read in meminfo + with open('/proc/meminfo') as mem_file: + mem_info = mem_file.readlines() + + # first check if module is loaded + for line in mem_info: + result = hugepage_re.match(line) + if not result: + continue + + num_huge = result.group('num_hp') + if not num_huge: + _LOGGER.info('No free hugepages.') + else: + _LOGGER.info('Found \'%s\' free hugepage(s).', num_huge) + return True + + return False + + +def _is_hugepage_mounted(): + """Check if hugepages are mounted. + """ + output = subprocess.check_output(['mount'], shell=True) + my_encoding = locale.getdefaultlocale()[1] + for line in output.decode(my_encoding).split('\n'): + if 'hugetlbfs' in line: + return True + + return False + + +def _mount_hugepages(): + """Ensure hugepages are mounted. + """ + if not _is_hugepage_available(): + return + + if _is_hugepage_mounted(): + return + + if not os.path.exists(settings.getValue('HUGEPAGE_DIR')): + os.makedirs(settings.getValue('HUGEPAGE_DIR')) + try: + tasks.run_task(['sudo', 'mount', '-t', 'hugetlbfs', 'nodev', + settings.getValue('HUGEPAGE_DIR')], + _LOGGER, 'Mounting hugepages...', True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to mount hugepages.') + + +def _umount_hugepages(): + """Ensure hugepages are unmounted. + """ + if not _is_hugepage_mounted(): + return + + try: + tasks.run_task(['sudo', 'umount', settings.getValue('HUGEPAGE_DIR')], + _LOGGER, 'Unmounting hugepages...', True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to umount hugepages.') + +# +# module management +# + + +def _is_module_inserted(module): + """Check if a module is inserted on system. + """ + with open('/proc/modules') as mod_file: + loaded_mods = mod_file.readlines() + + # first check if module is loaded + for line in loaded_mods: + if line.startswith(module): + return True + return False + + +def _insert_modules(): + """Ensure required modules are inserted on system. + """ + for module in settings.getValue('SYS_MODULES'): + if _is_module_inserted(module): + continue + + try: + tasks.run_task(['sudo', 'modprobe', module], _LOGGER, + 'Inserting module \'%s\'...' % module, True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to insert module \'%s\'.', module) + raise # fail catastrophically + + mod_path_prefix = settings.getValue('OVS_DIR') + _insert_module_group('OVS_MODULES', mod_path_prefix) + mod_path_prefix = os.path.join(settings.getValue('RTE_SDK'), + settings.getValue('RTE_TARGET')) + _insert_module_group('DPDK_MODULES', mod_path_prefix) + + +def _insert_module_group(module_group, group_path_prefix): + """Ensure all modules in a group are inserted into the system. + + :param module_group: A name of configuration item containing a list + of module names + """ + for module in settings.getValue(module_group): + # first check if module is loaded + if _is_module_inserted(module[1]): + continue + + try: + mod_path = os.path.join(group_path_prefix, module[0], + '%s.ko' % module[1]) + tasks.run_task(['sudo', 'insmod', mod_path], _LOGGER, + 'Inserting module \'%s\'...' % module[1], True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to insert module \'%s\'.', module[1]) + raise # fail catastrophically + + +def _remove_modules(): + """Ensure required modules are removed from system. + """ + _remove_module_group('OVS_MODULES') + _remove_module_group('DPDK_MODULES') + + for module in settings.getValue('SYS_MODULES'): + # first check if module is loaded + if not _is_module_inserted(module): + continue + + try: + tasks.run_task(['sudo', 'rmmod', module], _LOGGER, + 'Removing module \'%s\'...' % module, True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to remove module \'%s\'.', module) + continue + + +def _remove_module_group(module_group): + """Ensure all modules in a group are removed from the system. + + :param module_group: A name of configuration item containing a list + of module names + """ + for module in settings.getValue(module_group): + # first check if module is loaded + if not _is_module_inserted(module[1]): + continue + + try: + tasks.run_task(['sudo', 'rmmod', module[1]], _LOGGER, + 'Removing module \'%s\'...' % module[1], True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to remove module \'%s\'.', module[1]) + continue + + +# +# 'vhost-net' module management +# + +def _remove_vhost_net(): + """Remove vhost-net driver and file. + """ + if _is_module_inserted('vhost_net'): + try: + tasks.run_task(['sudo', 'rmmod', 'vhost_net'], _LOGGER, + 'Removing \'/dev/vhost-net\' directory...', True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to remove module \'vhost_net\'.') + + try: + tasks.run_task(['sudo', 'rm', '-f', '/dev/vhost-net'], _LOGGER, + 'Removing \'/dev/vhost-net\' directory...', True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to remove directory \'/dev/vhost-net\'.') + +# +# NIC management +# + + +def _bind_nics(): + """Bind NICs using the Intel DPDK ``pci_unbind.py`` tool. + """ + try: + tasks.run_task(['sudo', RTE_PCI_TOOL, '--bind', 'igb_uio'] + + settings.getValue('WHITELIST_NICS'), _LOGGER, + 'Binding NICs %s...' % + settings.getValue('WHITELIST_NICS'), + True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to bind NICs %s', + str(settings.getValue('WHITELIST_NICS'))) + + +def _unbind_nics(): + """Unbind NICs using the Intel DPDK ``pci_unbind.py`` tool. + """ + try: + tasks.run_task(['sudo', RTE_PCI_TOOL, '--unbind'] + + settings.getValue('WHITELIST_NICS'), _LOGGER, + 'Unbinding NICs %s...' % + str(settings.getValue('WHITELIST_NICS')), + True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to unbind NICs %s', + str(settings.getValue('WHITELIST_NICS'))) + + +def _copy_dpdk_for_guest(): + """Copy dpdk code to GUEST_SHARE_DIR for use by guests. + """ + guest_share_dir = os.path.join( + settings.getValue('GUEST_SHARE_DIR'), 'DPDK') + + if not os.path.exists(guest_share_dir): + os.makedirs(guest_share_dir) + + try: + tasks.run_task(['rsync', '-a', '-r', '-l', r'--exclude="\.git"', + os.path.join(settings.getValue('RTE_SDK'), ''), + guest_share_dir], + _LOGGER, + 'Copying DPDK to shared directory...', + True) + except subprocess.CalledProcessError: + _LOGGER.error('Unable to copy DPDK to shared directory') + + +# +# Vhost-user cleanup +# + +def _vhost_user_cleanup(): + """Remove files created by vhost-user tests. + """ + for sock in settings.getValue('VHOST_USER_SOCKS'): + if os.path.exists(sock): + try: + tasks.run_task(['sudo', 'rm', sock], + _LOGGER, + 'Deleting vhost-user socket \'%s\'...' % + sock, + True) + + except subprocess.CalledProcessError: + _LOGGER.error('Unable to delete vhost-user socket \'%s\'.', + sock) + continue + + +class Dpdk(object): + """A context manager for the system init/cleanup. + """ + def __enter__(self): + _LOGGER.info('Setting up DPDK') + init() + return self + + def __exit__(self, type_, value, traceback): + _LOGGER.info('Cleaning up DPDK') + cleanup() diff --git a/src/ovs/__init__.py b/src/ovs/__init__.py new file mode 100644 index 00000000..1a31ea2e --- /dev/null +++ b/src/ovs/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2015 Intel Corporation. +# +# 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. + +""" +A package for controlling Open vSwitch + +This package is intended to stay gneneric enough to support using any data +path of OVS (linux kernel, DPDK userspace, etc.) by different parameterization +and external setup of vswitchd-external process, kernel modules etc. + +""" + +from src.ovs.daemon import * +from src.ovs.ofctl import * diff --git a/src/ovs/daemon.py b/src/ovs/daemon.py new file mode 100644 index 00000000..ee3446d5 --- /dev/null +++ b/src/ovs/daemon.py @@ -0,0 +1,142 @@ +# Copyright 2015 Intel Corporation. +# +# 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. + +"""Class wrapper for controlling an OVS instance. + +Wraps a pair of ``ovs-vswitchd`` and ``ovsdb-server`` processes. +""" + +import os +import logging +import pexpect + +from conf import settings +from tools import tasks + +_OVS_VSWITCHD_BIN = os.path.join( + settings.getValue('OVS_DIR'), 'vswitchd', 'ovs-vswitchd') +_OVSDB_TOOL_BIN = os.path.join( + settings.getValue('OVS_DIR'), 'ovsdb', 'ovsdb-tool') +_OVSDB_SERVER_BIN = os.path.join( + settings.getValue('OVS_DIR'), 'ovsdb', 'ovsdb-server') + +_OVS_VAR_DIR = '/usr/local/var/run/openvswitch/' +_OVS_ETC_DIR = '/usr/local/etc/openvswitch/' + +_LOG_FILE_VSWITCHD = os.path.join( + settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_VSWITCHD')) + +class VSwitchd(tasks.Process): + """Class wrapper for controlling an OVS instance. + + Wraps a pair of ``ovs-vswitchd`` and ``ovsdb-server`` processes. + """ + _ovsdb_pid = None + _logfile = _LOG_FILE_VSWITCHD + + + _expect = r'EAL: Master l*core \d+ is ready' + _proc_name = 'ovs-vswitchd' + + def __init__(self, timeout=30, vswitchd_args=None): + """Initialise the wrapper with a specific start timeout and extra + parameters. + + :param timeout: Timeout to wait for application to start. + :param vswitchd_args: Command line parameters for vswitchd. + + :returns: None + """ + self._logger = logging.getLogger(__name__) + self._timeout = timeout + vswitchd_args = vswitchd_args or [] + + self._cmd = ['sudo', '-E', _OVS_VSWITCHD_BIN] + vswitchd_args + + # startup/shutdown + + def start(self): + """ Start ``ovsdb-server`` and ``ovs-vswitchd`` instance. + + :returns: None + :raises: pexpect.EOF, pexpect.TIMEOUT + """ + self._reset_ovsdb() + self._start_ovsdb() # this has to be started first + + try: + super(VSwitchd, self).start() + self.relinquish() + except (pexpect.EOF, pexpect.TIMEOUT) as exc: + self._kill_ovsdb() + raise exc + + def kill(self): + """Kill ``ovs-vswitchd`` instance if it is alive. + + :returns: None + """ + self._logger.info('Killing ovs-vswitchd...') + + self._kill_ovsdb() + + super(VSwitchd, self).kill() + + # helper functions + + def _reset_ovsdb(self): + """Reset system for 'ovsdb'. + + :returns: None + """ + self._logger.info('Resetting system after last run...') + + tasks.run_task(['sudo', 'rm', '-rf', _OVS_VAR_DIR], self._logger) + tasks.run_task(['sudo', 'mkdir', '-p', _OVS_VAR_DIR], self._logger) + tasks.run_task(['sudo', 'rm', '-rf', _OVS_ETC_DIR], self._logger) + tasks.run_task(['sudo', 'mkdir', '-p', _OVS_ETC_DIR], self._logger) + + tasks.run_task(['sudo', 'rm', '-f', + os.path.join(_OVS_ETC_DIR, 'conf.db')], + self._logger) + + self._logger.info('System reset after last run.') + + def _start_ovsdb(self): + """Start ``ovsdb-server`` instance. + + :returns: None + """ + tasks.run_task(['sudo', _OVSDB_TOOL_BIN, 'create', + os.path.join(_OVS_ETC_DIR, 'conf.db'), + os.path.join(settings.getValue('OVS_DIR'), 'vswitchd', + 'vswitch.ovsschema')], + self._logger, + 'Creating ovsdb configuration database...') + + self._ovsdb_pid = tasks.run_background_task( + ['sudo', _OVSDB_SERVER_BIN, + '--remote=punix:%s' % os.path.join(_OVS_VAR_DIR, 'db.sock'), + '--remote=db:Open_vSwitch,Open_vSwitch,manager_options'], + self._logger, + 'Starting ovsdb-server...') + + def _kill_ovsdb(self): + """Kill ``ovsdb-server`` instance. + + :returns: None + """ + if self._ovsdb_pid: + tasks.run_task(['sudo', 'kill', '-2', str(self._ovsdb_pid)], + self._logger, 'Killing ovsdb-server...') diff --git a/src/ovs/ofctl.py b/src/ovs/ofctl.py new file mode 100644 index 00000000..c6aaddc9 --- /dev/null +++ b/src/ovs/ofctl.py @@ -0,0 +1,316 @@ +# Copyright 2015 Intel Corporation. +# +# 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. + +"""Wrapper for an OVS bridge for convenient use of ``ovs-vsctl`` and +``ovs-ofctl`` on it. + +Much of this code is based on ``ovs-lib.py`` from Open Stack: + +https://github.com/openstack/neutron/blob/6eac1dc99124ca024d6a69b3abfa3bc69c735667/neutron/agent/linux/ovs_lib.py +""" + +import os +import logging +import string + +from tools import tasks +from conf import settings + +_OVS_VSCTL_BIN = os.path.join(settings.getValue('OVS_DIR'), 'utilities', + 'ovs-vsctl') +_OVS_OFCTL_BIN = os.path.join(settings.getValue('OVS_DIR'), 'utilities', + 'ovs-ofctl') + +class OFBase(object): + """Add/remove/show datapaths using ``ovs-ofctl``. + """ + def __init__(self, timeout=10): + """Initialise logger. + + :param timeout: Timeout to be used for each command + + :returns: None + """ + self.logger = logging.getLogger(__name__) + self.timeout = timeout + + # helpers + + def run_vsctl(self, args, check_error=False): + """Run ``ovs-vsctl`` with supplied arguments. + + :param args: Arguments to pass to ``ovs-vsctl`` + :param check_error: Throw exception on error + + :return: None + """ + cmd = ['sudo', _OVS_VSCTL_BIN, '--timeout', str(self.timeout)] + args + return tasks.run_task( + cmd, self.logger, 'Running ovs-vsctl...', check_error) + + # datapath management + + def add_br(self, br_name='br0'): + """Add datapath. + + :param br_name: Name of bridge + + :return: Instance of :class OFBridge: + """ + self.logger.debug('add bridge') + self.run_vsctl(['add-br', br_name]) + + return OFBridge(br_name, self.timeout) + + def del_br(self, br_name='br0'): + """Delete datapath. + + :param br_name: Name of bridge + + :return: None + """ + self.logger.debug('delete bridge') + self.run_vsctl(['del-br', br_name]) + + +class OFBridge(OFBase): + """Control a bridge instance using ``ovs-vsctl`` and ``ovs-ofctl``. + """ + def __init__(self, br_name='br0', timeout=10): + """Initialise bridge. + + :param br_name: Bridge name + :param timeout: Timeout to be used for each command + + :returns: None + """ + super(OFBridge, self).__init__(timeout) + self.br_name = br_name + self._ports = {} + + # context manager + + def __enter__(self): + """Create datapath + + :returns: self + """ + return self + + def __exit__(self, type_, value, traceback): + """Remove datapath. + """ + if not traceback: + self.destroy() + + # helpers + + def run_ofctl(self, args, check_error=False): + """Run ``ovs-ofctl`` with supplied arguments. + + :param args: Arguments to pass to ``ovs-ofctl`` + :param check_error: Throw exception on error + + :return: None + """ + cmd = ['sudo', _OVS_OFCTL_BIN, '--timeout', str(self.timeout)] + args + return tasks.run_task( + cmd, self.logger, 'Running ovs-ofctl...', check_error) + + def create(self): + """Create bridge. + """ + self.logger.debug('create bridge') + self.add_br(self.br_name) + + def destroy(self): + """Destroy bridge. + """ + self.logger.debug('destroy bridge') + self.del_br(self.br_name) + + def reset(self): + """Reset bridge. + """ + self.logger.debug('reset bridge') + self.destroy() + self.create() + + # port management + + def add_port(self, port_name, params): + """Add port to bridge. + + :param port_name: Name of port + :param params: Additional list of parameters to add-port + + :return: OpenFlow port number for the port + """ + self.logger.debug('add port') + self.run_vsctl(['add-port', self.br_name, port_name]+params) + + # This is how port number allocation works currently + # This possibly will not work correctly if there are port deletions + # in between + of_port = len(self._ports) + 1 + self._ports[port_name] = (of_port, params) + return of_port + + def del_port(self, port_name): + """Remove port from bridge. + + :param port_name: Name of port + + :return: None + """ + self.logger.debug('delete port') + self.run_vsctl(['del-port', self.br_name, port_name]) + self._ports.pop(port_name) + + def set_db_attribute(self, table_name, record, column, value): + """Set database attribute. + + :param table_name: Name of table + :param record: Name of record + :param column: Name of column + :param value: Value to set + + :return: None + """ + self.logger.debug('set attribute') + self.run_vsctl(['set', table_name, record, '%s=%s' % (column, value)]) + + def get_ports(self): + """Get the ports of this bridge + + Structure of the returned ports dictionary is + 'portname': (openflow_port_number, extra_parameters) + + Example: + ports = { + 'dpdkport0': + (1, ['--', 'set', 'Interface', 'dpdkport0', 'type=dpdk']), + 'dpdkvhostport0': + (2, ['--', 'set', 'Interface', 'dpdkvhostport0', + 'type=dpdkvhost']) + } + + :return: Dictionary of ports + """ + return self._ports + + def clear_db_attribute(self, table_name, record, column): + """Clear database attribute. + + :param table_name: Name of table + :param record: Name of record + :param column: Name of column + + :return: None + """ + self.logger.debug('clear attribute') + self.run_vsctl(['clear', table_name, record, column]) + + # flow mangement + + def add_flow(self, flow): + """Add flow to bridge. + + :param flow: Flow description as a dictionary + For flow dictionary structure, see function flow_key + + :return: None + """ + if not flow.get('actions'): + self.logger.error('add flow requires actions') + return + + self.logger.debug('add flow') + _flow_key = flow_key(flow) + self.logger.debug('key : %s', _flow_key) + self.run_ofctl(['add-flow', self.br_name, _flow_key]) + + def del_flow(self, flow): + """Delete flow from bridge. + + :param flow: Flow description as a dictionary + For flow dictionary structure, see function flow_key + flow=None will delete all flows + + :return: None + """ + self.logger.debug('delete flow') + _flow_key = flow_key(flow) + self.logger.debug('key : %s', _flow_key) + self.run_ofctl(['del-flows', self.br_name, _flow_key]) + + def del_flows(self): + """Delete all flows from bridge. + """ + self.logger.debug('delete flows') + self.run_ofctl(['del-flows', self.br_name]) + + def dump_flows(self): + """Dump all flows from bridge. + """ + self.logger.debug('dump flows') + self.run_ofctl(['dump-flows', self.br_name]) + +# +# helper functions +# + +def flow_key(flow): + """Model a flow key string for ``ovs-ofctl``. + + Syntax taken from ``ovs-ofctl`` manpages: + http://openvswitch.org/cgi-bin/ovsman.cgi?page=utilities%2Fovs-ofctl.8 + + Example flow dictionary: + flow = { + 'in_port': '1', + 'idle_timeout': '0', + 'actions': ['output:3'] + } + + :param flow: Flow description as a dictionary + + :return: String + :rtype: str + """ + _flow_add_key = string.Template('${fields},action=${actions}') + _flow_del_key = string.Template('${fields}') + + field_params = [] + + user_params = (x for x in list(flow.items()) if x[0] != 'actions') + for (key, default) in user_params: + field_params.append('%(field)s=%(value)s' % + {'field': key, 'value': default}) + + field_params = ','.join(field_params) + + _flow_key_param = { + 'fields': field_params, + } + + # no actions == delete key + if 'actions' in flow: + _flow_key_param['actions'] = ','.join(flow['actions']) + + flow_str = _flow_add_key.substitute(_flow_key_param) + else: + flow_str = _flow_del_key.substitute(_flow_key_param) + + return flow_str -- cgit 1.2.3-korg