XenAPI: OVS agent updates the wrong port with Neutron

With xen hypervisor, it will create two sets of port/interface
for each VM vif: One is named as tapx.y which is qemu-emulated
NIC and is used when no PV drivers installed with the VM; The
other one is named as vifx.0 which is the xen network frontend
NIC and is used when VM has PV drivers installed. But neutron
only handle one port. If the finally used port is not the one
handled by neutron, the guest VM will have no network access.
The solution is to change nova xenapi to add a bridge, which
we call the 'interim bridge', between the VM and the OVS
integration bridge. In this way it will only expose to neutron
with the integration bridge's port which connected to the
interim bridge. So it will fix the issue mentioned above.

Closes-Bug: #1268955

Change-Id: I0cfc0284e1fcd1a6169d31a7ad410716037e5cc2
This commit is contained in:
jianghuaw 2015-11-08 14:17:51 +00:00 committed by Jianghua Wang
parent f9f46ee3af
commit 97745f2bee
11 changed files with 397 additions and 32 deletions

View File

@ -181,6 +181,11 @@ class VirtualInterfacePlugException(NovaException):
msg_fmt = _("Virtual interface plugin failed")
class VirtualInterfaceUnplugException(NovaException):
msg_fmt = _("Virtual interface unplugin failed: "
"%(reason)s")
class GlanceConnectionFailed(NovaException):
msg_fmt = _("Connection to glance host %(server)s failed: "
"%(reason)s")

View File

@ -55,6 +55,11 @@ class ObjectsTestCase(stubs.XenAPITestBaseNoDB):
vdi.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VDI.get_X", "ref")
def test_VIF(self):
vdi = objects.VIF(self.session)
vdi.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VIF.get_X", "ref")
def test_VBD(self):
vbd = objects.VBD(self.session)
vbd.get_X("ref")

View File

@ -100,6 +100,10 @@ class ApplySessionHelpersTestCase(stubs.XenAPITestBaseNoDB):
self.session.VDI.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VDI.get_X", "ref")
def test_apply_session_helpers_add_VIF(self):
self.session.VIF.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VIF.get_X", "ref")
def test_apply_session_helpers_add_VBD(self):
self.session.VBD.get_X("ref")
self.session.call_xenapi.assert_called_once_with("VBD.get_X", "ref")

View File

