diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 47883740d6..e911e6df7a 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -466,10 +466,7 @@ class NodesController(rest.RestController): @wsme_pecan.wsexpose(None, unicode, status_code=204) def delete(self, node_id): - """Delete a node. - - TODO(deva): don't allow deletion of an associated node. - """ + """Delete a node.""" if self._from_chassis: raise exception.OperationNotPermitted diff --git a/ironic/common/exception.py b/ironic/common/exception.py index 1c3db8955c..cbfa9a8fb4 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -274,6 +274,10 @@ class NodeLocked(InvalidState): message = _("Node %(node)s is locked by another process.") +class NodeAssociated(InvalidState): + message = _("Node %(node)s is associated with instance %(instance)s.") + + class PortNotFound(NotFound): message = _("Port %(port)s could not be found.") diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index f361dae4eb..df89705baf 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -303,6 +303,9 @@ class Connection(api.Connection): raise exception.NodeNotFound(node=node) if node_ref['reservation'] is not None: raise exception.NodeLocked(node=node) + if node_ref['instance_uuid'] is not None: + raise exception.NodeAssociated(node=node, + instance=node_ref['instance_uuid']) # Get node ID, if an UUID was supplied. The ID is # required for deleting all ports, attached to the node. diff --git a/ironic/tests/api/test_nodes.py b/ironic/tests/api/test_nodes.py index 334126e6a0..b0fdf75feb 100644 --- a/ironic/tests/api/test_nodes.py +++ b/ironic/tests/api/test_nodes.py @@ -370,6 +370,12 @@ class TestDelete(base.FunctionalTest): expect_errors=True) self.assertEqual(response.status_int, 403) + def test_delete_associated(self): + ndict = dbutils.get_test_node(instance_uuid='fake-uuid-1234') + self.post_json('/nodes', ndict) + response = self.delete('/nodes/%s' % ndict['uuid'], expect_errors=True) + self.assertEqual(response.status_int, 409) + class TestPut(base.FunctionalTest): diff --git a/ironic/tests/db/test_nodes.py b/ironic/tests/db/test_nodes.py index 25edfe7e63..206b3a2190 100644 --- a/ironic/tests/db/test_nodes.py +++ b/ironic/tests/db/test_nodes.py @@ -155,6 +155,11 @@ class DbNodeTestCase(base.DbTestCase): self.assertRaises(exception.NodeLocked, self.dbapi.destroy_node, n['id']) + def test_destroy_associated_node(self): + n = self._create_test_node(instance_uuid='fake-uuid-1234') + self.assertRaises(exception.NodeAssociated, + self.dbapi.destroy_node, n['uuid']) + def test_ports_get_destroyed_after_destroying_a_node(self): n = self._create_test_node() node_id = n['id']