Detect and process live-migration in Cisco plugin
With Cisco/Nexus plugin, migration is not fully supported. Logic to detect port binding change needs to be added in update_port(), and provisioning of nexus switch(es) should be done accordingly added test code for update_port() in the model layer and the db layer Closes-Bug: #1229217 Change-Id: I2bd76030711c9d15462e91da9e4c0836a424834f
This commit is contained in:
parent
b94b4b087b
commit
684c9b0b10
@ -336,6 +336,47 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
|
||||
"""For this model this method will be delegated to vswitch plugin."""
|
||||
pass
|
||||
|
||||
def _check_nexus_net_create_needed(self, new_port, old_port):
|
||||
"""Check if nexus plugin should be invoked for net_create.
|
||||
|
||||
In the following cases, the plugin should be invoked:
|
||||
-- a port is attached to a VM instance. The old host id is None
|
||||
-- VM migration. The old host id has a valid value
|
||||
|
||||
When the plugin needs to be invoked, return the old_host_id,
|
||||
and a list of calling arguments.
|
||||
Otherwise, return '' for old host id and an empty list
|
||||
"""
|
||||
old_device_id = old_port['device_id']
|
||||
new_device_id = new_port.get('device_id')
|
||||
new_host_id = self._get_port_host_id_from_bindings(new_port)
|
||||
tenant_id = old_port['tenant_id']
|
||||
net_id = old_port['network_id']
|
||||
old_host_id = self._get_port_host_id_from_bindings(old_port)
|
||||
|
||||
LOG.debug(_("tenant_id: %(tid)s, net_id: %(nid)s, "
|
||||
"old_device_id: %(odi)s, new_device_id: %(ndi)s, "
|
||||
"old_host_id: %(ohi)s, new_host_id: %(nhi)s, "
|
||||
"old_device_owner: %(odo)s, new_device_owner: %(ndo)s"),
|
||||
{'tid': tenant_id, 'nid': net_id,
|
||||
'odi': old_device_id, 'ndi': new_device_id,
|
||||
'ohi': old_host_id, 'nhi': new_host_id,
|
||||
'odo': old_port.get('device_owner'),
|
||||
'ndo': new_port.get('device_owner')})
|
||||
|
||||
# A port is attached to an instance
|
||||
if (new_device_id and not old_device_id and new_host_id and
|
||||
self._check_valid_port_device_owner(new_port)):
|
||||
return '', [tenant_id, net_id, new_device_id, new_host_id]
|
||||
|
||||
# An instance is being migrated
|
||||
if (old_device_id and old_host_id and new_host_id != old_host_id and
|
||||
self._check_valid_port_device_owner(old_port)):
|
||||
return old_host_id, [tenant_id, net_id, old_device_id, new_host_id]
|
||||
|
||||
# no need to invoke the plugin
|
||||
return '', []
|
||||
|
||||
def update_port(self, context, id, port):
|
||||
"""Update port.
|
||||
|
||||
@ -344,24 +385,27 @@ class VirtualPhysicalSwitchModelV2(neutron_plugin_base_v2.NeutronPluginBaseV2):
|
||||
"""
|
||||
LOG.debug(_("update_port() called"))
|
||||
old_port = self.get_port(context, id)
|
||||
old_device = old_port['device_id']
|
||||
args = [context, id, port]
|
||||
ovs_output = self._invoke_plugin_per_device(const.VSWITCH_PLUGIN,
|
||||
self._func_name(),
|
||||
args)
|
||||
net_id = old_port['network_id']
|
||||
instance_id = ''
|
||||
if 'device_id' in port['port']:
|
||||
instance_id = port['port']['device_id']
|
||||
|
||||
# Check if there's a new device_id
|
||||
try:
|
||||
host_id = self._get_port_host_id_from_bindings(port['port'])
|
||||
if (instance_id and not old_device and host_id and
|
||||
self._check_valid_port_device_owner(port['port'])):
|
||||
tenant_id = old_port['tenant_id']
|
||||
self._invoke_nexus_for_net_create(
|
||||
context, tenant_id, net_id, instance_id, host_id)
|
||||
# Check if the nexus plugin needs to be invoked
|
||||
old_host_id, create_args = self._check_nexus_net_create_needed(
|
||||
port['port'], old_port)
|
||||
|
||||
# In the case of migration, invoke it to remove
|
||||
# the previous port binding
|
||||
if old_host_id:
|
||||
vlan_id = self._get_segmentation_id(old_port['network_id'])
|
||||
delete_args = [old_port['device_id'], vlan_id]
|
||||
self._invoke_plugin_per_device(const.NEXUS_PLUGIN,
|
||||
"delete_port",
|
||||
delete_args)
|
||||
|
||||
# Invoke the Nexus plugin to create a net and/or new port binding
|
||||
if create_args:
|
||||
self._invoke_nexus_for_net_create(context, *create_args)
|
||||
|
||||
return ovs_output[0]
|
||||
except Exception:
|
||||
|
@ -21,6 +21,7 @@ import mock
|
||||
import webob.exc as wexc
|
||||
|
||||
from neutron.api import extensions
|
||||
from neutron.api.v2 import attributes
|
||||
from neutron.api.v2 import base
|
||||
from neutron.common import exceptions as q_exc
|
||||
from neutron import context
|
||||
@ -50,12 +51,16 @@ BRIDGE_NAME = 'br-eth1'
|
||||
VLAN_START = 1000
|
||||
VLAN_END = 1100
|
||||
COMP_HOST_NAME = 'testhost'
|
||||
COMP_HOST_NAME_2 = 'testhost_2'
|
||||
NEXUS_IP_ADDR = '1.1.1.1'
|
||||
NEXUS_DEV_ID = 'NEXUS_SWITCH'
|
||||
NEXUS_USERNAME = 'admin'
|
||||
NEXUS_PASSWORD = 'mySecretPassword'
|
||||
NEXUS_SSH_PORT = 22
|
||||
NEXUS_INTERFACE = '1/1'
|
||||
NEXUS_INTERFACE_2 = '1/2'
|
||||
NEXUS_PORT_1 = 'ethernet:1/1'
|
||||
NEXUS_PORT_2 = 'ethernet:1/2'
|
||||
NETWORK_NAME = 'test_network'
|
||||
CIDR_1 = '10.0.0.0/24'
|
||||
CIDR_2 = '10.0.1.0/24'
|
||||
@ -104,6 +109,7 @@ class CiscoNetworkPluginV2TestCase(test_db_plugin.NeutronDbPluginV2TestCase):
|
||||
(NEXUS_DEV_ID, NEXUS_IP_ADDR, 'password'): NEXUS_PASSWORD,
|
||||
(NEXUS_DEV_ID, NEXUS_IP_ADDR, 'ssh_port'): NEXUS_SSH_PORT,
|
||||
(NEXUS_DEV_ID, NEXUS_IP_ADDR, COMP_HOST_NAME): NEXUS_INTERFACE,
|
||||
(NEXUS_DEV_ID, NEXUS_IP_ADDR, COMP_HOST_NAME_2): NEXUS_INTERFACE_2,
|
||||
}
|
||||
nexus_patch = mock.patch.dict(cisco_config.device_dictionary,
|
||||
nexus_config)
|
||||
@ -546,6 +552,198 @@ class TestCiscoPortsV2(CiscoNetworkPluginV2TestCase,
|
||||
NEXUS_IP_ADDR)
|
||||
self.assertEqual(start_rows, end_rows)
|
||||
|
||||
def test_model_update_port_attach(self):
|
||||
"""Test the model for update_port in attaching to an instance.
|
||||
|
||||
Mock the routines that call into the plugin code, and make sure they
|
||||
are called with correct arguments.
|
||||
|
||||
"""
|
||||
with contextlib.nested(
|
||||
self.port(),
|
||||
mock.patch.object(virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
|
||||
'_invoke_plugin_per_device'),
|
||||
mock.patch.object(virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
|
||||
'_invoke_nexus_for_net_create')
|
||||
) as (port, invoke_plugin_per_device, invoke_nexus_for_net_create):
|
||||
data = {'port': {portbindings.HOST_ID: COMP_HOST_NAME,
|
||||
'device_id': DEVICE_ID_1,
|
||||
'device_owner': DEVICE_OWNER}}
|
||||
|
||||
req = self.new_update_request('ports', data, port['port']['id'])
|
||||
# Note, due to mocking out the two model routines, response won't
|
||||
# contain any useful data
|
||||
req.get_response(self.api)
|
||||
|
||||
# Note that call_args_list is used instead of
|
||||
# assert_called_once_with which requires exact match of arguments.
|
||||
# This is because the mocked routines contain variable number of
|
||||
# arguments and/or dynamic objects.
|
||||
self.assertEqual(invoke_plugin_per_device.call_count, 1)
|
||||
self.assertEqual(
|
||||
invoke_plugin_per_device.call_args_list[0][0][0:2],
|
||||
(const.VSWITCH_PLUGIN, 'update_port'))
|
||||
self.assertEqual(invoke_nexus_for_net_create.call_count, 1)
|
||||
self.assertEqual(
|
||||
invoke_nexus_for_net_create.call_args_list[0][0][1:],
|
||||
(port['port']['tenant_id'], port['port']['network_id'],
|
||||
data['port']['device_id'],
|
||||
data['port'][portbindings.HOST_ID],))
|
||||
|
||||
def test_model_update_port_migrate(self):
|
||||
"""Test the model for update_port in migrating an instance.
|
||||
|
||||
Mock the routines that call into the plugin code, and make sure they
|
||||
are called with correct arguments.
|
||||
|
||||
"""
|
||||
arg_list = (portbindings.HOST_ID,)
|
||||
data = {portbindings.HOST_ID: COMP_HOST_NAME,
|
||||
'device_id': DEVICE_ID_1,
|
||||
'device_owner': DEVICE_OWNER}
|
||||
|
||||
with contextlib.nested(
|
||||
self.port(arg_list=arg_list, **data),
|
||||
mock.patch.object(virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
|
||||
'_invoke_plugin_per_device'),
|
||||
mock.patch.object(virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
|
||||
'_invoke_nexus_for_net_create')
|
||||
) as (port, invoke_plugin_per_device, invoke_nexus_for_net_create):
|
||||
data = {'port': {portbindings.HOST_ID: COMP_HOST_NAME_2}}
|
||||
req = self.new_update_request('ports', data, port['port']['id'])
|
||||
# Note, due to mocking out the two model routines, response won't
|
||||
# contain any useful data
|
||||
req.get_response(self.api)
|
||||
|
||||
# Note that call_args_list is used instead of
|
||||
# assert_called_once_with which requires exact match of arguments.
|
||||
# This is because the mocked routines contain variable number of
|
||||
# arguments and/or dynamic objects.
|
||||
self.assertEqual(invoke_plugin_per_device.call_count, 2)
|
||||
self.assertEqual(
|
||||
invoke_plugin_per_device.call_args_list[0][0][0:2],
|
||||
(const.VSWITCH_PLUGIN, 'update_port'))
|
||||
self.assertEqual(
|
||||
invoke_plugin_per_device.call_args_list[1][0][0:2],
|
||||
(const.NEXUS_PLUGIN, 'delete_port'))
|
||||
self.assertEqual(invoke_nexus_for_net_create.call_count, 1)
|
||||
self.assertEqual(
|
||||
invoke_nexus_for_net_create.call_args_list[0][0][1:],
|
||||
(port['port']['tenant_id'], port['port']['network_id'],
|
||||
port['port']['device_id'],
|
||||
data['port'][portbindings.HOST_ID],))
|
||||
|
||||
def test_model_update_port_net_create_not_needed(self):
|
||||
"""Test the model for update_port when no action is needed.
|
||||
|
||||
Mock the routines that call into the plugin code, and make sure that
|
||||
VSWITCH plugin is called with correct arguments, while NEXUS plugin is
|
||||
not called at all.
|
||||
|
||||
"""
|
||||
arg_list = (portbindings.HOST_ID,)
|
||||
data = {portbindings.HOST_ID: COMP_HOST_NAME,
|
||||
'device_id': DEVICE_ID_1,
|
||||
'device_owner': DEVICE_OWNER}
|
||||
|
||||
with contextlib.nested(
|
||||
self.port(arg_list=arg_list, **data),
|
||||
mock.patch.object(virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
|
||||
'_invoke_plugin_per_device'),
|
||||
mock.patch.object(virt_phy_sw_v2.VirtualPhysicalSwitchModelV2,
|
||||
'_invoke_nexus_for_net_create')
|
||||
) as (port, invoke_plugin_per_device, invoke_nexus_for_net_create):
|
||||
data = {'port': {portbindings.HOST_ID: COMP_HOST_NAME,
|
||||
'device_id': DEVICE_ID_1,
|
||||
'device_owner': DEVICE_OWNER}}
|
||||
req = self.new_update_request('ports', data, port['port']['id'])
|
||||
# Note, due to mocking out the two model routines, response won't
|
||||
# contain any useful data
|
||||
req.get_response(self.api)
|
||||
|
||||
# Note that call_args_list is used instead of
|
||||
# assert_called_once_with which requires exact match of arguments.
|
||||
# This is because the mocked routines contain variable number of
|
||||
# arguments and/or dynamic objects.
|
||||
self.assertEqual(invoke_plugin_per_device.call_count, 1)
|
||||
self.assertEqual(
|
||||
invoke_plugin_per_device.call_args_list[0][0][0:2],
|
||||
(const.VSWITCH_PLUGIN, 'update_port'))
|
||||
self.assertFalse(invoke_nexus_for_net_create.called)
|
||||
|
||||
def verify_portbinding(self, host_id1, host_id2,
|
||||
vlan, device_id, binding_port):
|
||||
"""Verify a port binding entry in the DB is correct."""
|
||||
self.assertEqual(host_id1, host_id2)
|
||||
pb = nexus_db_v2.get_nexusvm_bindings(vlan, device_id)
|
||||
self.assertEqual(len(pb), 1)
|
||||
self.assertEqual(pb[0].port_id, binding_port)
|
||||
self.assertEqual(pb[0].switch_ip, NEXUS_IP_ADDR)
|
||||
|
||||
def test_db_update_port_attach(self):
|
||||
"""Test DB for update_port in attaching to an instance.
|
||||
|
||||
Query DB for the port binding entry corresponding to the search key
|
||||
(vlan, device_id), and make sure that it's bound to correct switch port
|
||||
|
||||
"""
|
||||
with self.port() as port:
|
||||
data = {'port': {portbindings.HOST_ID: COMP_HOST_NAME,
|
||||
'device_id': DEVICE_ID_1,
|
||||
'device_owner': DEVICE_OWNER}}
|
||||
|
||||
req = self.new_update_request('ports', data, port['port']['id'])
|
||||
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
ctx = context.get_admin_context()
|
||||
net = self._show('networks', res['port']['network_id'],
|
||||
neutron_context=ctx)['network']
|
||||
self.assertTrue(attributes.is_attr_set(
|
||||
net.get(provider.SEGMENTATION_ID)))
|
||||
vlan = net[provider.SEGMENTATION_ID]
|
||||
self.assertEqual(vlan, VLAN_START)
|
||||
self.verify_portbinding(res['port'][portbindings.HOST_ID],
|
||||
data['port'][portbindings.HOST_ID],
|
||||
vlan,
|
||||
data['port']['device_id'],
|
||||
NEXUS_PORT_1)
|
||||
|
||||
def test_db_update_port_migrate(self):
|
||||
"""Test DB for update_port in migrating an instance.
|
||||
|
||||
Query DB for the port binding entry corresponding to the search key
|
||||
(vlan, device_id), and make sure that it's bound to correct switch port
|
||||
before and after the migration.
|
||||
|
||||
"""
|
||||
arg_list = (portbindings.HOST_ID,)
|
||||
data = {portbindings.HOST_ID: COMP_HOST_NAME,
|
||||
'device_id': DEVICE_ID_1,
|
||||
'device_owner': DEVICE_OWNER}
|
||||
|
||||
with self.port(arg_list=arg_list, **data) as port:
|
||||
ctx = context.get_admin_context()
|
||||
net = self._show('networks', port['port']['network_id'],
|
||||
neutron_context=ctx)['network']
|
||||
self.assertTrue(attributes.is_attr_set(
|
||||
net.get(provider.SEGMENTATION_ID)))
|
||||
vlan = net[provider.SEGMENTATION_ID]
|
||||
self.assertEqual(vlan, VLAN_START)
|
||||
self.verify_portbinding(port['port'][portbindings.HOST_ID],
|
||||
data[portbindings.HOST_ID],
|
||||
vlan,
|
||||
data['device_id'],
|
||||
NEXUS_PORT_1)
|
||||
|
||||
new_data = {'port': {portbindings.HOST_ID: COMP_HOST_NAME_2}}
|
||||
req = self.new_update_request('ports',
|
||||
new_data, port['port']['id'])
|
||||
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
self.verify_portbinding(res['port'][portbindings.HOST_ID],
|
||||
new_data['port'][portbindings.HOST_ID],
|
||||
vlan,
|
||||
data['device_id'],
|
||||
NEXUS_PORT_2)
|
||||
|
||||
|
||||
class TestCiscoNetworksV2(CiscoNetworkPluginV2TestCase,
|
||||
test_db_plugin.TestNetworksV2):
|
||||
|
Loading…
x
Reference in New Issue
Block a user