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
This commit is contained in:
@@ -176,6 +176,12 @@ class IPWrapper(SubProcessBase):
|
|||||||
return (IPDevice(name1, namespace=self.namespace),
|
return (IPDevice(name1, namespace=self.namespace),
|
||||||
IPDevice(name2, namespace=namespace2))
|
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):
|
def del_veth(self, name):
|
||||||
"""Delete a virtual interface between two namespaces."""
|
"""Delete a virtual interface between two namespaces."""
|
||||||
self._as_root([], 'link', ('del', name))
|
self._as_root([], 'link', ('del', name))
|
||||||
@@ -455,6 +461,9 @@ class IpLinkCommand(IpDeviceCommandBase):
|
|||||||
def set_address(self, mac_address):
|
def set_address(self, mac_address):
|
||||||
self._as_root([], ('set', self.name, 'address', 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):
|
def set_mtu(self, mtu_size):
|
||||||
self._as_root([], ('set', self.name, 'mtu', mtu_size))
|
self._as_root([], ('set', self.name, 'mtu', mtu_size))
|
||||||
|
|
||||||
|
20
neutron/cmd/eventlet/plugins/macvtap_neutron_agent.py
Normal file
20
neutron/cmd/eventlet/plugins/macvtap_neutron_agent.py
Normal file
@@ -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()
|
@@ -153,6 +153,8 @@ DEVICE_NAME_MAX_LEN = 15
|
|||||||
|
|
||||||
# vhost-user device names start with "vhu"
|
# vhost-user device names start with "vhu"
|
||||||
VHOST_USER_DEVICE_PREFIX = '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
|
# The vswitch side of a veth pair for a nova iptables filter setup
|
||||||
VETH_DEVICE_PREFIX = 'qvo'
|
VETH_DEVICE_PREFIX = 'qvo'
|
||||||
# prefix for SNAT interface in DVR
|
# prefix for SNAT interface in DVR
|
||||||
|
@@ -45,6 +45,7 @@ import neutron.openstack.common.cache.cache
|
|||||||
import neutron.plugins.ml2.config
|
import neutron.plugins.ml2.config
|
||||||
import neutron.plugins.ml2.drivers.agent.config
|
import neutron.plugins.ml2.drivers.agent.config
|
||||||
import neutron.plugins.ml2.drivers.linuxbridge.agent.common.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.agent.common.config
|
||||||
import neutron.plugins.ml2.drivers.mech_sriov.mech_driver.mech_driver
|
import neutron.plugins.ml2.drivers.mech_sriov.mech_driver.mech_driver
|
||||||
import neutron.plugins.ml2.drivers.openvswitch.agent.common.config
|
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():
|
def list_metadata_agent_opts():
|
||||||
return [
|
return [
|
||||||
('DEFAULT',
|
('DEFAULT',
|
||||||
|
35
neutron/plugins/ml2/drivers/macvtap/agent/config.py
Normal file
35
neutron/plugins/ml2/drivers/macvtap/agent/config.py
Normal file
@@ -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 "
|
||||||
|
"<physical_network>:<physical_interface> 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")
|
@@ -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()
|
@@ -53,6 +53,7 @@ PORT_PREFIX = 'port'
|
|||||||
VETH0_PREFIX = 'test-veth0'
|
VETH0_PREFIX = 'test-veth0'
|
||||||
VETH1_PREFIX = 'test-veth1'
|
VETH1_PREFIX = 'test-veth1'
|
||||||
PATCH_PREFIX = 'patch'
|
PATCH_PREFIX = 'patch'
|
||||||
|
MACVTAP_PREFIX = 'macvtap'
|
||||||
|
|
||||||
# port name should be shorter than DEVICE_NAME_MAX_LEN because if this
|
# port name should be shorter than DEVICE_NAME_MAX_LEN because if this
|
||||||
# port is used to provide vlan connection between two linuxbridge
|
# port is used to provide vlan connection between two linuxbridge
|
||||||
@@ -543,6 +544,40 @@ class NamedVethFixture(VethFixture):
|
|||||||
return name
|
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)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class PortFixture(fixtures.Fixture):
|
class PortFixture(fixtures.Fixture):
|
||||||
"""Create a port.
|
"""Create a port.
|
||||||
|
@@ -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())
|
@@ -331,6 +331,15 @@ class TestIpWrapper(base.BaseTestCase):
|
|||||||
run_as_root=True, namespace=None,
|
run_as_root=True, namespace=None,
|
||||||
log_fail_as_error=True)
|
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):
|
def test_del_veth(self):
|
||||||
ip_lib.IPWrapper().del_veth('fpr-1234')
|
ip_lib.IPWrapper().del_veth('fpr-1234')
|
||||||
self.execute.assert_called_once_with([], 'link',
|
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.link_cmd.set_address('aa:bb:cc:dd:ee:ff')
|
||||||
self._assert_sudo([], ('set', 'eth0', '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):
|
def test_set_mtu(self):
|
||||||
self.link_cmd.set_mtu(1500)
|
self.link_cmd.set_mtu(1500)
|
||||||
self._assert_sudo([], ('set', 'eth0', 'mtu', 1500))
|
self._assert_sudo([], ('set', 'eth0', 'mtu', 1500))
|
||||||
|
@@ -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)
|
19
releasenotes/notes/macvtap-l2-agent-2b551d8ec341196d.yaml
Normal file
19
releasenotes/notes/macvtap-l2-agent-2b551d8ec341196d.yaml
Normal file
@@ -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.
|
@@ -49,6 +49,7 @@ console_scripts =
|
|||||||
neutron-l3-agent = neutron.cmd.eventlet.agents.l3:main
|
neutron-l3-agent = neutron.cmd.eventlet.agents.l3:main
|
||||||
neutron-linuxbridge-agent = neutron.cmd.eventlet.plugins.linuxbridge_neutron_agent:main
|
neutron-linuxbridge-agent = neutron.cmd.eventlet.plugins.linuxbridge_neutron_agent:main
|
||||||
neutron-linuxbridge-cleanup = neutron.cmd.linuxbridge_cleanup: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-metadata-agent = neutron.cmd.eventlet.agents.metadata:main
|
||||||
neutron-netns-cleanup = neutron.cmd.netns_cleanup:main
|
neutron-netns-cleanup = neutron.cmd.netns_cleanup:main
|
||||||
neutron-ns-metadata-proxy = neutron.cmd.eventlet.agents.metadata_proxy: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.metering.agent = neutron.opts:list_metering_agent_opts
|
||||||
neutron.ml2 = neutron.opts:list_ml2_conf_opts
|
neutron.ml2 = neutron.opts:list_ml2_conf_opts
|
||||||
neutron.ml2.linuxbridge.agent = neutron.opts:list_linux_bridge_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.ovs.agent = neutron.opts:list_ovs_opts
|
||||||
neutron.ml2.sriov = neutron.opts:list_ml2_conf_sriov_opts
|
neutron.ml2.sriov = neutron.opts:list_ml2_conf_sriov_opts
|
||||||
neutron.ml2.sriov.agent = neutron.opts:list_sriov_agent_opts
|
neutron.ml2.sriov.agent = neutron.opts:list_sriov_agent_opts
|
||||||
|
Reference in New Issue
Block a user