@ -17,6 +17,7 @@ import mock
from nova import exception
from nova.network import model
from nova import test
from nova.tests.unit.virt.xenapi import stubs
from nova.virt.xenapi import network_utils
from nova.virt.xenapi import vif
@ -67,6 +68,22 @@ class XenVIFDriverTestBase(stubs.XenAPITestBaseNoDB):
self._session = mock.Mock()
self._session.call_xenapi.side_effect = fake_call_xenapi
def mock_patch_object(self, target, attribute, return_val=None,
side_effect=None):
"""Utilility function to mock object's attribute at runtime:
Some methods are dynamic, so standard mocking does not work
and we need to mock them at runtime.
e.g. self._session.VIF.get_record which is dynamically
created via the override function of __getattr__.
"""
patcher = mock.patch.object(target, attribute,
return_value=return_val,
side_effect=side_effect)
mock_one = patcher.start()
self.addCleanup(patcher.stop)
return mock_one
class XenVIFDriverTestCase(XenVIFDriverTestBase):
def setUp(self):
@ -149,41 +166,126 @@ class XenAPIBridgeDriverTestCase(XenVIFDriverTestBase, object):
ret_vif_ref = self.bridge_driver.plug(instance, vif, vm_ref, device)
self.assertEqual('fake_vif_ref', ret_vif_ref)
@mock.patch.object(vif.XenVIFDriver, '_get_vif_ref',
return_value='fake_vif_ref')
def test_unplug(self, mock_get_vif_ref):
instance = {'name': "fake_instance"}
vm_ref = "fake_vm_ref"
self.bridge_driver.unplug(instance, fake_vif, vm_ref)
expected = [mock.call('VIF.destroy', 'fake_vif_ref')]
self.assertEqual(expected, self._session.call_xenapi.call_args_list)
class XenAPIOpenVswitchDriverTestCase(XenVIFDriverTestBase):
def setUp(self):
super(XenAPIOpenVswitchDriverTestCase, self).setUp()
self.ovs_driver = vif.XenAPIOpenVswitchDriver(self._session)
@mock.patch.object(network_utils, 'find_network_with_bridge',
return_value='fake_network_ref')
@mock.patch.object(vif.XenVIFDriver, '_create_vif',
return_value='fake_vif_ref')
@mock.patch.object(vif.XenAPIOpenVswitchDriver,
'create_vif_interim_network')
@mock.patch.object(vif.XenVIFDriver, '_get_vif_ref', return_value=None)
def test_plug(self, mock_get_vif_ref, mock_create_vif,
mock_find_network_with_bridge):
@mock.patch.object(vif.vm_utils, 'lookup', return_value='fake_vm_ref')
def test_plug(self, mock_lookup, mock_get_vif_ref,
mock_create_vif_interim_network,
mock_create_vif):
instance = {'name': "fake_instance_name"}
vm_ref = "fake_vm_ref"
device = 1
ret_vif_ref = self.ovs_driver.plug(instance, fake_vif, vm_ref, device)
ret_vif_ref = self.ovs_driver.plug(
instance, fake_vif, vm_ref=None, device=1)
self.assertTrue(mock_lookup.called)
self.assertTrue(mock_get_vif_ref.called)
self.assertTrue(mock_create_vif_interim_network.called)
self.assertTrue(mock_create_vif.called)
self.assertEqual('fake_vif_ref', ret_vif_ref)
@mock.patch.object(vif.XenVIFDriver, '_get_vif_ref',
return_value='fake_vif_ref')
def test_unplug(self, mock_get_vif_ref):
@mock.patch.object(vif.XenAPIOpenVswitchDriver, '_ovs_del_br')
@mock.patch.object(vif.XenAPIOpenVswitchDriver, '_ovs_del_port')
@mock.patch.object(network_utils, 'find_network_with_name_label',
return_value='fake_network')
@mock.patch.object(vif.XenVIFDriver, 'unplug')
def test_unplug(self, mock_super_unplug,
mock_find_network_with_name_label,
mock_ovs_del_port,
mock_ovs_del_br):
instance = {'name': "fake_instance"}
vm_ref = "fake_vm_ref"
mock_network_get_VIFs = self.mock_patch_object(
self._session.network, 'get_VIFs', return_val=None)
mock_network_get_bridge = self.mock_patch_object(
self._session.network, 'get_bridge', return_val='fake_bridge')
mock_network_destroy = self.mock_patch_object(
self._session.network, 'destroy')
self.ovs_driver.unplug(instance, fake_vif, vm_ref)
expected = [mock.call('VIF.destroy', 'fake_vif_ref')]
self.assertEqual(expected, self._session.call_xenapi.call_args_list)
self.assertTrue(mock_super_unplug.called)
self.assertTrue(mock_find_network_with_name_label.called)
self.assertTrue(mock_network_get_VIFs.called)
self.assertTrue(mock_network_get_bridge.called)
self.assertEqual(mock_ovs_del_port.call_count, 2)
self.assertTrue(mock_network_destroy.called)
@mock.patch.object(vif.XenAPIOpenVswitchDriver, '_ovs_del_br')
@mock.patch.object(vif.XenAPIOpenVswitchDriver, '_ovs_del_port')
@mock.patch.object(network_utils, 'find_network_with_name_label',
return_value='fake_network')
@mock.patch.object(vif.XenVIFDriver, 'unplug')
def test_unplug_exception(self, mock_super_unplug,
mock_find_network_with_name_label,
mock_ovs_del_port,
mock_ovs_del_br):
instance = {'name': "fake_instance"}
vm_ref = "fake_vm_ref"
self.mock_patch_object(
self._session.network, 'get_VIFs', return_val=None)
self.mock_patch_object(
self._session.network, 'get_bridge', return_val='fake_bridge')
self.mock_patch_object(
self._session.network, 'destroy',
side_effect=test.TestingException)
self.assertRaises(exception.VirtualInterfaceUnplugException,
self.ovs_driver.unplug, instance, fake_vif,
vm_ref)
@mock.patch.object(vif.XenAPIOpenVswitchDriver, '_ovs_add_patch_port')
@mock.patch.object(vif.XenAPIOpenVswitchDriver, '_ovs_map_external_ids')
def test_post_start_actions(self, mock_ovs_map_external_ids,
mock_ovs_add_patch_port):
vif_ref = "fake_vif_ref"
instance = {'name': 'fake_instance_name'}
fake_vif_rec = {'uuid': fake_vif['uuid'],
'MAC': fake_vif['address'],
'network': 'fake_network',
'other_config': {
'nicira-iface-id': 'fake-nicira-iface-id'}
}
mock_VIF_get_record = self.mock_patch_object(
self._session.VIF, 'get_record', return_val=fake_vif_rec)
mock_network_get_bridge = self.mock_patch_object(
self._session.network, 'get_bridge',
return_val='fake_bridge_name')
mock_network_get_uuid = self.mock_patch_object(
self._session.network, 'get_uuid',
return_val='fake_network_uuid')
self.ovs_driver.post_start_actions(instance, vif_ref)
self.assertTrue(mock_VIF_get_record.called)
self.assertTrue(mock_network_get_bridge.called)
self.assertTrue(mock_network_get_uuid.called)
self.assertEqual(mock_ovs_add_patch_port.call_count, 2)
self.assertTrue(mock_ovs_map_external_ids.called)
@mock.patch.object(network_utils, 'find_network_with_name_label',
return_value="exist_network_ref")
def test_create_vif_interim_network_exist(self,
mock_find_network_with_name_label):
mock_network_create = self.mock_patch_object(
self._session.network, 'create', return_val='new_network_ref')
network_ref = self.ovs_driver.create_vif_interim_network(fake_vif)
self.assertFalse(mock_network_create.called)
self.assertEqual(network_ref, 'exist_network_ref')
@mock.patch.object(network_utils, 'find_network_with_name_label',
return_value=None)
def test_create_vif_interim_network_new(self,
mock_find_network_with_name_label):
mock_network_create = self.mock_patch_object(
self._session.network, 'create', return_val='new_network_ref')
network_ref = self.ovs_driver.create_vif_interim_network(fake_vif)
self.assertTrue(mock_network_create.called)
self.assertEqual(network_ref, 'new_network_ref')

