Handling dhcp port migration

Whenever a DHCP agent dies, the port is attached to a dummy device
identified by DEVICE_ID_RESERVED_DHCP_PORT. Once the dhcp agent is
respawned, the port is reattached to the newly created DHCP instance.
This deletes the old dhcp port from the old host and creates the port
on the new host. The dhcp port transitions from

(Active <old host, old dhcp, vif:ovs>) to
(Active <old host, reserved, vif:ovs>) to
(Down   <new host, new dhcp, vif:unbound>) to
(Down   <new host, new dhcp, vif:ovs>) to
(Build  <new host, new dhcp, vif:ovs>) to
(Active <new host, new dhcp, vif:ovs>)

When the port is updated to
(Active <old host, reserved, vif:ovs>), the port needs to be removed from
old host and when the port is updated to (Down <new host, new dhcp, vif:unbound>),
it should be created on the new host. Removal and creation should take place
in two updates because when the port is updated to
(Down <new host, new dhcp, vif:unbound>), the original port would have
the device id set to 'reserved_dhcp_port' and so it can't be removed
from CVX at that point.

Change-Id: I0ec0e116339b6e2de0b6e0c4bf1bac2e77f11483
changes/10/432510/9
Shashank Hegde 6 years ago committed by Mitchell Jameson
parent d7758a3f28
commit e1619f9490

@ -19,6 +19,7 @@ from neutron_lib import constants as n_const
from oslo_config import cfg
from oslo_log import log as logging
from neutron.common import constants as neutron_const
from neutron.extensions import portbindings
from neutron.plugins.common import constants as p_const
from neutron.plugins.ml2.common import exceptions as ml2_exc
@ -441,6 +442,7 @@ class AristaDriver(driver_api.MechanismDriver):
orig_status = context.original_status
new_status = context.status
new_host = context.host
new_port = context.current
port_id = orig_port['id']
if (new_host != orig_host and
@ -452,7 +454,7 @@ class AristaDriver(driver_api.MechanismDriver):
# Ensure that we use tenant Id for the network owner
tenant_id = self._network_owner_tenant(context, network_id,
tenant_id)
device_id = orig_port['device_id']
device_id = new_port['device_id']
with self.eos_sync_lock:
port_provisioned = db_lib.is_port_provisioned(port_id,
orig_host)
@ -705,7 +707,8 @@ class AristaDriver(driver_api.MechanismDriver):
context.network.current['name'], all_segments)
except arista_exc.AristaRpcError:
LOG.error(_LE("Failed to create network segments"))
raise ml2_exc.MechanismDriverError()
raise ml2_exc.MechanismDriverError(
method='update_port_postcommit')
try:
orig_host = context.original_host
@ -714,7 +717,8 @@ class AristaDriver(driver_api.MechanismDriver):
# We care about port status only for DVR ports
port_down = context.status == n_const.PORT_STATUS_DOWN
if orig_host and (port_down or host != orig_host):
if orig_host and (port_down or host != orig_host or
device_id == neutron_const.DEVICE_ID_RESERVED_DHCP_PORT):
try:
LOG.info("Deleting the port %s" % str(orig_port))
# The port moved to a different host or the VM
@ -726,7 +730,8 @@ class AristaDriver(driver_api.MechanismDriver):
# about it. Log a warning and move on.
LOG.warning(UNABLE_TO_DELETE_PORT_MSG)
if(port_provisioned and net_provisioned and hostname and
is_vm_boot and not port_down):
is_vm_boot and not port_down and
device_id != neutron_const.DEVICE_ID_RESERVED_DHCP_PORT):
LOG.info(_LI("Port plugged into network"))
# Plug port into the network only if it exists in the db
# and is bound to a host and the port is up.

