Move nova-lxd to use os-vif.

This patch introduces a new vif driver that implements only plug/unplug.
Those methods merely act as a thin layer over the os-vif functionality,
which requires some adaptation between nova instances/vifs and os-vif
instances/vifs.

The rest of the functionality that was once part of the vif driver has
been moved to functions, as there was no need to make them member methods
(i.e. they never needed `self`). Rather than using the dynamic function
call via string interpolation pattern (which was taken from the libvirt
driver, but is still fragile). Instead, I opted for an explicit map
for config generation.

This patch has 100% test coverage.

The vif library was in dire need of attention, and as a result of the
os-vif transition, it got it.

Change-Id: I175f4df9b58b038b594a40c16076b06f2521bcfc
This commit is contained in:
Paul Hummer 2016-11-15 20:39:59 +00:00
parent ccae5dcec4
commit f434b721cd
5 changed files with 210 additions and 363 deletions

View File

@ -0,0 +1,158 @@
# Copyright 2016 Canonical Ltd
# 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 mock
from nova import context
from nova import exception
from nova.network import model as network_model
from nova import test
from nova.tests.unit import fake_instance
from nova.virt.lxd import vif
GATEWAY = network_model.IP(address='101.168.1.1', type='gateway')
DNS_BRIDGE = network_model.IP(address='8.8.8.8', type=None)
SUBNET = network_model.Subnet(
cidr='101.168.1.0/24', dns=[DNS_BRIDGE], gateway=GATEWAY,
routes=None, dhcp_server='191.168.1.1')
NETWORK = network_model.Network(
id='network-id-xxx-yyy-zzz', bridge='br0', label=None,
subnets=[SUBNET], bridge_interface=None, vlan=99, mtu=1000)
VIF = network_model.VIF(
id='0123456789abcdef', address='ca:fe:de:ad:be:ef',
network=NETWORK, type=network_model.VIF_TYPE_OVS,
devname='tap-012-345-678', ovs_interfaceid='9abc-def-000')
INSTANCE = fake_instance.fake_instance_obj(
context.get_admin_context(), name='test')
class GetVifDevnameTest(test.NoDBTestCase):
"""Tests for get_vif_devname."""
def test_get_vif_devname_devname_exists(self):
an_vif = {
'id': '0123456789abcdef',
'devname': 'oth1',
}
devname = vif.get_vif_devname(an_vif)
self.assertEqual('oth1', devname)
def test_get_vif_devname_devname_nonexistent(self):
an_vif = {
'id': '0123456789abcdef',
}
devname = vif.get_vif_devname(an_vif)
self.assertEqual('nic0123456789a', devname)
class GetConfigTest(test.NoDBTestCase):
"""Tests for get_config."""
def setUp(self):
super(GetConfigTest, self).setUp()
self.CONF_patcher = mock.patch('nova.virt.lxd.vif.conf.CONF')
self.CONF = self.CONF_patcher.start()
self.CONF.firewall_driver = 'nova.virt.firewall.NoopFirewallDriver'
def tearDown(self):
super(GetConfigTest, self).tearDown()
self.CONF_patcher.stop()
def test_get_config_bad_vif_type(self):
"""Unsupported vif types raise an exception."""
an_vif = network_model.VIF(
id='0123456789abcdef', address='ca:fe:de:ad:be:ef',
network=NETWORK, type='invalid',
devname='tap-012-345-678', ovs_interfaceid='9abc-def-000')
self.assertRaises(
exception.NovaException, vif.get_config, an_vif)
def test_get_config_bridge(self):
expected = {'bridge': 'br0', 'mac_address': 'ca:fe:de:ad:be:ef'}
an_vif = network_model.VIF(
id='0123456789abcdef', address='ca:fe:de:ad:be:ef',
network=NETWORK, type='bridge',
devname='tap-012-345-678', ovs_interfaceid='9abc-def-000')
config = vif.get_config(an_vif)
self.assertEqual(expected, config)
def test_get_config_ovs_bridge(self):
expected = {
'bridge': 'br0', 'mac_address': 'ca:fe:de:ad:be:ef'}
an_vif = network_model.VIF(
id='0123456789abcdef', address='ca:fe:de:ad:be:ef',
network=NETWORK, type='ovs',
devname='tap-012-345-678', ovs_interfaceid='9abc-def-000')
config = vif.get_config(an_vif)
self.assertEqual(expected, config)
def test_get_config_ovs_hybrid(self):
self.CONF.firewall_driver = 'AnFirewallDriver'
expected = {
'bridge': 'qbr0123456789a', 'mac_address': 'ca:fe:de:ad:be:ef'}
an_vif = network_model.VIF(
id='0123456789abcdef', address='ca:fe:de:ad:be:ef',
network=NETWORK, type='ovs',
devname='tap-012-345-678', ovs_interfaceid='9abc-def-000')
config = vif.get_config(an_vif)
self.assertEqual(expected, config)
def test_get_config_tap(self):
expected = {'mac_address': 'ca:fe:de:ad:be:ef'}
an_vif = network_model.VIF(
id='0123456789abcdef', address='ca:fe:de:ad:be:ef',
network=NETWORK, type='tap',
devname='tap-012-345-678', ovs_interfaceid='9abc-def-000')
config = vif.get_config(an_vif)
self.assertEqual(expected, config)
class LXDGenericVifDriverTest(test.NoDBTestCase):
"""Tests for LXDGenericVifDriver."""
def setUp(self):
super(LXDGenericVifDriverTest, self).setUp()
self.vif_driver = vif.LXDGenericVifDriver()
@mock.patch('nova.virt.lxd.vif.os_vif')
def test_plug(self, os_vif):
self.vif_driver.plug(INSTANCE, VIF)
self.assertEqual(
'tap-012-345-678', os_vif.plug.call_args[0][0].vif_name)
self.assertEqual(
'instance-00000001', os_vif.plug.call_args[0][1].name)
@mock.patch('nova.virt.lxd.vif.os_vif')
def test_unplug(self, os_vif):
self.vif_driver.unplug(INSTANCE, VIF)
self.assertEqual(
'tap-012-345-678', os_vif.unplug.call_args[0][0].vif_name)
self.assertEqual(
'instance-00000001', os_vif.unplug.call_args[0][1].name)