View File

@ -100,6 +100,12 @@ class VDI(XenAPISessionObject):
super(VDI, self).__init__(session, "VDI")
class VIF(XenAPISessionObject):
"""Virtual Network Interface."""
def __init__(self, session):
super(VIF, self).__init__(session, "VIF")
class SR(XenAPISessionObject):
"""Storage Repository."""
def __init__(self, session):

View File

@ -53,6 +53,7 @@ def apply_session_helpers(session):
session.VM = cli_objects.VM(session)
session.SR = cli_objects.SR(session)
session.VDI = cli_objects.VDI(session)
session.VIF = cli_objects.VIF(session)
session.VBD = cli_objects.VBD(session)
session.PBD = cli_objects.PBD(session)
session.PIF = cli_objects.PIF(session)
@ -69,7 +70,7 @@ class XenAPISession(object):
# changed in development environments.
# MAJOR VERSION: Incompatible changes with the plugins
# MINOR VERSION: Compatible changes, new plguins, etc
PLUGIN_REQUIRED_VERSION = '1.4'
PLUGIN_REQUIRED_VERSION = '1.5'
def __init__(self, url, user, pw):
version_string = version.version_string_with_package()

View File

@ -760,7 +760,7 @@ class SessionBase(object):
return base64.b64encode(zlib.compress("dom_id: %s" % dom_id))
def _plugin_nova_plugin_version_get_version(self, method, args):
return pickle.dumps("1.4")
return pickle.dumps("1.5")
def _plugin_xenhost_query_gc(self, method, args):
return pickle.dumps("False")

View File

