diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/__init__.py | 21 | ||||
-rw-r--r-- | src/dpdk/__init__.py | 21 | ||||
-rw-r--r-- | src/dpdk/dpdk.py | 377 | ||||
-rw-r--r-- | src/ovs/__init__.py | 25 | ||||
-rw-r--r-- | src/ovs/daemon.py | 142 | ||||
-rw-r--r-- | src/ovs/ofctl.py | 316 |
6 files changed, 902 insertions, 0 deletions
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<num_hp>\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 |