Add binding activation to the Linuxbridge agent

As part of the implementation of multiple port bindings [1], add binding
activation support to the linux bridge agent. This will enable the
execution with linux bridge agents of the complete sequence of steps
outlined in [1] during an instance migration:

1) Create inactive port bindings for destination host
2) Migrate the instance to the destination host and plug its VIFs
3) Activate the port bindings in the destination host
4) Delete the port bindings for the source host

[1] https://review.openstack.org/#/c/309416/

Change-Id: I2c937cc0a551e5ce0e8534c4dd4384ec2ca92da1
Partial-Bug: #1580880
This commit is contained in:
Miguel Lavalle 2018-06-23 22:58:42 -05:00
parent 5c3bf12496
commit f7064f2b6c
8 changed files with 89 additions and 3 deletions

View File

@ -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)

View File

@ -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 "

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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':

View File

@ -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',

View File

@ -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``.