diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py index a84bd3f5c7c..e006651b26e 100644 --- a/neutron/common/ovn/utils.py +++ b/neutron/common/ovn/utils.py @@ -1059,10 +1059,13 @@ def determine_bind_host(sb_idl, port, port_context=None): def validate_port_binding_and_virtual_port( - port_context, nb_idl, sb_idl, ml2_plugin, port): + port_context, nb_idl, ml2_plugin, port, original_port): """If the port is type=virtual and it is bound, raise BadRequest""" - if not determine_bind_host(sb_idl, port, port_context=port_context): - # The port is not bound, exit. + # If the port receives an update of the device ID and the binding profile + # host ID fields, at the same time, this is because Nova is trying to bind + # the port to a VM (device ID) in a host (host ID). + if not (port['device_id'] != original_port['device_id'] and + port[portbindings.HOST_ID] != original_port[portbindings.HOST_ID]): return fixed_ips = port.get('fixed_ips', []) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py index 6bf95b35dad..78b68e82e67 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py @@ -823,7 +823,7 @@ class OVNMechanismDriver(api.MechanismDriver): ovn_utils.validate_and_get_data_from_binding_profile(port) self._validate_port_extra_dhcp_opts(port) ovn_utils.validate_port_binding_and_virtual_port( - context, self.nb_ovn, self.sb_ovn, self._plugin, port) + context, self.nb_ovn, self._plugin, port, original_port) if self._is_port_provisioning_required(port, context.host, context.original_host): self._insert_port_provisioning_block(context.plugin_context, diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py index 8ab9e8bebee..48bc21a4e52 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py @@ -4434,20 +4434,21 @@ class TestOVNVVirtualPort(OVNMechanismDriverTestCase): with self.port(subnet=self.subnet, is_admin=True) as port: port = port['port'] updated_port = copy.deepcopy(port) - updated_port[portbindings.HOST_ID] = 'host1' - context = mock.Mock(current=updated_port, original=port) + updated_port['device_id'] = 'device_id_new' + updated_port[portbindings.HOST_ID] = 'host_id_new' + _context = mock.Mock(current=updated_port, original=port) with mock.patch.object(self.mech_driver._plugin, 'get_subnets') \ as mock_get_subnets: mock_get_subnets.return_value = [self.subnet['subnet']] # 1) The port is not virtual, it has no parents. self.mock_vp_parents.return_value = '' - self.mech_driver.update_port_precommit(context) + self.mech_driver.update_port_precommit(_context) # 2) The port (LSP) has parents, that means it is a virtual # port. self.mock_vp_parents.return_value = ['parent-0', 'parent-1'] self.assertRaises(n_exc.BadRequest, self.mech_driver.update_port_precommit, - context) + _context) class TestOVNAvailabilityZone(OVNMechanismDriverTestCase): diff --git a/releasenotes/notes/ovn-virtual-port-prevent-binding-50efba5521e8a28e.yaml b/releasenotes/notes/ovn-virtual-port-prevent-binding-50efba5521e8a28e.yaml index 3d1373d5a00..9977c76c588 100644 --- a/releasenotes/notes/ovn-virtual-port-prevent-binding-50efba5521e8a28e.yaml +++ b/releasenotes/notes/ovn-virtual-port-prevent-binding-50efba5521e8a28e.yaml @@ -6,4 +6,5 @@ other: first one is considered a virtual port. If the second port (non-virtual) is bound to ML2/OVN, the virtual port cannot be bound to a virtual machine; a virtual port is created only to reserve a set of IP addresses - to be used by other ports. + to be used by other ports. The OVN mechanism driver prevents that a virtual + port has a device ID; a device ID is provided when the port is being bound.