@ -23,6 +23,7 @@ import nova.conf
from nova import exception
from nova.i18n import _
from nova.i18n import _LW
from nova.network import model as network_model
from nova.virt.xenapi import network_utils
from nova.virt.xenapi import vm_utils
@ -180,11 +181,18 @@ class XenAPIBridgeDriver(XenVIFDriver):
def unplug(self, instance, vif, vm_ref):
super(XenAPIBridgeDriver, self).unplug(instance, vif, vm_ref)
def post_start_actions(self, instance, vif_ref):
"""no further actions needed for this driver type"""
pass
class XenAPIOpenVswitchDriver(XenVIFDriver):
"""VIF driver for Open vSwitch with XenAPI."""
def plug(self, instance, vif, vm_ref=None, device=None):
"""create an interim network for this vif; and build
the vif_rec which will be used by xapi to create VM vif
"""
if not vm_ref:
vm_ref = vm_utils.lookup(self._session, instance['name'])
@ -198,10 +206,10 @@ class XenAPIOpenVswitchDriver(XenVIFDriver):
if not device:
device = 0
# with OVS model, always plug into an OVS integration bridge
# that is already created
network_ref = network_utils.find_network_with_bridge(
self._session, CONF.xenserver.ovs_integration_bridge)
# Create an interim network for each VIF, so dom0 has a single
# bridge for each device (the emulated and PV ethernet devices
# will both be on this bridge.
network_ref = self.create_vif_interim_network(vif)
vif_rec = {}
vif_rec['device'] = str(device)
vif_rec['network'] = network_ref
@ -216,4 +224,157 @@ class XenAPIOpenVswitchDriver(XenVIFDriver):
return self._create_vif(vif, vif_rec, vm_ref)
def unplug(self, instance, vif, vm_ref):
"""unplug vif:
1. unplug and destroy vif.
2. delete the patch port pair between the integration bridge and
the interim network.
3. destroy the interim network
4. delete the OVS bridge service for the interim network
"""
super(XenAPIOpenVswitchDriver, self).unplug(instance, vif, vm_ref)
net_name = self.get_vif_interim_net_name(vif)
network = network_utils.find_network_with_name_label(
self._session, net_name)
if network is None:
return
vifs = self._session.network.get_VIFs(network)
if vifs:
# only remove the interim network when it's empty.
# for resize/migrate on local host, vifs on both of the
# source and target VM will be connected to the same
# interim network.
return
LOG.debug('destroying patch port pair for vif: vif_id=%(vif_id)s',
{'vif_id': vif['id']})
bridge_name = self._session.network.get_bridge(network)
patch_port1, patch_port2 = self._get_patch_port_pair_names(vif['id'])
try:
# delete the patch port pair
self._ovs_del_port(bridge_name, patch_port1)
self._ovs_del_port(CONF.xenserver.ovs_integration_bridge,
patch_port2)
except Exception as e:
LOG.warn(_LW("Failed to delete patch port pair for vif %(if)s,"
" exception:%(exception)s"),
{'if': vif, 'exception': e}, instance=instance)
raise exception.VirtualInterfaceUnplugException(
reason=_("Failed to delete patch port pair"))
LOG.debug('destroying network: network=%(network)s,'
'bridge=%(br)s',
{'network': network, 'br': bridge_name})
try:
self._session.network.destroy(network)
# delete bridge if it still exists.
# As there is patch port existing on this bridge when destroying
# the VM vif (which happens when shutdown the VM), the bridge
# won't be destroyed automatically by XAPI. So let's destroy it
# at here.
self._ovs_del_br(bridge_name)
except Exception as e:
LOG.warn(_LW("Failed to delete bridge for vif %(if)s, "
"exception:%(exception)s"),
{'if': vif, 'exception': e}, instance=instance)
raise exception.VirtualInterfaceUnplugException(
reason=_("Failed to delete bridge"))
def post_start_actions(self, instance, vif_ref):
"""Do needed actions post vif start:
plug the interim ovs bridge to the integration bridge;
set external_ids to the int-br port which will service
for this vif.
"""
vif_rec = self._session.VIF.get_record(vif_ref)
network_ref = vif_rec['network']
bridge_name = self._session.network.get_bridge(network_ref)
network_uuid = self._session.network.get_uuid(network_ref)
iface_id = vif_rec['other_config']['nicira-iface-id']
patch_port1, patch_port2 = self._get_patch_port_pair_names(iface_id)
LOG.debug('plug_ovs_bridge: port1=%(port1)s, port2=%(port2)s,'
'network_uuid=%(uuid)s, bridge_name=%(bridge_name)s',
{'port1': patch_port1, 'port2': patch_port2,
'uuid': network_uuid, 'bridge_name': bridge_name})
if bridge_name is None:
raise exception.VirtualInterfacePlugException(
_("Failed to find bridge for vif"))
self._ovs_add_patch_port(bridge_name, patch_port1, patch_port2)
self._ovs_add_patch_port(CONF.xenserver.ovs_integration_bridge,
patch_port2, patch_port1)
self._ovs_map_external_ids(patch_port2, vif_rec)
def get_vif_interim_net_name(self, vif):
return ("net-" + vif['id'])[:network_model.NIC_NAME_LEN]
def create_vif_interim_network(self, vif):
net_name = self.get_vif_interim_net_name(vif)
network_rec = {'name_label': net_name,
'name_description': "interim network for vif",
'other_config': {}}
network_ref = network_utils.find_network_with_name_label(
self._session, net_name)
if network_ref:
# already exist, just return
# in some scenarios: e..g resize/migrate, it won't create new
# interim network.
return network_ref
try:
network_ref = self._session.network.create(network_rec)
except Exception as e:
LOG.warn(_LW("Failed to create interim network for vif %(if)s, "
"exception:%(exception)s"),
{'if': vif, 'exception': e})
raise exception.VirtualInterfacePlugException(
_("Failed to create the interim network for vif"))
return network_ref
def _get_patch_port_pair_names(self, iface_id):
return (("pp1-%s" % iface_id)[:network_model.NIC_NAME_LEN],
("pp2-%s" % iface_id)[:network_model.NIC_NAME_LEN])
def _ovs_add_patch_port(self, bridge_name, port_name, peer_port_name):
cmd = 'ovs_add_patch_port'
args = {'bridge_name': bridge_name,
'port_name': port_name,
'peer_port_name': peer_port_name
}
self._exec_dom0_cmd(cmd, args)
def _ovs_del_port(self, bridge_name, port_name):
cmd = 'ovs_del_port'
args = {'bridge_name': bridge_name,
'port_name': port_name
}
self._exec_dom0_cmd(cmd, args)
def _ovs_del_br(self, bridge_name):
cmd = 'ovs_del_br'
args = {'bridge_name': bridge_name}
self._exec_dom0_cmd(cmd, args)
def _ovs_set_if_external_id(self, interface, extneral_id, value):
cmd = 'ovs_set_if_external_id'
args = {'interface': interface,
'extneral_id': extneral_id,
'value': value}
self._exec_dom0_cmd(cmd, args)
def _ovs_map_external_ids(self, interface, vif_rec):
'''set external ids on the integration bridge vif
'''
mac = vif_rec['MAC']
iface_id = vif_rec['other_config']['nicira-iface-id']
vif_uuid = vif_rec['uuid']
status = 'active'
self._ovs_set_if_external_id(interface, 'attached-mac', mac)
self._ovs_set_if_external_id(interface, 'iface-id', iface_id)
self._ovs_set_if_external_id(interface, 'xs-vif-uuid', vif_uuid)
self._ovs_set_if_external_id(interface, 'iface-status', status)
def _exec_dom0_cmd(self, cmd, cmd_args):
args = {'cmd': cmd,
'args': cmd_args
}
self._session.call_plugin_serialized('xenhost', 'network_config', args)

