From f874a4652c12350edb6549730119924a26e3030c Mon Sep 17 00:00:00 2001 From: "Daniel P. Berrange" Date: Wed, 27 Jan 2016 15:55:55 +0000 Subject: [PATCH] import openvswitch plugin implementation While most of the vendor plugins will be in separate repositories, the os-vif library will include the openvswitch plugin as one of the reference implementations. Change-Id: Icd9e452a4f0726823c0b1e07a29bb53a05fd1651 --- setup.cfg | 3 + vif_plug_ovs/__init__.py | 0 vif_plug_ovs/exception.py | 28 ++++++ vif_plug_ovs/i18n.py | 49 ++++++++++ vif_plug_ovs/linux_net.py | 104 ++++++++++++++++++++ vif_plug_ovs/ovs.py | 41 ++++++++ vif_plug_ovs/ovs_hybrid.py | 135 ++++++++++++++++++++++++++ vif_plug_ovs/tests/__init__.py | 0 vif_plug_ovs/tests/test_plugin.py | 155 ++++++++++++++++++++++++++++++ 9 files changed, 515 insertions(+) create mode 100644 vif_plug_ovs/__init__.py create mode 100644 vif_plug_ovs/exception.py create mode 100644 vif_plug_ovs/i18n.py create mode 100644 vif_plug_ovs/linux_net.py create mode 100644 vif_plug_ovs/ovs.py create mode 100644 vif_plug_ovs/ovs_hybrid.py create mode 100644 vif_plug_ovs/tests/__init__.py create mode 100644 vif_plug_ovs/tests/test_plugin.py diff --git a/setup.cfg b/setup.cfg index 862e5efa..7f75f4e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ setup-hooks = packages = os_vif vif_plug_linux_bridge + vif_plug_ovs [egg_info] tag_build = @@ -57,3 +58,5 @@ output_file = os_vif/locale/os-vif.pot [entry_points] os_vif = linux_bridge = vif_plug_linux_bridge.linux_bridge:LinuxBridgePlugin + ovs = vif_plug_ovs.ovs:OvsBridgePlugin + ovs_hybrid = vif_plug_ovs.ovs_hybrid:OvsHybridPlugin diff --git a/vif_plug_ovs/__init__.py b/vif_plug_ovs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vif_plug_ovs/exception.py b/vif_plug_ovs/exception.py new file mode 100644 index 00000000..66dc4937 --- /dev/null +++ b/vif_plug_ovs/exception.py @@ -0,0 +1,28 @@ +# 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 os_vif.i18n import _ + +from os_vif import exception as osv_exception + + +class AgentError(osv_exception.ExceptionBase): + msg_fmt = _('Error during following call to agent: %(method)s') + + +class MissingPortProfile(osv_exception.ExceptionBase): + msg_fmt = _('A port profile is mandatory for the OpenVSwitch plugin') + + +class WrongPortProfile(osv_exception.ExceptionBase): + msg_fmt = _('Port profile %(profile)s is not a subclass ' + 'of VIFPortProfileOpenVSwitch') diff --git a/vif_plug_ovs/i18n.py b/vif_plug_ovs/i18n.py new file mode 100644 index 00000000..8fcbf345 --- /dev/null +++ b/vif_plug_ovs/i18n.py @@ -0,0 +1,49 @@ +# Copyright 2014 IBM Corp. +# +# 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. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html . + +""" + +import oslo_i18n + +# Normally this would be the plugin specific name +# eg 'vif_plug_ovs', but since the OVS plugin is +# in-tree, this is a special case +DOMAIN = 'os_vif' + +_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical + + +def translate(value, user_locale): + return oslo_i18n.translate(value, user_locale) + + +def get_available_languages(): + return oslo_i18n.get_available_languages(DOMAIN) diff --git a/vif_plug_ovs/linux_net.py b/vif_plug_ovs/linux_net.py new file mode 100644 index 00000000..95534129 --- /dev/null +++ b/vif_plug_ovs/linux_net.py @@ -0,0 +1,104 @@ +# Derived from nova/network/linux_net.py +# +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +"""Implements vlans, bridges using linux utilities.""" + +import os + +from oslo_concurrency import processutils +from oslo_log import log as logging +from oslo_utils import excutils + +from vif_plug_ovs import exception +from vif_plug_ovs.i18n import _LE + +LOG = logging.getLogger(__name__) + + +def _ovs_vsctl(args, timeout=None): + full_args = ['ovs-vsctl'] + if timeout is not None: + full_args += ['--timeout=%s' % timeout] + full_args += args + try: + return processutils.execute(*full_args, run_as_root=True) + except Exception as e: + LOG.error(_LE("Unable to execute %(cmd)s. Exception: %(exception)s"), + {'cmd': full_args, 'exception': e}) + raise exception.AgentError(method=full_args) + + +def create_ovs_vif_port(bridge, dev, iface_id, mac, instance_id, mtu, + timeout=None): + _ovs_vsctl(['--', '--if-exists', 'del-port', dev, '--', + 'add-port', bridge, dev, + '--', 'set', 'Interface', dev, + 'external-ids:iface-id=%s' % iface_id, + 'external-ids:iface-status=active', + 'external-ids:attached-mac=%s' % mac, + 'external-ids:vm-uuid=%s' % instance_id], + timeout=timeout) + _set_device_mtu(dev, mtu) + + +def delete_ovs_vif_port(bridge, dev, timeout=None): + _ovs_vsctl(['--', '--if-exists', 'del-port', bridge, dev], + timeout=timeout) + delete_net_dev(dev) + + +def device_exists(device): + """Check if ethernet device exists.""" + return os.path.exists('/sys/class/net/%s' % device) + + +def delete_net_dev(dev): + """Delete a network device only if it exists.""" + if device_exists(dev): + try: + processutils.execute('ip', 'link', 'delete', dev, + check_exit_code=[0, 2, 254], + run_as_root=True) + LOG.debug("Net device removed: '%s'", dev) + except processutils.ProcessExecutionError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Failed removing net device: '%s'"), dev) + + +def create_veth_pair(dev1_name, dev2_name, mtu): + """Create a pair of veth devices with the specified names, + deleting any previous devices with those names. + """ + for dev in [dev1_name, dev2_name]: + delete_net_dev(dev) + + processutils.execute('ip', 'link', 'add', dev1_name, + 'type', 'veth', 'peer', 'name', dev2_name, + run_as_root=True) + for dev in [dev1_name, dev2_name]: + processutils.execute('ip', 'link', 'set', dev, 'up', + run_as_root=True) + processutils.execute('ip', 'link', 'set', dev, 'promisc', 'on') + _set_device_mtu(dev, mtu) + + +def _set_device_mtu(dev, mtu): + """Set the device MTU.""" + processutils.execute('ip', 'link', 'set', dev, 'mtu', mtu, + check_exit_code=[0, 2, 254]) diff --git a/vif_plug_ovs/ovs.py b/vif_plug_ovs/ovs.py new file mode 100644 index 00000000..e73f8bb1 --- /dev/null +++ b/vif_plug_ovs/ovs.py @@ -0,0 +1,41 @@ +# Derived from nova/virt/libvirt/vif.py +# +# Copyright (C) 2011 Midokura KK +# Copyright (C) 2011 Nicira, Inc +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 os_vif import objects +from os_vif import plugin + + +class OvsBridgePlugin(plugin.PluginBase): + """An OVS VIF type that uses a standard Linux bridge for integration.""" + + def describe(self): + return plugin.PluginInfo( + [ + plugin.PluginVIFInfo( + objects.vif.VIFOpenVSwitch, + "1.0", "1.0") + ]) + + def plug(self, vif, instance_info): + # Nothing required to plug an OVS port... + pass + + def unplug(self, vif, instance_info): + # Nothing required to unplug an OVS port... + pass diff --git a/vif_plug_ovs/ovs_hybrid.py b/vif_plug_ovs/ovs_hybrid.py new file mode 100644 index 00000000..82c391bf --- /dev/null +++ b/vif_plug_ovs/ovs_hybrid.py @@ -0,0 +1,135 @@ +# Derived from nova/virt/libvirt/vif.py +# +# Copyright (C) 2011 Midokura KK +# Copyright (C) 2011 Nicira, Inc +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# 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 os_vif import objects +from os_vif import plugin +from oslo_config import cfg + +from oslo_concurrency import processutils + +from vif_plug_ovs import exception +from vif_plug_ovs import linux_net + + +class OvsHybridPlugin(plugin.PluginBase): + """ + An OVS VIF type that uses a pair of devices in order to allow + security group rules to be applied to traffic coming in or out of + a virtual machine. + """ + + NIC_NAME_LEN = 14 + + CONFIG_OPTS = ( + cfg.IntOpt('network_device_mtu', + default=1500, + help='MTU setting for network interface.', + deprecated_group="DEFAULT"), + cfg.IntOpt('ovs_vsctl_timeout', + default=120, + help='Amount of time, in seconds, that ovs_vsctl should ' + 'wait for a response from the database. 0 is to wait ' + 'forever.', + deprecated_group="DEFAULT"), + ) + + @staticmethod + def get_veth_pair_names(vif): + iface_id = vif.id + return (("qvb%s" % iface_id)[:OvsHybridPlugin.NIC_NAME_LEN], + ("qvo%s" % iface_id)[:OvsHybridPlugin.NIC_NAME_LEN]) + + def describe(self): + return plugin.PluginInfo( + [ + plugin.PluginVIFInfo( + objects.vif.VIFBridge, + "1.0", "1.0") + ]) + + def plug(self, vif, instance_info): + """Plug using hybrid strategy + + Create a per-VIF linux bridge, then link that bridge to the OVS + integration bridge via a veth device, setting up the other end + of the veth device just like a normal OVS port. Then boot the + VIF on the linux bridge using standard libvirt mechanisms. + """ + + if not hasattr(vif, "port_profile"): + raise exception.MissingPortProfile() + if not isinstance(vif.port_profile, + objects.vif.VIFPortProfileOpenVSwitch): + raise exception.WrongPortProfile( + profile=vif.port_profile.__class__.__name__) + + v1_name, v2_name = self.get_veth_pair_names(vif) + + if not linux_net.device_exists(vif.bridge_name): + processutils.execute('brctl', 'addbr', vif.bridge_name, + run_as_root=True) + processutils.execute('brctl', 'setfd', vif.bridge_name, 0, + run_as_root=True) + processutils.execute('brctl', 'stp', vif.bridge_name, 'off', + run_as_root=True) + syspath = '/sys/class/net/%s/bridge/multicast_snooping' + syspath = syspath % vif.bridge_name + processutils.execute('tee', syspath, process_input='0', + check_exit_code=[0, 1], + run_as_root=True) + + if not linux_net.device_exists(v2_name): + linux_net.create_veth_pair(v1_name, v2_name, + self.config.network_device_mtu) + processutils.execute('ip', 'link', 'set', vif.bridge_name, 'up', + run_as_root=True) + processutils.execute('brctl', 'addif', vif.bridge_name, v1_name, + run_as_root=True) + linux_net.create_ovs_vif_port( + vif.network.bridge, + v2_name, + vif.port_profile.interface_id, + vif.address, instance_info.uuid, + timeout=self.config.ovs_vsctl_timeout) + + def unplug(self, vif, instance_info): + """UnPlug using hybrid strategy + + Unhook port from OVS, unhook port from bridge, delete + bridge, and delete both veth devices. + """ + if not hasattr(vif, "port_profile"): + raise exception.MissingPortProfile() + if not isinstance(vif.port_profile, + objects.vif.VIFPortProfileOpenVSwitch): + raise exception.WrongPortProfile( + profile=vif.port_profile.__class__.__name__) + + v1_name, v2_name = self.get_veth_pair_names(vif) + + if linux_net.device_exists(vif.bridge_name): + processutils.execute('brctl', 'delif', vif.bridge_name, v1_name, + run_as_root=True) + processutils.execute('ip', 'link', 'set', vif.bridge_name, 'down', + run_as_root=True) + processutils.execute('brctl', 'delbr', vif.bridge_name, + run_as_root=True) + + linux_net.delete_ovs_vif_port(vif.network.bridge, v2_name, + timeout=self.config.ovs_vsctl_timeout) diff --git a/vif_plug_ovs/tests/__init__.py b/vif_plug_ovs/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vif_plug_ovs/tests/test_plugin.py b/vif_plug_ovs/tests/test_plugin.py new file mode 100644 index 00000000..128d692f --- /dev/null +++ b/vif_plug_ovs/tests/test_plugin.py @@ -0,0 +1,155 @@ +# 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 contextlib +import mock +import six +import testtools + +from os_vif import objects + +from oslo_concurrency import processutils + +from vif_plug_ovs import linux_net +from vif_plug_ovs import ovs_hybrid + + +if six.PY2: + nested = contextlib.nested +else: + @contextlib.contextmanager + def nested(*contexts): + with contextlib.ExitStack() as stack: + yield [stack.enter_context(c) for c in contexts] + + +class PluginTest(testtools.TestCase): + + def __init__(self, *args, **kwargs): + super(PluginTest, self).__init__(*args, **kwargs) + + objects.register_all() + + self.subnet_bridge_4 = objects.subnet.Subnet( + cidr='101.168.1.0/24', + dns=['8.8.8.8'], + gateway='101.168.1.1', + dhcp_server='191.168.1.1') + + self.subnet_bridge_6 = objects.subnet.Subnet( + cidr='101:1db9::/64', + gateway='101:1db9::1') + + self.subnets = objects.subnet.SubnetList( + objects=[self.subnet_bridge_4, + self.subnet_bridge_6]) + + self.network_ovs = objects.network.Network( + id='network-id-xxx-yyy-zzz', + bridge='br0', + subnets=self.subnets, + vlan=99) + + self.profile_ovs = objects.vif.VIFPortProfileOpenVSwitch( + interface_id='aaa-bbb-ccc') + self.vif_ovs = objects.vif.VIFBridge( + id='vif-xxx-yyy-zzz', + address='ca:fe:de:ad:be:ef', + network=self.network_ovs, + dev_name='tap-xxx-yyy-zzz', + bridge_name="qbrvif-xxx-yyy", + port_profile=self.profile_ovs) + + self.instance = objects.instance_info.InstanceInfo( + name='demo', + uuid='f0000000-0000-0000-0000-000000000001') + + def test_plug_ovs_hybrid(self): + calls = { + 'device_exists': [mock.call('qbrvif-xxx-yyy'), + mock.call('qvovif-xxx-yyy')], + '_create_veth_pair': [mock.call('qvbvif-xxx-yyy', + 'qvovif-xxx-yyy', + 1500)], + 'execute': [mock.call('brctl', 'addbr', 'qbrvif-xxx-yyy', + run_as_root=True), + mock.call('brctl', 'setfd', 'qbrvif-xxx-yyy', 0, + run_as_root=True), + mock.call('brctl', 'stp', 'qbrvif-xxx-yyy', 'off', + run_as_root=True), + mock.call('tee', ('/sys/class/net/qbrvif-xxx-yyy' + '/bridge/multicast_snooping'), + process_input='0', run_as_root=True, + check_exit_code=[0, 1]), + mock.call('ip', 'link', 'set', 'qbrvif-xxx-yyy', 'up', + run_as_root=True), + mock.call('brctl', 'addif', 'qbrvif-xxx-yyy', + 'qvbvif-xxx-yyy', run_as_root=True)], + 'create_ovs_vif_port': [mock.call( + 'br0', 'qvovif-xxx-yyy', 'aaa-bbb-ccc', + 'ca:fe:de:ad:be:ef', + 'f0000000-0000-0000-0000-000000000001', + timeout=120)] + } + with nested( + mock.patch.object(linux_net, 'device_exists', + return_value=False), + mock.patch.object(processutils, 'execute'), + mock.patch.object(linux_net, 'create_veth_pair'), + mock.patch.object(linux_net, 'create_ovs_vif_port') + ) as (device_exists, execute, _create_veth_pair, create_ovs_vif_port): + plugin = ovs_hybrid.OvsHybridPlugin.load("ovs_hybrid") + plugin.plug(self.vif_ovs, self.instance) + device_exists.assert_has_calls(calls['device_exists']) + _create_veth_pair.assert_has_calls(calls['_create_veth_pair']) + execute.assert_has_calls(calls['execute']) + create_ovs_vif_port.assert_has_calls(calls['create_ovs_vif_port']) + + def test_unplug_ovs_hybrid(self): + calls = { + 'device_exists': [mock.call('qbrvif-xxx-yyy')], + 'execute': [mock.call('brctl', 'delif', 'qbrvif-xxx-yyy', + 'qvbvif-xxx-yyy', run_as_root=True), + mock.call('ip', 'link', 'set', + 'qbrvif-xxx-yyy', 'down', run_as_root=True), + mock.call('brctl', 'delbr', + 'qbrvif-xxx-yyy', run_as_root=True)], + 'delete_ovs_vif_port': [mock.call('br0', 'qvovif-xxx-yyy', + timeout=120)] + } + with nested( + mock.patch.object(linux_net, 'device_exists', + return_value=True), + mock.patch.object(processutils, 'execute'), + mock.patch.object(linux_net, 'delete_ovs_vif_port') + ) as (device_exists, execute, delete_ovs_vif_port): + plugin = ovs_hybrid.OvsHybridPlugin.load("ovs_hybrid") + plugin.unplug(self.vif_ovs, self.instance) + device_exists.assert_has_calls(calls['device_exists']) + execute.assert_has_calls(calls['execute']) + delete_ovs_vif_port.assert_has_calls(calls['delete_ovs_vif_port']) + + def test_unplug_ovs_hybrid_bridge_does_not_exist(self): + calls = { + 'device_exists': [mock.call('qbrvif-xxx-yyy')], + 'delete_ovs_vif_port': [mock.call('br0', 'qvovif-xxx-yyy', + timeout=120)] + } + with nested( + mock.patch.object(linux_net, 'device_exists', + return_value=False), + mock.patch.object(linux_net, 'delete_ovs_vif_port') + ) as (device_exists, delete_ovs_vif_port): + plugin = ovs_hybrid.OvsHybridPlugin.load("ovs_hybrid") + plugin.unplug(self.vif_ovs, self.instance) + device_exists.assert_has_calls(calls['device_exists']) + delete_ovs_vif_port.assert_has_calls(calls['delete_ovs_vif_port'])