View File

@ -1,164 +0,0 @@
# Copyright 2015 Canonical Ltd
# 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 copy
import ddt
import mock
from oslo_concurrency import processutils
from nova import exception
from nova.network import model as network_model
from nova import test
from nova.virt.lxd import vif
import stubs
@ddt.ddt
class LXDTestNetworkDriver(test.NoDBTestCase):
vif_data = {
'id': '0123456789abcdef',
'type': network_model.VIF_TYPE_OVS,
'address': '00:11:22:33:44:55',
'network': {
'bridge': 'fakebr'}}
def setUp(self):
super(LXDTestNetworkDriver, self).setUp()
self.vif_driver = vif.LXDGenericDriver()
mn = mock.Mock()
net_patcher = mock.patch.object(vif, 'linux_net', mn)
net_patcher.start()
self.addCleanup(net_patcher.stop)
me = mock.Mock()
net_patcher = mock.patch.object(vif.utils, 'execute', me)
net_patcher.start()
self.addCleanup(net_patcher.stop)
self.mgr = mock.Mock()
self.mgr.attach_mock(mn, 'net')
self.mgr.attach_mock(me, 'ex')
def test_nonetype(self):
instance = stubs.MockInstance()
vif_data = {'type': None}
self.assertRaises(
exception.NovaException,
self.vif_driver.plug,
instance, vif_data)
def test_get_config_ovs(self):
instance = stubs._fake_instance()
vif_data = copy.deepcopy(self.vif_data)
vif_type = self.vif_driver.get_config(instance, vif_data)
self.assertEqual(vif_type, {'bridge': 'qbr0123456789a',
'mac_address': '00:11:22:33:44:55'})
def test_get_config_bridge(self):
instance = stubs._fake_instance()
vif_data = copy.deepcopy(self.vif_data)
vif_type = self.vif_driver.get_config(instance, vif_data)
self.assertEqual(vif_type, {'bridge': 'qbr0123456789a',
'mac_address': '00:11:22:33:44:55'})
@stubs.annotated_data(
('id', {}, [True, True]),
('ovs-id', {'ovs_interfaceid': '123456789abcdef0'}, [True, True]),
('no-bridge', {}, [False, True]),
('no-v2', {}, [True, False]),
('no-bridge-or-v2', {}, [False, False]),
)
def test_plug(self, tag, vif_data, exists):
instance = stubs.MockInstance()
vif_data = copy.deepcopy(self.vif_data)
vif_data.update(vif_data)
self.mgr.net.device_exists.side_effect = exists
self.assertEqual(
None,
self.vif_driver.plug(instance, vif_data))
calls = [
mock.call.net.device_exists('qbr0123456789a'),
mock.call.net.device_exists('qvo0123456789a')
]
if not exists[0]:
calls[1:1] = [
mock.call.ex(
'brctl', 'addbr', 'qbr0123456789a', run_as_root=True),
mock.call.ex(
'brctl', 'setfd', 'qbr0123456789a', 0, run_as_root=True),
mock.call.ex('brctl', 'stp', 'qbr0123456789a', 'off',
run_as_root=True),
mock.call.ex('tee',
'/sys/class/net/qbr0123456789a/'
'bridge/multicast_snooping',
process_input='0', run_as_root=True,
check_exit_code=[0, 1]),
]
if not exists[1]:
calls.extend([
mock.call.net._create_veth_pair('qvb0123456789a',
'qvo0123456789a'),
mock.call.ex('ip', 'link', 'set', 'qbr0123456789a', 'up',
run_as_root=True),
mock.call.ex('brctl', 'addif', 'qbr0123456789a',
'qvb0123456789a', run_as_root=True)])
calls.append(mock.call.net.create_ovs_vif_port(
'fakebr', 'qvo0123456789a', '0123456789abcdef',
'00:11:22:33:44:55', 'fake-uuid'))
self.assertEqual(calls, self.mgr.method_calls)
def test_unplug_fail(self):
instance = stubs.MockInstance()
vif_data = copy.deepcopy(self.vif_data)
self.mgr.net.device_exists.side_effect = (
processutils.ProcessExecutionError)
self.assertEqual(
None,
self.vif_driver.unplug(instance, vif_data))
@stubs.annotated_data(
('id', {}, [True, True]),
('ovs-id', {'ovs_interfaceid': '123456789abcdef0'}, [True, True]),
('no-bridge', {}, [False, True]),
('no-v2', {}, [True, False]),
('no-bridge-or-v2', {}, [False, False]),
)
def test_unplug(self, tag, vif_data, exists):
instance = stubs.MockInstance()
vif = copy.deepcopy(self.vif_data)
self.mgr.net.device_exists.side_effect = exists
self.assertEqual(
None,
self.vif_driver.unplug(instance, vif))
calls = [mock.call.net.device_exists('qbr0123456789a')]
if exists[0]:
calls[1:1] = [
mock.call.ex('brctl', 'delif', 'qbr0123456789a',
'qvb0123456789a', run_as_root=True),
mock.call.ex('ip', 'link', 'set', 'qbr0123456789a',
'down', run_as_root=True),
mock.call.ex('brctl', 'delbr', 'qbr0123456789a',
run_as_root=True),
mock.call.net.delete_ovs_vif_port('fakebr', 'qvo0123456789a')
]
self.assertEqual(calls, self.mgr.method_calls)

