From 2e7eb09271912e9db1948b15ab3f8e184d4c324a Mon Sep 17 00:00:00 2001 From: Andreas Scheuring Date: Tue, 2 Feb 2016 16:34:59 +0100 Subject: [PATCH] macvtap: Macvtap L2 Agent This agent is required by the macvtap ml2 driver to support macvtap attachments for libvirt qemu/kvm instances. It introduces a new configuration option MACVTAP.physical_interface_mappings. The review is submitted in three parts: - Part 1 Common functions that are used by the ml2 driver and the agent - Part 2 The Mechanism Driver to support port binding for macvtap attachments - Part 3 (this part) The Macvtap L2 Agent. DocImpact New ML2 mech driver + l2 agent New config option "macvtap.physical_interface_mappings" Change-Id: I219d80b4c704ac2f41edd3501f4b2198925778d6 Closes-Bug: #1480979 --- neutron/agent/linux/ip_lib.py | 9 + .../eventlet/plugins/macvtap_neutron_agent.py | 20 ++ neutron/common/constants.py | 2 + neutron/opts.py | 12 + .../ml2/drivers/macvtap/agent/__init__.py | 0 .../ml2/drivers/macvtap/agent/config.py | 35 +++ .../macvtap/agent/macvtap_neutron_agent.py | 211 ++++++++++++++++ neutron/tests/common/net_helpers.py | 35 +++ .../plugins/ml2/drivers/__init__.py | 0 .../plugins/ml2/drivers/macvtap/__init__.py | 0 .../ml2/drivers/macvtap/agent/__init__.py | 0 .../agent/test_macvtap_neutron_agent.py | 36 +++ neutron/tests/unit/agent/linux/test_ip_lib.py | 13 + .../ml2/drivers/macvtap/agent/__init__.py | 0 .../agent/test_macvtap_neutron_agent.py | 237 ++++++++++++++++++ .../macvtap-l2-agent-2b551d8ec341196d.yaml | 19 ++ setup.cfg | 2 + 17 files changed, 631 insertions(+) create mode 100644 neutron/cmd/eventlet/plugins/macvtap_neutron_agent.py create mode 100644 neutron/plugins/ml2/drivers/macvtap/agent/__init__.py create mode 100644 neutron/plugins/ml2/drivers/macvtap/agent/config.py create mode 100644 neutron/plugins/ml2/drivers/macvtap/agent/macvtap_neutron_agent.py create mode 100644 neutron/tests/functional/plugins/ml2/drivers/__init__.py create mode 100644 neutron/tests/functional/plugins/ml2/drivers/macvtap/__init__.py create mode 100644 neutron/tests/functional/plugins/ml2/drivers/macvtap/agent/__init__.py create mode 100644 neutron/tests/functional/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/macvtap/agent/__init__.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py create mode 100644 releasenotes/notes/macvtap-l2-agent-2b551d8ec341196d.yaml diff --git a/neutron/agent/linux/ip_lib.py b/neutron/agent/linux/ip_lib.py index cc8cc837092..b9dfa25c18a 100644 --- a/neutron/agent/linux/ip_lib.py +++ b/neutron/agent/linux/ip_lib.py @@ -176,6 +176,12 @@ class IPWrapper(SubProcessBase): return (IPDevice(name1, namespace=self.namespace), IPDevice(name2, namespace=namespace2)) + def add_macvtap(self, name, src_dev, mode='bridge'): + args = ['add', 'link', src_dev, 'name', name, 'type', 'macvtap', + 'mode', mode] + self._as_root([], 'link', tuple(args)) + return IPDevice(name, namespace=self.namespace) + def del_veth(self, name): """Delete a virtual interface between two namespaces.""" self._as_root([], 'link', ('del', name)) @@ -455,6 +461,9 @@ class IpLinkCommand(IpDeviceCommandBase): def set_address(self, mac_address): self._as_root([], ('set', self.name, 'address', mac_address)) + def set_allmulticast_on(self): + self._as_root([], ('set', self.name, 'allmulticast', 'on')) + def set_mtu(self, mtu_size): self._as_root([], ('set', self.name, 'mtu', mtu_size)) diff --git a/neutron/cmd/eventlet/plugins/macvtap_neutron_agent.py b/neutron/cmd/eventlet/plugins/macvtap_neutron_agent.py new file mode 100644 index 00000000000..9438dbac53f --- /dev/null +++ b/neutron/cmd/eventlet/plugins/macvtap_neutron_agent.py @@ -0,0 +1,20 @@ +# 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 neutron.plugins.ml2.drivers.macvtap.agent import ( + macvtap_neutron_agent as agent_main) + + +def main(): + agent_main.main() diff --git a/neutron/common/constants.py b/neutron/common/constants.py index 7c632aaca68..eb18f7e7d88 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -153,6 +153,8 @@ DEVICE_NAME_MAX_LEN = 15 # vhost-user device names start with "vhu" VHOST_USER_DEVICE_PREFIX = 'vhu' +# Device names start with "macvtap" +MACVTAP_DEVICE_PREFIX = 'macvtap' # The vswitch side of a veth pair for a nova iptables filter setup VETH_DEVICE_PREFIX = 'qvo' # prefix for SNAT interface in DVR diff --git a/neutron/opts.py b/neutron/opts.py index fba7402aa44..41668119cdb 100644 --- a/neutron/opts.py +++ b/neutron/opts.py @@ -45,6 +45,7 @@ import neutron.openstack.common.cache.cache import neutron.plugins.ml2.config import neutron.plugins.ml2.drivers.agent.config import neutron.plugins.ml2.drivers.linuxbridge.agent.common.config +import neutron.plugins.ml2.drivers.macvtap.agent.config import neutron.plugins.ml2.drivers.mech_sriov.agent.common.config import neutron.plugins.ml2.drivers.mech_sriov.mech_driver.mech_driver import neutron.plugins.ml2.drivers.openvswitch.agent.common.config @@ -199,6 +200,17 @@ def list_l3_agent_opts(): ] +def list_macvtap_opts(): + return [ + ('macvtap', + neutron.plugins.ml2.drivers.macvtap.agent.config.macvtap_opts), + ('agent', + neutron.plugins.ml2.drivers.agent.config.agent_opts), + ('securitygroup', + neutron.agent.securitygroups_rpc.security_group_opts) + ] + + def list_metadata_agent_opts(): return [ ('DEFAULT', diff --git a/neutron/plugins/ml2/drivers/macvtap/agent/__init__.py b/neutron/plugins/ml2/drivers/macvtap/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/ml2/drivers/macvtap/agent/config.py b/neutron/plugins/ml2/drivers/macvtap/agent/config.py new file mode 100644 index 00000000000..ebab9a1881b --- /dev/null +++ b/neutron/plugins/ml2/drivers/macvtap/agent/config.py @@ -0,0 +1,35 @@ +# Copyright (c) 2016 IBM Corp. +# +# 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 oslo_config import cfg + +DEFAULT_INTERFACE_MAPPINGS = [] + +macvtap_opts = [ + cfg.ListOpt('physical_interface_mappings', + default=DEFAULT_INTERFACE_MAPPINGS, + help=_("Comma-separated list of " + ": tuples " + "mapping physical network names to the agent's " + "node-specific physical network interfaces to be used " + "for flat and VLAN networks. All physical networks " + "listed in network_vlan_ranges on the server should " + "have mappings to appropriate interfaces on each " + "agent.")), +] + + +cfg.CONF.register_opts(macvtap_opts, "macvtap") diff --git a/neutron/plugins/ml2/drivers/macvtap/agent/macvtap_neutron_agent.py b/neutron/plugins/ml2/drivers/macvtap/agent/macvtap_neutron_agent.py new file mode 100644 index 00000000000..11987b5c2e3 --- /dev/null +++ b/neutron/plugins/ml2/drivers/macvtap/agent/macvtap_neutron_agent.py @@ -0,0 +1,211 @@ +# Copyright (c) 2016 IBM Corp. +# +# 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. + +import os +import sys + +from oslo_config import cfg +from oslo_log import log as logging +import oslo_messaging +from oslo_service import service + +from neutron._i18n import _LE, _LI +from neutron.agent.linux import ip_lib +from neutron.agent.linux import utils +from neutron.agent import securitygroups_rpc as sg_rpc +from neutron.common import config as common_config +from neutron.common import constants +from neutron.common import topics +from neutron.common import utils as n_utils +from neutron.plugins.common import constants as p_constants +from neutron.plugins.ml2.drivers.agent import _agent_manager_base as amb +from neutron.plugins.ml2.drivers.agent import _common_agent as ca +from neutron.plugins.ml2.drivers.macvtap.agent import config # noqa +from neutron.plugins.ml2.drivers.macvtap import macvtap_common + +LOG = logging.getLogger(__name__) + +MACVTAP_AGENT_BINARY = "neutron-macvtap-agent" +MACVTAP_FS = "/sys/class/net/" +EXTENSION_DRIVER_TYPE = 'macvtap' + + +class MacvtapRPCCallBack(sg_rpc.SecurityGroupAgentRpcCallbackMixin, + amb.CommonAgentManagerRpcCallBackBase): + # Set RPC API version to 1.0 by default. + # history + # 1.1 Support Security Group RPC + # 1.3 Added param devices_to_update to security_groups_provider_updated + # 1.4 Added support for network_update + target = oslo_messaging.Target(version='1.4') + + def network_delete(self, context, **kwargs): + LOG.debug("network_delete received") + network_id = kwargs.get('network_id') + + if network_id not in self.network_map: + LOG.error(_LE("Network %s is not available."), network_id) + return + + segment = self.network_map.get(network_id) + if segment and segment.network_type == p_constants.TYPE_VLAN: + if_mappings = self.agent.mgr.interface_mappings + vlan_device_name = macvtap_common.get_vlan_device_name( + if_mappings[segment.physical_network], + str(segment.segmentation_id)) + ip_dev = ip_lib.IPDevice(vlan_device_name) + if ip_dev.exists(): + LOG.debug("Delete %s", ip_dev.name) + ip_dev.link.delete() + else: + LOG.debug("Cannot delete vlan device %s; it does not exist", + vlan_device_name) + + def port_update(self, context, **kwargs): + port = kwargs['port'] + LOG.debug("port_update received for port %s ", port) + mac = port['mac_address'] + # Put the device name in the updated_devices set. + # Do not store port details, as if they're used for processing + # notifications there is no guarantee the notifications are + # processed in the same order as the relevant API requests. + self.updated_devices.add(mac) + + +class MacvtapManager(amb.CommonAgentManagerBase): + def __init__(self, interface_mappings): + self.interface_mappings = interface_mappings + self.validate_interface_mappings() + self.mac_device_name_mappings = dict() + + def validate_interface_mappings(self): + for physnet, interface in self.interface_mappings.items(): + if not ip_lib.device_exists(interface): + LOG.error(_LE("Interface %(intf)s for physical network " + "%(net)s does not exist. Agent terminated!"), + {'intf': interface, 'net': physnet}) + sys.exit(1) + + def ensure_port_admin_state(self, device, admin_state_up): + LOG.debug("Setting admin_state_up to %s for device %s", + admin_state_up, device) + dev = ip_lib.IPDevice(self.mac_device_name_mappings[device]) + if admin_state_up: + dev.link.set_up() + else: + dev.link.set_down() + + def get_agent_configurations(self): + return {'interface_mappings': self.interface_mappings} + + def get_agent_id(self): + devices = ip_lib.IPWrapper().get_devices(True) + if devices: + mac = utils.get_interface_mac(devices[0].name) + return 'macvtap%s' % mac.replace(":", "") + else: + LOG.error(_LE("Unable to obtain MAC address for unique ID. " + "Agent terminated!")) + sys.exit(1) + + def get_all_devices(self): + devices = set() + all_device_names = os.listdir(MACVTAP_FS) + # Refresh the mac_device_name mapping + self.mac_device_name_mappings = dict() + for device_name in all_device_names: + if device_name.startswith(constants.MACVTAP_DEVICE_PREFIX): + mac = utils.get_interface_mac(device_name) + self.mac_device_name_mappings[mac] = device_name + devices.add(mac) + return devices + + def get_extension_driver_type(self): + return EXTENSION_DRIVER_TYPE + + def get_rpc_callbacks(self, context, agent, sg_agent): + return MacvtapRPCCallBack(context, agent, sg_agent) + + def get_rpc_consumers(self): + consumers = [[topics.PORT, topics.UPDATE], + [topics.NETWORK, topics.DELETE], + [topics.SECURITY_GROUP, topics.UPDATE]] + return consumers + + def plug_interface(self, network_id, network_segment, device, + device_owner): + # Setting ALLMULTICAST Flag on macvtap device to allow the guest + # receiving traffic for arbitrary multicast addresses. + # The alternative would be to let libvirt instantiate the macvtap + # device with the 'trustGuestRxFilters' option. But doing so, the guest + # would be able to change its mac address and therefore the mac + # address of the macvtap device. + dev = ip_lib.IPDevice(self.mac_device_name_mappings[device]) + dev.link.set_allmulticast_on() + return True + + def setup_arp_spoofing_protection(self, device, device_details): + pass + + def delete_arp_spoofing_protection(self, devices): + pass + + def delete_unreferenced_arp_protection(self, current_devices): + pass + + +def parse_interface_mappings(): + try: + interface_mappings = n_utils.parse_mappings( + cfg.CONF.macvtap.physical_interface_mappings) + LOG.info(_LI("Interface mappings: %s"), interface_mappings) + return interface_mappings + except ValueError as e: + LOG.error(_LE("Parsing physical_interface_mappings failed: %s. " + "Agent terminated!"), e) + sys.exit(1) + + +def validate_firewall_driver(): + fw_driver = cfg.CONF.SECURITYGROUP.firewall_driver + if fw_driver != 'neutron.agent.firewall.NoopFirewallDriver': + LOG.error(_LE('Unsupported configuration option for "SECURITYGROUP.' + 'firewall_driver"! Only "neutron.agent.firewall.' + 'NoopFirewallDriver" is supported by macvtap agent, but' + '"%s" is configured. Agent terminated!'), + fw_driver) + sys.exit(1) + + +def main(): + common_config.init(sys.argv[1:]) + + common_config.setup_logging() + + validate_firewall_driver() + interface_mappings = parse_interface_mappings() + + manager = MacvtapManager(interface_mappings) + + polling_interval = cfg.CONF.AGENT.polling_interval + quitting_rpc_timeout = cfg.CONF.AGENT.quitting_rpc_timeout + agent = ca.CommonAgentLoop(manager, polling_interval, + quitting_rpc_timeout, + constants.AGENT_TYPE_MACVTAP, + MACVTAP_AGENT_BINARY) + LOG.info(_LI("Agent initialized successfully, now running... ")) + launcher = service.launch(cfg.CONF, agent) + launcher.wait() diff --git a/neutron/tests/common/net_helpers.py b/neutron/tests/common/net_helpers.py index 00e3007336d..759a75872ee 100644 --- a/neutron/tests/common/net_helpers.py +++ b/neutron/tests/common/net_helpers.py @@ -53,6 +53,7 @@ PORT_PREFIX = 'port' VETH0_PREFIX = 'test-veth0' VETH1_PREFIX = 'test-veth1' PATCH_PREFIX = 'patch' +MACVTAP_PREFIX = 'macvtap' # port name should be shorter than DEVICE_NAME_MAX_LEN because if this # port is used to provide vlan connection between two linuxbridge @@ -543,6 +544,40 @@ class NamedVethFixture(VethFixture): return name +class MacvtapFixture(fixtures.Fixture): + """Create a macvtap. + + :param src_dev: source device for macvtap + :type src_dev: IPDevice + :param mode: mode of macvtap + :type mode: string + :ivar ip_dev: created macvtap + :type ip_dev: IPDevice + """ + def __init__(self, src_dev=None, mode=None, prefix=MACVTAP_PREFIX): + super(MacvtapFixture, self).__init__() + self.src_dev = src_dev + self.mode = mode + self.prefix = prefix + + def _setUp(self): + ip_wrapper = ip_lib.IPWrapper() + self.ip_dev = common_base.create_resource( + self.prefix, + ip_wrapper.add_macvtap, + self.src_dev, mode=self.mode) + self.addCleanup(self.destroy) + + def destroy(self): + ip_wrapper = ip_lib.IPWrapper(self.ip_dev.namespace) + if (ip_wrapper.netns.exists(self.ip_dev.namespace) or + self.ip_dev.namespace is None): + try: + self.ip_dev.link.delete() + except RuntimeError: + pass + + @six.add_metaclass(abc.ABCMeta) class PortFixture(fixtures.Fixture): """Create a port. diff --git a/neutron/tests/functional/plugins/ml2/drivers/__init__.py b/neutron/tests/functional/plugins/ml2/drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/functional/plugins/ml2/drivers/macvtap/__init__.py b/neutron/tests/functional/plugins/ml2/drivers/macvtap/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/functional/plugins/ml2/drivers/macvtap/agent/__init__.py b/neutron/tests/functional/plugins/ml2/drivers/macvtap/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/functional/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py b/neutron/tests/functional/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py new file mode 100644 index 00000000000..04825f9c6da --- /dev/null +++ b/neutron/tests/functional/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py @@ -0,0 +1,36 @@ +# Copyright (c) 2016 IBM Corp. +# +# 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 neutron.common import constants +from neutron.plugins.ml2.drivers.macvtap.agent import macvtap_neutron_agent +from neutron.tests.common import net_helpers +from neutron.tests.functional import base as functional_base + + +class MacvtapAgentTestCase(functional_base.BaseSudoTestCase): + def setUp(self): + super(MacvtapAgentTestCase, self).setUp() + self.mgr = macvtap_neutron_agent.MacvtapManager({}) + + def test_get_all_devices(self): + # Veth is simulating the hosts eth device. In this test it is used as + # src_dev for the macvtap + veth1, veth2 = self.useFixture(net_helpers.VethFixture()).ports + macvtap = self.useFixture(net_helpers.MacvtapFixture( + src_dev=veth1.name, mode='bridge', + prefix=constants.MACVTAP_DEVICE_PREFIX)).ip_dev + self.assertEqual(set([macvtap.link.address]), + self.mgr.get_all_devices()) diff --git a/neutron/tests/unit/agent/linux/test_ip_lib.py b/neutron/tests/unit/agent/linux/test_ip_lib.py index bde884315e9..9627cd79993 100644 --- a/neutron/tests/unit/agent/linux/test_ip_lib.py +++ b/neutron/tests/unit/agent/linux/test_ip_lib.py @@ -331,6 +331,15 @@ class TestIpWrapper(base.BaseTestCase): run_as_root=True, namespace=None, log_fail_as_error=True) + def test_add_macvtap(self): + ip_lib.IPWrapper().add_macvtap('macvtap0', 'eth0', 'bridge') + self.execute.assert_called_once_with([], 'link', + ('add', 'link', 'eth0', 'name', + 'macvtap0', 'type', 'macvtap', + 'mode', 'bridge'), + run_as_root=True, namespace=None, + log_fail_as_error=True) + def test_del_veth(self): ip_lib.IPWrapper().del_veth('fpr-1234') self.execute.assert_called_once_with([], 'link', @@ -697,6 +706,10 @@ class TestIpLinkCommand(TestIPCmdBase): self.link_cmd.set_address('aa:bb:cc:dd:ee:ff') self._assert_sudo([], ('set', 'eth0', 'address', 'aa:bb:cc:dd:ee:ff')) + def test_set_allmulticast_on(self): + self.link_cmd.set_allmulticast_on() + self._assert_sudo([], ('set', 'eth0', 'allmulticast', 'on')) + def test_set_mtu(self): self.link_cmd.set_mtu(1500) self._assert_sudo([], ('set', 'eth0', 'mtu', 1500)) diff --git a/neutron/tests/unit/plugins/ml2/drivers/macvtap/agent/__init__.py b/neutron/tests/unit/plugins/ml2/drivers/macvtap/agent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py b/neutron/tests/unit/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py new file mode 100644 index 00000000000..fb20980bf5e --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/macvtap/agent/test_macvtap_neutron_agent.py @@ -0,0 +1,237 @@ +# Copyright (c) 2016 IBM Corp. +# +# 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. + +import os +import sys + +import mock + +from oslo_config import cfg +from oslo_service import service + +from neutron.agent.linux import ip_lib +from neutron.agent.linux import utils +from neutron.common import config as common_config +from neutron.common import topics +from neutron.common import utils as n_utils +from neutron.plugins.ml2.drivers.agent import _agent_manager_base as amb +from neutron.plugins.ml2.drivers.macvtap.agent import macvtap_neutron_agent +from neutron.plugins.ml2.drivers.macvtap import macvtap_common +from neutron.tests import base + + +INTERFACE_MAPPINGS = {'physnet1': 'eth1'} +NETWORK_ID = 'net-id123' +NETWORK_SEGMENT_VLAN = amb.NetworkSegment('vlan', 'physnet1', 1) +NETWORK_SEGMENT_FLAT = amb.NetworkSegment('flat', 'physnet1', None) + + +class TestMacvtapRPCCallbacks(base.BaseTestCase): + def setUp(self): + super(TestMacvtapRPCCallbacks, self).setUp() + + agent = mock.Mock() + agent.mgr = mock.Mock() + agent.mgr.interface_mappings = INTERFACE_MAPPINGS + self.rpc = macvtap_neutron_agent.MacvtapRPCCallBack(mock.Mock(), agent, + mock.Mock()) + + def test_network_delete_vlan(self): + self.rpc.network_map = {NETWORK_ID: NETWORK_SEGMENT_VLAN} + with mock.patch.object(ip_lib.IpLinkCommand, 'delete') as mock_del,\ + mock.patch.object(macvtap_common, 'get_vlan_device_name', + return_value='vlan1'),\ + mock.patch.object(ip_lib.IPDevice, 'exists', return_value=True): + self.rpc.network_delete("anycontext", network_id=NETWORK_ID) + self.assertTrue(mock_del.called) + + def test_network_delete_flat(self): + self.rpc.network_map = {NETWORK_ID: NETWORK_SEGMENT_FLAT} + with mock.patch.object(ip_lib.IpLinkCommand, 'delete') as mock_del: + self.rpc.network_delete( + "anycontext", network_id=NETWORK_SEGMENT_FLAT.segmentation_id) + self.assertFalse(mock_del.called) + + def test_port_update(self): + port = {'id': 'port-id123', 'mac_address': 'mac1'} + self.rpc.port_update(context=None, port=port) + self.assertEqual(set(['mac1']), self.rpc.updated_devices) + + +class TestMacvtapManager(base.BaseTestCase): + def setUp(self): + super(TestMacvtapManager, self).setUp() + with mock.patch.object(ip_lib, 'device_exists', return_value=True): + self.mgr = macvtap_neutron_agent.MacvtapManager(INTERFACE_MAPPINGS) + + def test_validate_interface_mappings_dev_exists(self): + good_mapping = {'physnet1': 'eth1', 'physnet2': 'eth2'} + self.mgr.interface_mappings = good_mapping + with mock.patch.object(ip_lib, 'device_exists', return_value=True)\ + as mock_de: + self.mgr.validate_interface_mappings() + mock_de.assert_any_call('eth1') + mock_de.assert_any_call('eth2') + self.assertEqual(2, mock_de.call_count) + + def test_validate_interface_mappings_dev_not_exists(self): + bad_mapping = {'physnet1': 'foo'} + self.mgr.interface_mappings = bad_mapping + with mock.patch.object(ip_lib, 'device_exists', return_value=False)\ + as mock_de, mock.patch.object(sys, 'exit') as mock_exit: + self.mgr.validate_interface_mappings() + mock_de.assert_called_with('foo') + mock_exit.assert_called_once_with(1) + + def _test_ensure_port_admin_state(self, admin_state): + dev = 'macvtap1' + mac = 'mac1' + + self.mgr.mac_device_name_mappings = {mac: dev} + with mock.patch.object(ip_lib, 'IPDevice') as mock_ip_dev: + self.mgr.ensure_port_admin_state(mac, admin_state) + self.assertEqual(admin_state, mock_ip_dev(dev).link.set_up.called) + self.assertNotEqual(admin_state, + mock_ip_dev(dev).link.set_down.called) + + def test_ensure_port_admin_state_up(self): + self._test_ensure_port_admin_state(True) + + def test_ensure_port_admin_state_down(self): + self._test_ensure_port_admin_state(False) + + def test_get_all_devices(self): + listing = ['foo', 'macvtap0', 'macvtap1', 'bar'] + # set some mac mappings to make sure they are cleaned up + self.mgr.mac_device_name_mappings = {'foo': 'bar'} + with mock.patch.object(os, 'listdir', return_value=listing)\ + as mock_ld,\ + mock.patch.object(utils, 'get_interface_mac') as mock_gdn: + mock_gdn.side_effect = ['mac0', 'mac1'] + + result = self.mgr.get_all_devices() + mock_ld.assert_called_once_with(macvtap_neutron_agent.MACVTAP_FS) + self.assertEqual(set(['mac0', 'mac1']), result) + self.assertEqual({'mac0': 'macvtap0', 'mac1': 'macvtap1'}, + self.mgr.mac_device_name_mappings) + + def test_get_agent_configurations(self): + expected = {'interface_mappings': INTERFACE_MAPPINGS} + self.assertEqual(expected, self.mgr.get_agent_configurations()) + + def test_get_agent_id_ok(self): + mock_devices = [ip_lib.IPDevice('macvtap1')] + with mock.patch.object(ip_lib.IPWrapper, 'get_devices', + return_value=mock_devices),\ + mock.patch.object(utils, 'get_interface_mac', + return_value='foo:bar'): + self.assertEqual('macvtapfoobar', self.mgr.get_agent_id()) + + def test_get_agent_id_fail(self): + mock_devices = [] + with mock.patch.object(ip_lib.IPWrapper, 'get_devices', + return_value=mock_devices),\ + mock.patch.object(sys, 'exit') as mock_exit: + self.mgr.get_agent_id() + mock_exit.assert_called_once_with(1) + + def test_get_extension_driver_type(self): + self.assertEqual('macvtap', self.mgr.get_extension_driver_type()) + + def test_get_rpc_callbacks(self): + context = mock.Mock() + agent = mock.Mock() + sg_agent = mock.Mock() + obj = self.mgr.get_rpc_callbacks(context, agent, sg_agent) + self.assertIsInstance(obj, macvtap_neutron_agent.MacvtapRPCCallBack) + + def test_get_rpc_consumers(self): + consumers = [[topics.PORT, topics.UPDATE], + [topics.NETWORK, topics.DELETE], + [topics.SECURITY_GROUP, topics.UPDATE]] + self.assertEqual(consumers, self.mgr.get_rpc_consumers()) + + def test_plug_interface(self): + self.mgr.mac_device_name_mappings['mac1'] = 'macvtap0' + with mock.patch.object(ip_lib.IpLinkCommand, 'set_allmulticast_on')\ + as mock_sao: + self.mgr.plug_interface('network_id', 'network_segment', 'mac1', + 'device_owner') + self.assertTrue(mock_sao.called) + + +class TestMacvtapMain(base.BaseTestCase): + def test_parse_interface_mappings_good(self): + cfg.CONF.set_override('physical_interface_mappings', 'good_mapping', + 'macvtap') + with mock.patch.object(n_utils, 'parse_mappings', + return_value=INTERFACE_MAPPINGS): + mappings = macvtap_neutron_agent.parse_interface_mappings() + self.assertEqual(INTERFACE_MAPPINGS, mappings) + + def test_parse_interface_mappings_bad(self): + cfg.CONF.set_override('physical_interface_mappings', 'bad_mapping', + 'macvtap') + with mock.patch.object(n_utils, 'parse_mappings', + side_effect=ValueError('bad mapping')),\ + mock.patch.object(sys, 'exit') as mock_exit: + macvtap_neutron_agent.parse_interface_mappings() + mock_exit.assert_called_with(1) + + def test_validate_firewall_driver_noop(self): + cfg.CONF.set_override('firewall_driver', + 'neutron.agent.firewall.NoopFirewallDriver', + 'SECURITYGROUP') + macvtap_neutron_agent.validate_firewall_driver() + + def test_validate_firewall_driver_other(self): + cfg.CONF.set_override('firewall_driver', + 'foo', + 'SECURITYGROUP') + with mock.patch.object(sys, 'exit')as mock_exit: + macvtap_neutron_agent.validate_firewall_driver() + mock_exit.assert_called_with(1) + + def test_main(self): + cfg.CONF.set_override('quitting_rpc_timeout', 1, 'AGENT') + cfg.CONF.set_override('polling_interval', 2, 'AGENT') + + mock_manager_return = mock.Mock(spec=amb.CommonAgentManagerBase) + mock_launch_return = mock.Mock() + + with mock.patch.object(common_config, 'init'),\ + mock.patch.object(common_config, 'setup_logging'),\ + mock.patch.object(service, 'launch', + return_value=mock_launch_return) as mock_launch,\ + mock.patch.object(macvtap_neutron_agent, + 'parse_interface_mappings', + return_value=INTERFACE_MAPPINGS) as mock_pim,\ + mock.patch.object(macvtap_neutron_agent, + 'validate_firewall_driver') as mock_vfd,\ + mock.patch('neutron.plugins.ml2.drivers.agent._common_agent.' + 'CommonAgentLoop') as mock_loop,\ + mock.patch('neutron.plugins.ml2.drivers.macvtap.agent.' + 'macvtap_neutron_agent.MacvtapManager', + return_value=mock_manager_return) as mock_manager: + macvtap_neutron_agent.main() + self.assertTrue(mock_vfd.called) + self.assertTrue(mock_pim.called) + mock_manager.assert_called_with(INTERFACE_MAPPINGS) + mock_loop.assert_called_with(mock_manager_return, 2, 1, + 'Macvtap agent', + 'neutron-macvtap-agent') + self.assertTrue(mock_launch.called) + self.assertTrue(mock_launch_return.wait.called) diff --git a/releasenotes/notes/macvtap-l2-agent-2b551d8ec341196d.yaml b/releasenotes/notes/macvtap-l2-agent-2b551d8ec341196d.yaml new file mode 100644 index 00000000000..b387a4fd078 --- /dev/null +++ b/releasenotes/notes/macvtap-l2-agent-2b551d8ec341196d.yaml @@ -0,0 +1,19 @@ +--- +prelude: > + Adding MacVtap ML2 driver and L2 Agent as new vswitch choice +features: + - Libvirt qemu/kvm instances can now be attached via MacVtap in + bridge mode to a network. VLAN and FLAT attachments are + supported. Other attachmentes than compute are not supported. +issues: + - To ensure any kind of migration works between all compute nodes, + make sure that the same physical_interface_mappings is + configured on each MacVtap compute node. Having different + mappings could cause live migration to fail (if the configured + physical network interface does not exist on the target host), or + even worse, result in an instance placed on the wrong physical + network (if the physical network interface exists on the target + host, but is used by another physical network or not used at all + by OpenStack). Such an instance does not have access to its + configured networks anymore. It then has layer 2 connectivity to + either another OpenStack network, or one of the hosts networks. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 01fdb7cf5a9..a6b2a298b2f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ console_scripts = neutron-l3-agent = neutron.cmd.eventlet.agents.l3:main neutron-linuxbridge-agent = neutron.cmd.eventlet.plugins.linuxbridge_neutron_agent:main neutron-linuxbridge-cleanup = neutron.cmd.linuxbridge_cleanup:main + neutron-macvtap-agent = neutron.cmd.eventlet.plugins.macvtap_neutron_agent:main neutron-metadata-agent = neutron.cmd.eventlet.agents.metadata:main neutron-netns-cleanup = neutron.cmd.netns_cleanup:main neutron-ns-metadata-proxy = neutron.cmd.eventlet.agents.metadata_proxy:main @@ -137,6 +138,7 @@ oslo.config.opts = neutron.metering.agent = neutron.opts:list_metering_agent_opts neutron.ml2 = neutron.opts:list_ml2_conf_opts neutron.ml2.linuxbridge.agent = neutron.opts:list_linux_bridge_opts + neutron.ml2.macvtap.agent = neutron.opts:list_macvtap_opts neutron.ml2.ovs.agent = neutron.opts:list_ovs_opts neutron.ml2.sriov = neutron.opts:list_ml2_conf_sriov_opts neutron.ml2.sriov.agent = neutron.opts:list_sriov_agent_opts