diff --git a/neutron/plugins/ml2/drivers/agent/_common_agent.py b/neutron/plugins/ml2/drivers/agent/_common_agent.py index 4e5b15ac950..bf559a3deb3 100644 --- a/neutron/plugins/ml2/drivers/agent/_common_agent.py +++ b/neutron/plugins/ml2/drivers/agent/_common_agent.py @@ -39,6 +39,7 @@ from neutron.agent import securitygroups_rpc as agent_sg_rpc from neutron.api.rpc.callbacks import resources from neutron.api.rpc.handlers import securitygroups_rpc as sg_rpc from neutron.common import config as common_config +from neutron.common import constants as c_const from neutron.plugins.ml2.drivers.agent import _agent_manager_base as amb from neutron.plugins.ml2.drivers.agent import capabilities from neutron.plugins.ml2.drivers.agent import config as cagt_config # noqa @@ -311,6 +312,8 @@ class CommonAgentLoop(service.Service): events.AFTER_UPDATE, self, context=self.context, device_details=device_details) + elif c_const.NO_ACTIVE_BINDING in device_details: + LOG.info("Device %s has no active binding in host", device) else: LOG.info("Device %s not defined on plugin", device) diff --git a/neutron/plugins/ml2/drivers/linuxbridge/agent/linuxbridge_neutron_agent.py b/neutron/plugins/ml2/drivers/linuxbridge/agent/linuxbridge_neutron_agent.py index cbd7986a368..d153f4ecdfb 100644 --- a/neutron/plugins/ml2/drivers/linuxbridge/agent/linuxbridge_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/linuxbridge/agent/linuxbridge_neutron_agent.py @@ -835,7 +835,8 @@ class LinuxBridgeManager(amb.CommonAgentManagerBase): [topics.NETWORK, topics.DELETE], [topics.NETWORK, topics.UPDATE], [topics.SECURITY_GROUP, topics.UPDATE], - [n_topics.PORT_BINDING, n_topics.DEACTIVATE]] + [n_topics.PORT_BINDING, n_topics.DEACTIVATE], + [n_topics.PORT_BINDING, n_topics.ACTIVATE]] if cfg.CONF.VXLAN.l2_population: consumers.append([topics.L2POPULATION, topics.UPDATE]) return consumers @@ -871,7 +872,7 @@ class LinuxBridgeRpcCallbacks( # 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 - # 1.5 Added binding_deactivate + # 1.5 Added binding_activate and binding_deactivate target = oslo_messaging.Target(version='1.5') def network_delete(self, context, **kwargs): @@ -914,6 +915,17 @@ class LinuxBridgeRpcCallbacks( 'bridge_name': bridge_name}) self.agent.mgr.remove_interface(bridge_name, interface_name) + def binding_activate(self, context, **kwargs): + if kwargs.get('host') != cfg.CONF.host: + return + # Since the common agent loop treats added and updated the same way, + # just add activated ports to the updated devices list. This way, + # adding binding activation is less disruptive to the existing code + port_id = kwargs.get('port_id') + device_name = self.agent.mgr.get_tap_device_name(port_id) + self.updated_devices.add(device_name) + LOG.debug("Binding activation received for port: %s", port_id) + def network_update(self, context, **kwargs): network_id = kwargs['network']['id'] LOG.debug("network_update message processed for network " diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index dd33eae0347..7be8cd3ce78 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -2202,6 +2202,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, self.notifier.binding_deactivate(context, port_id, active_binding.host, network['id']) + self.notifier.binding_activate(context, port_id, + inactive_binding.host) return self._make_port_binding_dict(cur_context._binding) raise n_exc.PortBindingError(port_id=port_id, host=host) diff --git a/neutron/plugins/ml2/rpc.py b/neutron/plugins/ml2/rpc.py index 01bf781b89b..8092545f692 100644 --- a/neutron/plugins/ml2/rpc.py +++ b/neutron/plugins/ml2/rpc.py @@ -28,6 +28,7 @@ from sqlalchemy.orm import exc from neutron.agent import _topics as n_topics from neutron.api.rpc.handlers import dvr_rpc from neutron.api.rpc.handlers import securitygroups_rpc as sg_rpc +from neutron.common import constants as c_const from neutron.common import rpc as n_rpc from neutron.db import l3_hamode_db from neutron.db import provisioning_blocks @@ -124,6 +125,15 @@ class RpcCallbacks(type_tunnel.TunnelRpcCallbackMixin): 'vif_type': port_context.vif_type}) return {'device': device} + if (port['device_owner'].startswith( + n_const.DEVICE_OWNER_COMPUTE_PREFIX) and + port[portbindings.HOST_ID] != host): + LOG.debug("Device %(device)s has no active binding in host " + "%(host)s", {'device': device, + 'host': host}) + return {'device': device, + c_const.NO_ACTIVE_BINDING: True} + network_qos_policy_id = port_context.network._network.get( qos_consts.QOS_POLICY_ID) entry = {'device': device, @@ -384,7 +394,7 @@ class AgentNotifierApi(dvr_rpc.DVRAgentRpcApiMixin, 1.1 - Added get_active_networks_info, create_dhcp_port, update_dhcp_port, and removed get_dhcp_port methods. 1.4 - Added network_update - 1.5 - Added binding_deactivate + 1.5 - Added binding_activate and binding_deactivate """ def __init__(self, topic): @@ -403,6 +413,8 @@ class AgentNotifierApi(dvr_rpc.DVRAgentRpcApiMixin, topics.UPDATE) self.topic_port_binding_deactivate = topics.get_topic_name( topic, n_topics.PORT_BINDING, n_topics.DEACTIVATE) + self.topic_port_binding_activate = topics.get_topic_name( + topic, n_topics.PORT_BINDING, n_topics.ACTIVATE) target = oslo_messaging.Target(topic=topic, version='1.0') self.client = n_rpc.get_client(target) @@ -435,3 +447,8 @@ class AgentNotifierApi(dvr_rpc.DVRAgentRpcApiMixin, fanout=True, version='1.5') cctxt.cast(context, 'binding_deactivate', port_id=port_id, host=host, network_id=network_id) + + def binding_activate(self, context, port_id, host): + cctxt = self.client.prepare(topic=self.topic_port_binding_activate, + fanout=True, version='1.5') + cctxt.cast(context, 'binding_activate', port_id=port_id, host=host) diff --git a/neutron/tests/unit/plugins/ml2/drivers/agent/test__common_agent.py b/neutron/tests/unit/plugins/ml2/drivers/agent/test__common_agent.py index c7091bbf5cf..4ecc7703a13 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/agent/test__common_agent.py +++ b/neutron/tests/unit/plugins/ml2/drivers/agent/test__common_agent.py @@ -24,6 +24,7 @@ from oslo_config import cfg import testtools from neutron.agent.linux import bridge_lib +from neutron.common import constants as n_const 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.tests import base @@ -520,6 +521,13 @@ class TestCommonAgentLoop(base.BaseTestCase): # device exists so it should raise self.agent._process_device_if_exists(mock_details) + def test__process_device_if_exists_no_active_binding_in_host(self): + mock_details = {'device': 'dev123', + n_const.NO_ACTIVE_BINDING: True} + self.agent.mgr = mock.Mock() + self.agent._process_device_if_exists(mock_details) + self.agent.mgr.setup_arp_spoofing_protection.assert_not_called() + def test_set_rpc_timeout(self): self.agent.stop() for rpc_client in (self.agent.plugin_rpc.client, diff --git a/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/test_linuxbridge_neutron_agent.py b/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/test_linuxbridge_neutron_agent.py index a72fe0332d4..9837f0e39d1 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/test_linuxbridge_neutron_agent.py +++ b/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/test_linuxbridge_neutron_agent.py @@ -1051,6 +1051,18 @@ class TestLinuxBridgeRpcCallbacks(base.BaseTestCase): get_tap_fn.assert_not_called() rem_intf.assert_not_called() + def test_binding_activate(self): + with mock.patch.object(self.lb_rpc.agent.mgr, + "get_tap_device_name") as get_tap_fun: + get_tap_fun.return_value = "tap456" + self.lb_rpc.binding_activate(mock.ANY, host="host", port_id="456") + self.assertIn("tap456", self.lb_rpc.updated_devices) + + def test_binding_activate_not_for_host(self): + self.lb_rpc.binding_activate(mock.ANY, host="other-host", + port_id="456") + self.assertFalse(self.lb_rpc.updated_devices) + def _test_fdb_add(self, proxy_enabled=False): fdb_entries = {'net_id': {'ports': diff --git a/neutron/tests/unit/plugins/ml2/test_rpc.py b/neutron/tests/unit/plugins/ml2/test_rpc.py index 646e6a5f946..e4e6ccda18d 100644 --- a/neutron/tests/unit/plugins/ml2/test_rpc.py +++ b/neutron/tests/unit/plugins/ml2/test_rpc.py @@ -21,6 +21,7 @@ import collections import mock from neutron_lib.agent import topics +from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import resources from neutron_lib import constants from neutron_lib.plugins import constants as plugin_constants @@ -31,6 +32,7 @@ from oslo_context import context as oslo_context from sqlalchemy.orm import exc from neutron.agent import rpc as agent_rpc +from neutron.common import constants as n_const from neutron.db import provisioning_blocks from neutron.plugins.ml2 import db as ml2_db from neutron.plugins.ml2.drivers import type_tunnel @@ -154,6 +156,14 @@ class RpcCallbacksTestCase(base.BaseTestCase): res = self.callbacks.get_device_details(mock.Mock(), host='fake') self.assertEqual('test-policy-id', res['network_qos_policy_id']) + def test_get_device_details_port_no_active_in_host(self): + port = collections.defaultdict(lambda: 'fake_port') + self.plugin.get_bound_port_context().current = port + port['device_owner'] = constants.DEVICE_OWNER_COMPUTE_PREFIX + port[portbindings.HOST_ID] = 'other-host' + res = self.callbacks.get_device_details(mock.Mock(), host='host') + self.assertIn(n_const.NO_ACTIVE_BINDING, res) + def test_get_device_details_qos_policy_id_from_port(self): port = collections.defaultdict( lambda: 'fake_port', diff --git a/releasenotes/notes/add-multiple-port-bindings-f16eb47ebdddff2d.yaml b/releasenotes/notes/add-multiple-port-bindings-f16eb47ebdddff2d.yaml new file mode 100644 index 00000000000..1cad21191ab --- /dev/null +++ b/releasenotes/notes/add-multiple-port-bindings-f16eb47ebdddff2d.yaml @@ -0,0 +1,22 @@ +--- +prelude: > + Support multiple bindings for compute owned ports. +features: + - | + In order to better support instance migration, multiple port + bindings can be associated to compute owned ports. + + * Create, update, list, show and activate operations are supported + for port bindings by the ReST API. + * A compute owned port can have one active binding and many + inactive bindings. + * There can be only one binding (active or inactive) per compute + host. + * When the ``activate`` operation is executed, a previously + inactive binding is made active. The previously active binding + becomes inactive. + * As a consequence of the multiple port bindings implementation, + the ``port_binding`` relationship in the SQLAlchemy ``Port`` + object has been renamed ``port_bindings``. Similarly, the + ``binding`` attribute of the ``Port`` OVO has been renamed + ``bindings``.