diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index ce71cf2cc3..cea2afe12b 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -2335,13 +2335,16 @@ class ConductorManager(base_manager.BaseConductorManager): with task_manager.acquire(context, port.node_id, purpose='port deletion') as task: node = task.node + vif = task.driver.network.get_current_vif(task, port) if ((node.provision_state == states.ACTIVE or node.instance_uuid) - and not node.maintenance): + and not node.maintenance and vif): msg = _("Cannot delete the port %(port)s as node " "%(node)s is active or has " - "instance UUID assigned") + "instance UUID assigned or port is bound " + "to vif %(vif)s") raise exception.InvalidState(msg % {'node': node.uuid, - 'port': port.uuid}) + 'port': port.uuid, + 'vif': vif}) port.destroy() LOG.info('Successfully deleted port %(port)s. ' 'The node associated with the port was %(node)s', diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 3a101b5d7a..44a4c3e61c 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -8150,9 +8150,10 @@ class DestroyPortTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): node = obj_utils.create_test_node(self.context, driver='fake-hardware', instance_uuid=instance_uuid, provision_state='active') - port = obj_utils.create_test_port(self.context, - node_id=node.id, - extra={'vif_port_id': 'fake-id'}) + port = obj_utils.create_test_port( + self.context, + node_id=node.id, + internal_info={'tenant_vif_port_id': 'foo'}) exc = self.assertRaises(messaging.rpc.ExpectedException, self.service.destroy_port, self.context, port) @@ -8172,18 +8173,44 @@ class DestroyPortTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): self.dbapi.get_port_by_uuid, port.uuid) - def test_destroy_port_with_instance_not_in_active(self): + def test_destroy_port_with_instance_not_in_active_port_unbound(self): instance_uuid = uuidutils.generate_uuid() node = obj_utils.create_test_node(self.context, driver='fake-hardware', instance_uuid=instance_uuid, provision_state='deploy failed') port = obj_utils.create_test_port(self.context, node_id=node.id) + self.service.destroy_port(self.context, port) + self.assertRaises(exception.PortNotFound, + self.dbapi.get_port_by_uuid, + port.uuid) + + def test_destroy_port_with_instance_not_in_active_port_bound(self): + instance_uuid = uuidutils.generate_uuid() + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + instance_uuid=instance_uuid, + provision_state='deploy failed') + port = obj_utils.create_test_port( + self.context, + node_id=node.id, + internal_info={'tenant_vif_port_id': 'foo'}) exc = self.assertRaises(messaging.rpc.ExpectedException, self.service.destroy_port, self.context, port) self.assertEqual(exception.InvalidState, exc.exc_info[0]) + def test_destroy_port_node_active_port_unbound(self): + instance_uuid = uuidutils.generate_uuid() + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + instance_uuid=instance_uuid, + provision_state='active') + port = obj_utils.create_test_port(self.context, + node_id=node.id) + self.service.destroy_port(self.context, port) + self.assertRaises(exception.PortNotFound, + self.dbapi.get_port_by_uuid, + port.uuid) + @mgr_utils.mock_record_keepalive class DestroyPortgroupTestCase(mgr_utils.ServiceSetUpMixin, diff --git a/releasenotes/notes/allow-deleting-unbound-ports-fa78069b52f099ac.yaml b/releasenotes/notes/allow-deleting-unbound-ports-fa78069b52f099ac.yaml new file mode 100644 index 0000000000..1d1bdd8b23 --- /dev/null +++ b/releasenotes/notes/allow-deleting-unbound-ports-fa78069b52f099ac.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Allows deleting unbound ports on an active node. See + https://storyboard.openstack.org/#!/story/2006385 for details.