View File

@ -333,6 +333,18 @@ class VMOps(object):
if bad_volumes_callback and bad_devices:
bad_volumes_callback(bad_devices)
# Do some operations which have to be done after start:
# e.g. The vif's interim bridge won't be created until VM starts.
# So the operations on the interim bridge have be done after
# start.
self._post_start_actions(instance)
def _post_start_actions(self, instance):
vm_ref = vm_utils.lookup(self._session, instance['name'])
vif_refs = self._session.call_xenapi("VM.get_VIFs", vm_ref)
for vif_ref in vif_refs:
self.vif_driver.post_start_actions(instance, vif_ref)
def _get_vdis_for_instance(self, context, instance, name_label,
image_meta, image_type, block_device_info):
"""Create or connect to all virtual disks for this instance."""
@ -1920,13 +1932,20 @@ class VMOps(object):
"""Creates vifs for an instance."""
LOG.debug("Creating vifs", instance=instance)
vif_refs = []
# this function raises if vm_ref is not a vm_opaque_ref
self._session.call_xenapi("VM.get_domid", vm_ref)
for device, vif in enumerate(network_info):
LOG.debug('Create VIF %s', vif, instance=instance)
self.vif_driver.plug(instance, vif, vm_ref=vm_ref, device=device)
vif_ref = self.vif_driver.plug(instance, vif,
vm_ref=vm_ref, device=device)
vif_refs.append(vif_ref)
LOG.debug('Created the vif_refs: %(vifs)s for VM name: %(name)s',
{'vifs': vif_refs, 'name': instance['name']},
instance=instance)
def plug_vifs(self, instance, network_info):
"""Set up VIF networking on the host."""

