From 2bcc178be15dce850162be76a870e443eee84c47 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Wed, 23 Jan 2019 16:21:33 +0000 Subject: [PATCH] Change provider network segmentation ID in OVS agent Added the ability to change the segmentation ID of a network with ports bound to OVS agent. The rules, both in the integration bridge and the physical bridge, to convert the internal VLAN tag and the external segmentation ID (external VLAN tag) are deleted and created again with the new value. The traffic from the tenant networks will be tagged then with the new segmentation ID. Added get network details agent RPC call to retrieve the information of the updated network. Partial-Bug: #1806052 Change-Id: I69f6f3ef717c3ed40218099b1f389afd3d39bd62 --- neutron/agent/rpc.py | 6 ++ .../openvswitch/agent/ovs_neutron_agent.py | 35 +++++++++++ .../drivers/openvswitch/agent/vlanmanager.py | 6 ++ .../mech_driver/mech_openvswitch.py | 5 ++ neutron/plugins/ml2/rpc.py | 14 ++++- neutron/tests/unit/agent/test_rpc.py | 3 + .../agent/test_ovs_neutron_agent.py | 63 ++++++++++++++++++- .../openvswitch/agent/test_vlanmanager.py | 7 +++ neutron/tests/unit/plugins/ml2/test_rpc.py | 11 ++++ ...-segmentation-id-ovs-a201e0ac1c4d4fb6.yaml | 8 +++ 10 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/change-segmentation-id-ovs-a201e0ac1c4d4fb6.yaml diff --git a/neutron/agent/rpc.py b/neutron/agent/rpc.py index 6a3effd1d97..feeac8f5d49 100644 --- a/neutron/agent/rpc.py +++ b/neutron/agent/rpc.py @@ -112,6 +112,7 @@ class PluginApi(object): 1.4 - tunnel_sync rpc signature upgrade to obtain 'host' 1.5 - Support update_device_list and get_devices_details_list_and_failed_devices + 1.6 - Support get_network_details ''' def __init__(self, topic): @@ -142,6 +143,11 @@ class PluginApi(object): 'get_devices_details_list_and_failed_devices', devices=devices, agent_id=agent_id, host=host) + def get_network_details(self, context, network, agent_id, host=None): + cctxt = self.client.prepare(version='1.6') + return cctxt.call(context, 'get_network_details', network=network, + agent_id=agent_id, host=host) + def update_device_down(self, context, device, agent_id, host=None): cctxt = self.client.prepare() return cctxt.call(context, 'update_device_down', device=device, diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py index affca3d0ef8..fa8a4c0dbc2 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py @@ -25,6 +25,7 @@ import netaddr from neutron_lib.agent import constants as agent_consts from neutron_lib.agent import topics from neutron_lib.api.definitions import portbindings +from neutron_lib.api.definitions import provider_net from neutron_lib.callbacks import events as callback_events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources as callback_resources @@ -400,6 +401,37 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin, n_const.TYPE_GRE: {}, n_const.TYPE_VXLAN: {}} + def _update_network_segmentation_id(self, network): + if network[provider_net.NETWORK_TYPE] != n_const.TYPE_VLAN: + return + + try: + lvm = self.vlan_manager.get(network['id']) + except vlanmanager.MappingNotFound: + return + + segmentation_id_old = lvm.segmentation_id + if segmentation_id_old == network[provider_net.SEGMENTATION_ID]: + return + self.vlan_manager.update_segmentation_id( + network['id'], network[provider_net.SEGMENTATION_ID]) + + lvid = lvm.vlan + physical_network = network[provider_net.PHYSICAL_NETWORK] + phys_br = self.phys_brs[physical_network] + phys_port = self.phys_ofports[physical_network] + int_port = self.int_ofports[physical_network] + phys_br.reclaim_local_vlan(port=phys_port, lvid=lvid) + phys_br.provision_local_vlan( + port=phys_port, lvid=lvid, + segmentation_id=network[provider_net.SEGMENTATION_ID], + distributed=self.enable_distributed_routing) + self.int_br.reclaim_local_vlan(port=int_port, + segmentation_id=segmentation_id_old) + self.int_br.provision_local_vlan( + port=int_port, lvid=lvid, + segmentation_id=network[provider_net.SEGMENTATION_ID]) + def setup_rpc(self): self.plugin_rpc = OVSPluginApi(topics.PLUGIN) # allow us to receive port_update/delete callbacks from the cache @@ -449,6 +481,9 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin, def network_update(self, context, **kwargs): network_id = kwargs['network']['id'] + network = self.plugin_rpc.get_network_details( + self.context, network_id, self.agent_id, self.conf.host) + self._update_network_segmentation_id(network) for port_id in self.network_ports[network_id]: # notifications could arrive out of order, if the port is deleted # we don't want to update it anymore diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/vlanmanager.py b/neutron/plugins/ml2/drivers/openvswitch/agent/vlanmanager.py index 5e98dd049ec..15317670755 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/vlanmanager.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/vlanmanager.py @@ -108,3 +108,9 @@ class LocalVlanManager(object): return self.mapping.pop(net_id) except KeyError: raise MappingNotFound(net_id=net_id) + + def update_segmentation_id(self, net_id, segmentation_id): + try: + self.mapping[net_id].segmentation_id = segmentation_id + except KeyError: + raise MappingNotFound(net_id=net_id) diff --git a/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py b/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py index 214259d5198..fec8ae1e246 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py +++ b/neutron/plugins/ml2/drivers/openvswitch/mech_driver/mech_openvswitch.py @@ -17,6 +17,7 @@ import os import uuid from neutron_lib.api.definitions import portbindings +from neutron_lib.api.definitions import provider_net from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib import constants @@ -200,3 +201,7 @@ class OpenvswitchMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase): a_const.VHOST_USER_SOCKET_DIR) sock_name = (constants.VHOST_USER_DEVICE_PREFIX + port_id)[:14] return os.path.join(sockdir, sock_name) + + @staticmethod + def provider_network_attribute_updates_supported(): + return [provider_net.SEGMENTATION_ID] diff --git a/neutron/plugins/ml2/rpc.py b/neutron/plugins/ml2/rpc.py index b2eaebbf98e..c48f2fdda5a 100644 --- a/neutron/plugins/ml2/rpc.py +++ b/neutron/plugins/ml2/rpc.py @@ -51,7 +51,8 @@ class RpcCallbacks(type_tunnel.TunnelRpcCallbackMixin): # 1.4 tunnel_sync rpc signature upgrade to obtain 'host' # 1.5 Support update_device_list and # get_devices_details_list_and_failed_devices - target = oslo_messaging.Target(version='1.5') + # 1.6 Support get_network_details + target = oslo_messaging.Target(version='1.6') def __init__(self, notifier, type_manager): self.setup_tunnel_callback_mixin(notifier, type_manager) @@ -69,7 +70,7 @@ class RpcCallbacks(type_tunnel.TunnelRpcCallbackMixin): def _get_request_details(kwargs): return (kwargs.get('agent_id'), kwargs.get('host'), - kwargs.get('device')) + kwargs.get('device') or kwargs.get('network')) def get_device_details(self, rpc_context, **kwargs): """Agent requests device details.""" @@ -219,6 +220,15 @@ class RpcCallbacks(type_tunnel.TunnelRpcCallbackMixin): return {'devices': devices, 'failed_devices': failed_devices} + def get_network_details(self, rpc_context, **kwargs): + """Agent requests network details.""" + agent_id, host, network = self._get_request_details(kwargs) + LOG.debug("Network %(network)s details requested by agent " + "%(agent_id)s with host %(host)s", + {'network': network, 'agent_id': agent_id, 'host': host}) + plugin = directory.get_plugin() + return plugin.get_network(rpc_context, network) + def update_device_down(self, rpc_context, **kwargs): """Device no longer exists on agent.""" # TODO(garyk) - live migration and port status diff --git a/neutron/tests/unit/agent/test_rpc.py b/neutron/tests/unit/agent/test_rpc.py index 724eed89b9b..38efd74f57f 100644 --- a/neutron/tests/unit/agent/test_rpc.py +++ b/neutron/tests/unit/agent/test_rpc.py @@ -54,6 +54,9 @@ class AgentRPCPluginApi(base.BaseTestCase): def test_get_devices_details_list(self): self._test_rpc_call('get_devices_details_list') + def test_get_network_details(self): + self._test_rpc_call('get_network_details') + def test_update_device_down(self): self._test_rpc_call('update_device_down') diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py index 1ae9472641a..3beb3368ca7 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py @@ -17,6 +17,7 @@ import time import mock from neutron_lib.agent import constants as agent_consts +from neutron_lib.api.definitions import provider_net from neutron_lib import constants as n_const from neutron_lib import rpc as n_rpc from oslo_config import cfg @@ -1208,7 +1209,10 @@ class TestOvsNeutronAgent(object): port = {'id': TEST_PORT_ID1, 'network_id': network['id']} self.agent._update_port_network(port['id'], port['network_id']) - self.agent.network_update(context=None, network=network) + with mock.patch.object(self.agent.plugin_rpc, 'get_network_details'), \ + mock.patch.object(self.agent, + '_update_network_segmentation_id'): + self.agent.network_update(context=None, network=network) self.assertEqual(set([port['id']]), self.agent.updated_ports) def test_network_update_outoforder(self): @@ -1222,7 +1226,10 @@ class TestOvsNeutronAgent(object): self.agent._update_port_network(port['id'], port['network_id']) self.agent.port_delete(context=None, port_id=port['id']) - self.agent.network_update(context=None, network=network) + with mock.patch.object(self.agent.plugin_rpc, 'get_network_details'), \ + mock.patch.object(self.agent, + '_update_network_segmentation_id'): + self.agent.network_update(context=None, network=network) self.assertEqual(set(), self.agent.updated_ports) def test_update_port_network(self): @@ -2362,6 +2369,58 @@ class TestOvsNeutronAgent(object): else: bridge.set_datapath_id.assert_called_once_with(dpid) + def test__update_network_segmentation_id(self): + network = {'id': 'my-net-uuid', + provider_net.SEGMENTATION_ID: 1005, + provider_net.PHYSICAL_NETWORK: 'provider_net', + provider_net.NETWORK_TYPE: n_const.TYPE_VLAN} + self.agent.vlan_manager.add('my-net-uuid', 5, n_const.TYPE_VLAN, + 'provider_net', 1004, None) + mock_phys_br = mock.Mock() + self.agent.phys_brs['provider_net'] = mock_phys_br + self.agent.phys_ofports['provider_net'] = 'phy_ofport' + self.agent.int_ofports['provider_net'] = 'int_ofport' + + with mock.patch.object(self.agent.int_br, 'reclaim_local_vlan') \ + as mock_reclaim_local_vlan, \ + mock.patch.object(self.agent.int_br, 'provision_local_vlan') \ + as mock_provision_local_vlan: + self.agent._update_network_segmentation_id(network) + mock_reclaim_local_vlan.assert_called_once_with( + port='int_ofport', segmentation_id=1004) + mock_provision_local_vlan.assert_called_once_with( + port='int_ofport', lvid=5, segmentation_id=1005) + mock_phys_br.reclaim_local_vlan.assert_called_once_with( + port='phy_ofport', lvid=5) + + def test__update_network_segmentation_id_not_vlan(self): + network = {provider_net.NETWORK_TYPE: 'not_vlan'} + with mock.patch.object(self.agent.vlan_manager, 'get') as mock_get: + self.agent._update_network_segmentation_id(network) + mock_get.assert_not_called() + + def test__update_network_segmentation_id_vlan_not_found(self): + network = {'id': 'my-net-uuid', + provider_net.SEGMENTATION_ID: 1005, + provider_net.NETWORK_TYPE: n_const.TYPE_VLAN, + provider_net.PHYSICAL_NETWORK: 'default_network'} + with mock.patch.object(self.agent.vlan_manager, + 'update_segmentation_id') as mock_update_segid: + self.agent._update_network_segmentation_id(network) + mock_update_segid.assert_not_called() + + def test__update_network_segmentation_id_segmentation_id_not_updated(self): + network = {'id': 'my-net-uuid', + provider_net.SEGMENTATION_ID: 1005, + provider_net.NETWORK_TYPE: n_const.TYPE_VLAN, + provider_net.PHYSICAL_NETWORK: 'default_network'} + self.agent.vlan_manager.add('my-net-uuid', 5, n_const.TYPE_VLAN, + 'provider_net', 1005, None) + with mock.patch.object(self.agent.vlan_manager, + 'update_segmentation_id') as mock_update_segid: + self.agent._update_network_segmentation_id(network) + mock_update_segid.assert_not_called() + class TestOvsNeutronAgentOFCtl(TestOvsNeutronAgent, ovs_test_base.OVSOFCtlTestBase): diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_vlanmanager.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_vlanmanager.py index 429327bee00..9be3f7a83a6 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_vlanmanager.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_vlanmanager.py @@ -118,3 +118,10 @@ class TestLocalVlanManager(base.BaseTestCase): def test_pop_non_existing_raises_exception(self): with testtools.ExpectedException(vlanmanager.MappingNotFound): self.vlan_manager.pop(1) + + def test_update_segmentation_id(self): + self.vlan_manager.add('net_id', 'vlan_id', 'vlan', 'phys_net', + 1001, None) + self.assertEqual(1001, self.vlan_manager.get('net_id').segmentation_id) + self.vlan_manager.update_segmentation_id('net_id', 1002) + self.assertEqual(1002, self.vlan_manager.get('net_id').segmentation_id) diff --git a/neutron/tests/unit/plugins/ml2/test_rpc.py b/neutron/tests/unit/plugins/ml2/test_rpc.py index 69390393cd0..41bd89201d0 100644 --- a/neutron/tests/unit/plugins/ml2/test_rpc.py +++ b/neutron/tests/unit/plugins/ml2/test_rpc.py @@ -214,6 +214,17 @@ class RpcCallbacksTestCase(base.BaseTestCase): self.callbacks.get_devices_details_list_and_failed_devices) self._test_get_devices_list(callback, devices, expected) + def test_get_network_details(self): + kwargs = {'agent_id': 'agent_id', + 'host': 'host_id', + 'network': 'network'} + with mock.patch.object(self.plugin, 'get_network') as mock_get_network: + mock_get_network.return_value = 'net_details' + self.assertEqual( + 'net_details', + self.callbacks.get_network_details('fake_context', **kwargs)) + mock_get_network.assert_called_once_with('fake_context', 'network') + def test_get_devices_details_list_and_failed_devices_empty_dev(self): with mock.patch.object(self.callbacks, 'get_device_details') as f: res = self.callbacks.get_devices_details_list_and_failed_devices( diff --git a/releasenotes/notes/change-segmentation-id-ovs-a201e0ac1c4d4fb6.yaml b/releasenotes/notes/change-segmentation-id-ovs-a201e0ac1c4d4fb6.yaml new file mode 100644 index 00000000000..9c0342bcdfb --- /dev/null +++ b/releasenotes/notes/change-segmentation-id-ovs-a201e0ac1c4d4fb6.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The segmentation ID of a provider network can be now modified, even with + OVS ports bound. Note that, during this process, the traffic of the bound + ports tagged with the former segmentation ID (external VLAN) will be mapped + to the new one. This can provoke a traffic disruption while the external + network VLAN is migrated to the new tag.