View File

@ -222,7 +222,7 @@ class LXDDriver(driver.ComputeDriver):
self.client = None # Initialized by init_host
self.host = NOVA_CONF.host
self.network_api = network.API()
self.vif_driver = lxd_vif.LXDGenericDriver()
self.vif_driver = lxd_vif.LXDGenericVifDriver()
self.firewall_driver = firewall.load_driver(
default='nova.virt.firewall.NoopFirewallDriver')
@ -596,7 +596,7 @@ class LXDDriver(driver.ComputeDriver):
interfaces.append(key)
net_device = 'eth{}'.format(len(interfaces))
network_config = self.vif_driver.get_config(instance, vif)
network_config = lxd_vif.get_config(vif)
if 'bridge' in network_config:
config_update = {
net_device: {
@ -1603,7 +1603,7 @@ class LXDDriver(driver.ComputeDriver):
return
for vifaddr in network_info:
cfg = self.vif_driver.get_config(instance, vifaddr)
cfg = lxd_vif.get_config(vifaddr)
if 'bridge' in cfg:
key = str(cfg['bridge'])
network_devices[key] = {
@ -1619,7 +1619,7 @@ class LXDDriver(driver.ComputeDriver):
'hwaddr': str(cfg['mac_address']),
'type': 'nic'
}
host_device = self.vif_driver.get_vif_devname(vifaddr)
host_device = lxd_vif.get_vif_devname(vifaddr)
if host_device:
network_devices[key]['host_name'] = host_device
# Set network device quotas

View File

@ -11,223 +11,75 @@
# 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_concurrency import processutils
from oslo_log import log as logging
import nova.conf
from nova import conf
from nova import exception
from nova import i18n
from nova.network import linux_net
from nova.network import model as network_model
from nova import utils
from nova.network import os_vif_util
_ = i18n._
_LE = i18n._LE
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
import os_vif
class LXDGenericDriver(object):
def get_vif_devname(vif):
"""Get device name for a given vif."""
if 'devname' in vif:
return vif['devname']
return ("nic" + vif['id'])[:network_model.NIC_NAME_LEN]
def get_vif_devname(self, vif):
if 'devname' in vif:
return vif['devname']
return ("nic" + vif['id'])[:network_model.NIC_NAME_LEN]
def get_vif_devname_with_prefix(self, vif, prefix):
devname = self.get_vif_devname(vif)
return prefix + devname[3:]
def _get_bridge_config(vif):
return {
'bridge': vif['network']['bridge'],
'mac_address': vif['address']}
def get_bridge_name(self, vif):
return vif['network']['bridge']
def get_ovs_interfaceid(self, vif):
return vif.get('ovs_interfaceid') or vif['id']
def _get_ovs_config(vif):
if (conf.CONF.firewall_driver != 'nova.virt.firewall.NoopFirewallDriver' or
vif.is_hybrid_plug_enabled()):
return {
'bridge': ('qbr{}'.format(vif['id']))[:network_model.NIC_NAME_LEN],
'mac_address': vif['address']}
else:
return {
'bridge': vif['network']['bridge'],
'mac_address': vif['address']}
def get_br_name(self, iface_id):
return ("qbr" + iface_id)[:network_model.NIC_NAME_LEN]
def get_veth_pair_names(self, iface_id):
return (("qvb%s" % iface_id)[:network_model.NIC_NAME_LEN],
("qvo%s" % iface_id)[:network_model.NIC_NAME_LEN])
def _get_tap_config(vif):
return {'mac_address': vif['address']}
def get_firewall_required(self, vif):
if CONF.firewall_driver != "nova.virt.firewall.NoopFirewallDriver":
return True
return False
def get_config(self, instance, vif):
vif_type = vif['type']
CONFIG_GENERATORS = {
'bridge': _get_bridge_config,
'ovs': _get_ovs_config,
'tap': _get_tap_config,
}
LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
'vif=%(vif)s',
{'vif_type': vif_type, 'instance': instance,
'vif': vif})
if vif_type is None:
raise exception.NovaException(
_("vif_type parameter must be present "
"for this vif_driver implementation"))
func = getattr(self, 'get_config_%s' % vif_type, None)
if not func:
raise exception.NovaException(
_("Unexpected vif_type=%s") % vif_type)
return func(instance, vif)
def get_config(vif):
"""Get LXD specific config for a vif."""
vif_type = vif['type']
def get_config_bridge(self, instance, vif):
conf = {'bridge': self.get_bridge_name(vif),
'mac_address': vif['address']}
return conf
try:
return CONFIG_GENERATORS[vif_type](vif)
except KeyError:
raise exception.NovaException(
'Unsupported vif type: {}'.format(vif_type))
def get_config_ovs_hybrid(self, instance, vif):
conf = {'bridge': self.get_br_name(vif['id']),
'mac_address': vif['address']}
return conf
class LXDGenericVifDriver(object):
"""Generic VIF driver for LXD networking."""
def get_config_ovs_bridge(self, instance, vif):
conf = {'bridge': self.get_bridge_name(vif),
'mac_address': vif['address']}
return conf
def get_config_ovs(self, instance, vif):
if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
return self.get_config_ovs_hybrid(instance, vif)
else:
return self.get_config_ovs_bridge(instance, vif)
def get_config_tap(self, instance, vif):
conf = {'mac_address': vif['address']}
return conf
def __init__(self):
os_vif.initialize()
def plug(self, instance, vif):
vif_type = vif['type']
instance_info = os_vif_util.nova_to_osvif_instance(instance)
vif_obj = os_vif_util.nova_to_osvif_vif(vif)
LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
'vif=%(vif)s',
{'vif_type': vif_type, 'instance': instance,
'vif': vif})
if vif_type is None:
raise exception.NovaException(
_("vif_type parameter must be present "
"for this vif_driver implementation"))
func = getattr(self, 'plug_%s' % vif_type, None)
if not func:
raise exception.NovaException(
_("Unexpected vif_type=%s") % vif_type)
return func(instance, vif)
def plug_bridge(self, instance, vif):
network = vif['network']
if (not network.get_meta('multi_host', False) and
network.get_meta('should_create_bridge', False)):
if network.get_meta('should_create_vlan', False):
iface = (CONF.vlan_interface or
network.get_meta('bridge_interface'))
LOG.debug('Ensuring vlan %(vlan)s and bridge %(bridge)s',
{'vlan': network.get_meta('vlan'),
'bridge': self.get_bridge_name(vif)},
instance=instance)
linux_net.LinuxBridgeInterfaceDriver.ensure_vlan_bridge(
network.get_meta('vlan'),
self.get_bridge_name(vif), iface)
else:
iface = (CONF.flat_interface or
network.get_meta('bridge_interface'))
LOG.debug("Ensuring bridge %s",
self.get_bridge_name(vif), instance=instance)
linux_net.LinuxBridgeInterfaceDriver.ensure_bridge(
self.get_bridge_name(vif), iface)
def plug_ovs(self, instance, vif):
if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
self.plug_ovs_hybrid(instance, vif)
else:
self.plug_ovs_bridge(instance, vif)
def plug_ovs_bridge(self, instance, vif):
pass
def plug_ovs_hybrid(self, instance, vif):
iface_id = self.get_ovs_interfaceid(vif)
br_name = self.get_br_name(vif['id'])
v1_name, v2_name = self.get_veth_pair_names(vif['id'])
if not linux_net.device_exists(br_name):
utils.execute('brctl', 'addbr', br_name, run_as_root=True)
utils.execute('brctl', 'setfd', br_name, 0, run_as_root=True)
utils.execute('brctl', 'stp', br_name, 'off', run_as_root=True)
utils.execute('tee',
('/sys/class/net/%s/bridge/multicast_snooping' %
br_name),
process_input='0',
run_as_root=True,
check_exit_code=[0, 1])
if not linux_net.device_exists(v2_name):
linux_net._create_veth_pair(v1_name, v2_name)
utils.execute('ip', 'link', 'set', br_name, 'up', run_as_root=True)
utils.execute('brctl', 'addif', br_name, v1_name, run_as_root=True)
linux_net.create_ovs_vif_port(self.get_bridge_name(vif),
v2_name, iface_id,
vif['address'], instance.name)
def plug_tap(self, instance, vif):
pass
os_vif.plug(vif_obj, instance_info)
def unplug(self, instance, vif):
vif_type = vif['type']
instance_info = os_vif_util.nova_to_osvif_instance(instance)
vif_obj = os_vif_util.nova_to_osvif_vif(vif)
LOG.debug('vif_type=%(vif_type)s instance=%(instance)s '
'vif=%(vif)s',
{'vif_type': vif_type, 'instance': instance,
'vif': vif})
if vif_type is None:
raise exception.NovaException(
_("vif_type parameter must be present "
"for this vif_driver implementation"))
func = getattr(self, 'unplug_%s' % vif_type, None)
if not func:
raise exception.NovaException(
_("Unexpected vif_type=%s") % vif_type)
return func(instance, vif)
def unplug_ovs(self, instance, vif):
if self.get_firewall_required(vif) or vif.is_hybrid_plug_enabled():
self.unplug_ovs_hybrid(instance, vif)
else:
self.unplug_ovs_bridge(instance, vif)
def unplug_ovs_hybrid(self, instance, vif):
try:
br_name = self.get_br_name(vif['id'])
v1_name, v2_name = self.get_veth_pair_names(vif['id'])
if linux_net.device_exists(br_name):
utils.execute('brctl', 'delif', br_name, v1_name,
run_as_root=True)
utils.execute('ip', 'link', 'set', br_name, 'down',
run_as_root=True)
utils.execute('brctl', 'delbr', br_name,
run_as_root=True)
linux_net.delete_ovs_vif_port(self.get_bridge_name(vif),
v2_name)
except processutils.ProcessExecutionError:
LOG.exception(_LE("Failed while unplugging vif"),
instance=instance)
def unplug_ovs_bridge(self, instance, vif):
pass
def unplug_bridge(self, instance, vif):
pass
def unplug_tap(self, instance, vif):
pass
os_vif.unplug(vif_obj, instance_info)

View File

@ -4,6 +4,7 @@
pbr>=1.8 # Apache-2.0
os-brick>=1.8.0 # Apache-2.0
os-vif>=1.3.0 # Apache-2.0
oslo.config!=3.18.0,>=3.14.0 # Apache-2.0
oslo.concurrency>=3.8.0 # Apache-2.0
oslo.utils>=3.18.0 # Apache-2.0