@ -16,6 +16,7 @@
import mock
from neutron_lib import constants as n_const
from neutron.common import constants as neutron_const
from neutron.extensions import portbindings
from neutron.plugins.ml2 import driver_api as api
from neutron.tests.unit import testlib_api
@ -983,6 +984,370 @@ class AristaDriverTestCase(testlib_api.SqlTestCase):
mechanism_arista.db_lib.assert_has_calls(expected_calls)
def test_update_port_precommit_dhcp_reserved_port(self):
'''Test to ensure the dhcp port migration is handled correctly.
Whenever a DHCP agent dies, the port is attached to a dummy device
identified by DEVICE_ID_RESERVED_DHCP_PORT. Once the dhcp agent is
respawned, the port is reattached to the newly created DHCP instance.
This deletes the old dhcp port from the old host and creates the port
on the new host. The dhcp port transitions from
(Active <old host, old dhcp, vif:ovs>) to
(Active <old host, reserved, vif:ovs>) to
(Down <new host, new dhcp, vif:unbound>) to
(Down <new host, new dhcp, vif:ovs>) to
(Build <new host, new dhcp, vif:ovs>) to
(Active <new host, new dhcp, vif:ovs>)
When the port is updated to (Active <old host, reserved, vif:ovs>),
the port needs to be removed from old host and when the port is updated
to (Down <new host, new dhcp, vif:unbound>), it should be created on
the new host. Removal and creation should take place in two updates
because when the port is updated to
(Down <new host, new dhcp, vif:unbound>), the original port would have
the device id set to 'reserved_dhcp_port' and so it can't be removed
from CVX at that point.
'''
tenant_id = 't1'
network_id = 'n1'
old_device_id = 'old_device_id'
new_device_id = 'new_device_id'
reserved_device = neutron_const.DEVICE_ID_RESERVED_DHCP_PORT
old_host = 'ubuntu1'
new_host = 'ubuntu2'
port_id = 101
segmentation_id = 1000
network_context = self._get_network_context(tenant_id,
network_id,
segmentation_id,
False)
segment_id = network_context.network_segments[-1]['id']
# (Active <old host, old dhcp, vif:ovs>) to
# (Active <old host, reserved, vif:ovs>)
context = self._get_port_context(
tenant_id, network_id, old_device_id,
network_context, device_owner=n_const.DEVICE_OWNER_DHCP)
context.current['device_id'] = reserved_device
network = {'tenant_id': tenant_id}
self.drv.ndb.get_network_from_net_id.return_value = [network]
mechanism_arista.db_lib.is_port_provisioned.return_value = True
mechanism_arista.db_lib.is_network_provisioned.return_value = True
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_precommit(context)
expected_calls = [
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
segment_id),
mock.call.is_port_provisioned(port_id, None),
mock.call.update_port(reserved_device,
old_host, port_id,
network_id, tenant_id)
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Active <old host, reserved, vif:ovs>) to
# (Down <new host, new dhcp, vif:unbound>)
context = self._get_port_context(
tenant_id, network_id, reserved_device, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP)
context.current['device_id'] = new_device_id
context.current['binding:host_id'] = new_host
context.current['status'] = 'DOWN'
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_precommit(context)
expected_calls = [
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
segment_id),
mock.call.is_port_provisioned(port_id, None),
mock.call.update_port(new_device_id,
new_host, port_id,
network_id, tenant_id)
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Down <new host, new dhcp, vif:unbound>) to
# (Down <new host, new dhcp, vif:ovs>) to
context = self._get_port_context(
tenant_id, network_id, new_device_id, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP, status='DOWN')
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_precommit(context)
expected_calls = [
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
segment_id),
mock.call.is_port_provisioned(port_id, None),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Down <new host, new dhcp, vif:ovs>) to
# (Build <new host, new dhcp, vif:ovs>) to
context = self._get_port_context(
tenant_id, network_id, new_device_id, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP, status='DOWN')
context.current['status'] = 'BUILD'
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_precommit(context)
expected_calls = [
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
segment_id),
mock.call.is_port_provisioned(port_id, None),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Build <new host, new dhcp, vif:ovs>) to
# (Active <new host, new dhcp, vif:ovs>)
context = self._get_port_context(
tenant_id, network_id, new_device_id, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP, status='BUILD')
context.current['status'] = 'ACTIVE'
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_precommit(context)
expected_calls = [
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
segment_id),
mock.call.is_port_provisioned(port_id, None),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
def test_update_port_postcommit_dhcp_reserved_port(self):
'''Test to ensure the dhcp port migration is handled correctly.
Whenever a DHCP agent dies, the port is attached to a dummy device
identified by DEVICE_ID_RESERVED_DHCP_PORT. Once the dhcp agent is
respawned, the port is reattached to the newly created DHCP instance.
This deletes the old dhcp port from the old host and creates the port
on the new host. The dhcp port transitions from
(Active <old host, old dhcp, vif:ovs>) to
(Active <old host, reserved, vif:ovs>) to
(Down <new host, new dhcp, vif:unbound>) to
(Down <new host, new dhcp, vif:ovs>) to
(Build <new host, new dhcp, vif:ovs>) to
(Active <new host, new dhcp, vif:ovs>)
When the port is updated to (Active <old host, reserved, vif:ovs>),
the port needs to be removed from old host and when the port is updated
to (Down <new host, new dhcp, vif:unbound>), it should be created on
the new host. Removal and creation should take place in two updates
because when the port is updated to
(Down <new host, new dhcp, vif:unbound>), the original port would have
the device id set to 'reserved_dhcp_port' and so it can't be removed
from CVX at that point.
'''
tenant_id = 't1'
network_id = 'n1'
old_device_id = 'old_device_id'
new_device_id = 'new_device_id'
reserved_device = neutron_const.DEVICE_ID_RESERVED_DHCP_PORT
old_host = 'ubuntu1'
new_host = 'ubuntu2'
port_id = 101
segmentation_id = 1000
network_context = self._get_network_context(tenant_id,
network_id,
segmentation_id,
False)
segments = network_context.network_segments
# (Active <old host, old dhcp, vif:ovs>) to
# (Active <old host, reserved, vif:ovs>)
context = self._get_port_context(
tenant_id, network_id, old_device_id, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP)
context.current['device_id'] = reserved_device
vnic_type = context.current['binding:vnic_type']
profile = context.current['binding:profile']
port_name = context.current['name']
network = {'tenant_id': tenant_id}
self.drv.ndb.get_network_from_net_id.return_value = [network]
mechanism_arista.db_lib.is_port_provisioned.return_value = True
mechanism_arista.db_lib.is_network_provisioned.return_value = True
mechanism_arista.db_lib.get_shared_network_owner_id.return_value = 1
mechanism_arista.db_lib.num_nets_provisioned.return_value = 1
mechanism_arista.db_lib.num_vms_provisioned.return_value = 1
self.drv.rpc.hpb_supported.return_value = False
self.drv.ndb.get_network_segments.return_value = segments
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_postcommit(context)
expected_calls = [
mock.call.is_port_provisioned(port_id, None),
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
None),
mock.call.hpb_supported(),
mock.call.is_network_provisioned(tenant_id, network_id,
None, None),
mock.call.unplug_port_from_network(old_device_id,
n_const.DEVICE_OWNER_DHCP,
old_host,
port_id, network_id,
tenant_id,
None, vnic_type,
switch_bindings=profile),
mock.call.remove_security_group(None, profile),
mock.call.num_nets_provisioned(tenant_id),
mock.call.num_vms_provisioned(tenant_id),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Active <old host, reserved, vif:ovs>) to
# (Down <new host, new dhcp, vif:unbound>)
context = self._get_port_context(
tenant_id, network_id, reserved_device, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP)
context.current['device_id'] = new_device_id
context.current['binding:host_id'] = new_host
context.current['status'] = 'DOWN'
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_postcommit(context)
expected_calls = [
mock.call.is_port_provisioned(port_id, None),
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
None),
mock.call.hpb_supported(),
mock.call.is_network_provisioned(tenant_id, network_id,
None, None),
mock.call.unplug_port_from_network(reserved_device,
n_const.DEVICE_OWNER_DHCP,
old_host,
port_id, network_id,
tenant_id,
None, vnic_type,
switch_bindings=profile),
mock.call.remove_security_group(None, profile),
mock.call.num_nets_provisioned(tenant_id),
mock.call.num_vms_provisioned(tenant_id),
mock.call.plug_port_into_network(new_device_id,
new_host,
port_id, network_id,
tenant_id, port_name,
n_const.DEVICE_OWNER_DHCP,
None, None, vnic_type,
segments=[],
switch_bindings=profile),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Down <new host, new dhcp, vif:unbound>) to
# (Down <new host, new dhcp, vif:ovs>) to
context = self._get_port_context(
tenant_id, network_id, new_device_id, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP, status='DOWN')
context.current['binding:host_id'] = new_host
context.original['binding:host_id'] = new_host
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_postcommit(context)
expected_calls = [
mock.call.is_port_provisioned(port_id, None),
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
None),
mock.call.hpb_supported(),
mock.call.plug_port_into_network(new_device_id,
new_host,
port_id, network_id,
tenant_id, port_name,
n_const.DEVICE_OWNER_DHCP,
None, None, vnic_type,
segments=[],
switch_bindings=profile),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Down <new host, new dhcp, vif:ovs>) to
# (Build <new host, new dhcp, vif:ovs>) to
context = self._get_port_context(
tenant_id, network_id, new_device_id, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP, status='DOWN')
context.current['binding:host_id'] = new_host
context.original['binding:host_id'] = new_host
context.current['status'] = 'BUILD'
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_postcommit(context)
expected_calls = [
mock.call.is_port_provisioned(port_id, None),
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
None),
mock.call.hpb_supported(),
mock.call.plug_port_into_network(new_device_id,
new_host,
port_id, network_id,
tenant_id, port_name,
n_const.DEVICE_OWNER_DHCP,
None, None, vnic_type,
segments=[],
switch_bindings=profile),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
# (Build <new host, new dhcp, vif:ovs>) to
# (Active <new host, new dhcp, vif:ovs>)
context = self._get_port_context(
tenant_id, network_id, new_device_id, network_context,
device_owner=n_const.DEVICE_OWNER_DHCP, status='BUILD')
context.current['binding:host_id'] = new_host
context.original['binding:host_id'] = new_host
context.current['status'] = 'ACTIVE'
mechanism_arista.db_lib.reset_mock()
self.drv.update_port_postcommit(context)
expected_calls = [
mock.call.is_port_provisioned(port_id, None),
mock.call.is_network_provisioned(tenant_id, network_id,
segmentation_id,
None),
mock.call.hpb_supported(),
]
mechanism_arista.db_lib.assert_has_calls(expected_calls)
def _get_network_context(self, tenant_id, net_id,
segmentation_id, shared):
network = {'id': net_id,
@ -1079,6 +1444,7 @@ class FakePortContext(object):
self._network_context = network
self._status = status
self._binding_levels = binding_levels
self._original_binding_levels = []
@property
def current(self):

Loading…
Cancel
Save