View File

@ -30,7 +30,8 @@ import utils
# 1.2 - Added support for pci passthrough devices
# 1.3 - Add vhd2 functions for doing glance operations by url
# 1.4 - Add support of Glance v2 api
PLUGIN_VERSION = "1.4"
# 1.5 - Added function for network configuration on ovs bridge
PLUGIN_VERSION = "1.5"
def get_version(session):

View File

@ -217,6 +217,65 @@ def iptables_config(session, args):
raise pluginlib.PluginError(_("Invalid iptables command"))
def _ovs_add_patch_port(args):
bridge_name = pluginlib.exists(args, 'bridge_name')
port_name = pluginlib.exists(args, 'port_name')
peer_port_name = pluginlib.exists(args, 'peer_port_name')
cmd_args = ['ovs-vsctl', '--', '--if-exists', 'del-port',
port_name, '--', 'add-port', bridge_name, port_name,
'--', 'set', 'interface', port_name,
'type=patch', 'options:peer=%s' % peer_port_name]
return _run_command(cmd_args)
def _ovs_del_port(args):
bridge_name = pluginlib.exists(args, 'bridge_name')
port_name = pluginlib.exists(args, 'port_name')
cmd_args = ['ovs-vsctl', '--', '--if-exists', 'del-port',
bridge_name, port_name]
return _run_command(cmd_args)
def _ovs_del_br(args):
bridge_name = pluginlib.exists(args, 'bridge_name')
cmd_args = ['ovs-vsctl', '--', '--if-exists',
'del-br', bridge_name]
return _run_command(cmd_args)
def _ovs_set_if_external_id(args):
interface = pluginlib.exists(args, 'interface')
extneral_id = pluginlib.exists(args, 'extneral_id')
value = pluginlib.exists(args, 'value')
cmd_args = ['ovs-vsctl', 'set', 'Interface', interface,
'external-ids:%s=%s' % (extneral_id, value)]
return _run_command(cmd_args)
ALLOWED_NETWORK_CMDS = {
# allowed cmds to config OVS bridge
'ovs_add_patch_port': _ovs_add_patch_port,
'ovs_del_port': _ovs_del_port,
'ovs_del_br': _ovs_del_br,
'ovs_set_if_external_id': _ovs_set_if_external_id
}
def network_config(session, args):
"""network config functions"""
cmd = pluginlib.exists(args, 'cmd')
if not isinstance(cmd, basestring):
msg = _("invalid command '%s'") % str(cmd)
raise pluginlib.PluginError(msg)
return
if cmd not in ALLOWED_NETWORK_CMDS:
msg = _("Dom0 execution of '%s' is not permitted") % cmd
raise pluginlib.PluginError(msg)
return
cmd_args = pluginlib.exists(args, 'args')
return ALLOWED_NETWORK_CMDS[cmd](cmd_args)
def _power_action(action, arg_dict):
# Host must be disabled first
host_uuid = arg_dict['host_uuid']
@ -453,10 +512,12 @@ def get_pci_type(session, pci_device):
if __name__ == "__main__":
# Support both serialized and non-serialized plugin approaches
_, methodname = xmlrpclib.loads(sys.argv[1])
if methodname in ['query_gc', 'get_pci_device_details', 'get_pci_type']:
if methodname in ['query_gc', 'get_pci_device_details', 'get_pci_type',
'network_config']:
utils.register_plugin_calls(query_gc,
get_pci_device_details,
get_pci_type)
get_pci_type,
network_config)
XenAPIPlugin.dispatch(
{"host_data": host_data,