From 06c8d72494166813e444a22c795ad856e8fcdef5 Mon Sep 17 00:00:00 2001 From: Yuriy Zveryanskyy Date: Mon, 21 Oct 2013 13:12:27 +0300 Subject: [PATCH] Don't allow deletion of associated node Checks that node is associated with instance added to method destroy_node() in db api. Exception NodeAssociated raised in case associated node. Change-Id: I52c7a0b3d33078b38460b5fd08e4cd2d4c7731ef --- ironic/api/controllers/v1/node.py | 5 +---- ironic/common/exception.py | 4 ++++ ironic/db/sqlalchemy/api.py | 3 +++ ironic/tests/api/test_nodes.py | 6 ++++++ ironic/tests/db/test_nodes.py | 5 +++++ 5 files changed, 19 insertions(+), 4 deletions(-) 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']