diff --git a/ironic/common/states.py b/ironic/common/states.py index e09cc9bcb5..3f550d6bc5 100644 --- a/ironic/common/states.py +++ b/ironic/common/states.py @@ -223,7 +223,7 @@ UPDATE_ALLOWED_STATES = (DEPLOYFAIL, INSPECTING, INSPECTFAIL, INSPECTWAIT, UNRESCUEFAIL) """Transitional states in which we allow updating a node.""" -DELETE_ALLOWED_STATES = (AVAILABLE, MANAGEABLE, ENROLL, ADOPTFAIL) +DELETE_ALLOWED_STATES = (MANAGEABLE, ENROLL, ADOPTFAIL) """States in which node deletion is allowed.""" STABLE_STATES = (ENROLL, MANAGEABLE, AVAILABLE, ACTIVE, ERROR, RESCUE) diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 4b7b1524c5..47fd71b53c 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -2267,15 +2267,18 @@ class ConductorManager(base_manager.BaseConductorManager): # CLEANFAIL -> MANAGEABLE # INSPECTIONFAIL -> MANAGEABLE # DEPLOYFAIL -> DELETING + delete_allowed_states = states.DELETE_ALLOWED_STATES + if CONF.conductor.allow_deleting_available_nodes: + delete_allowed_states += (states.AVAILABLE,) if (not node.maintenance and node.provision_state - not in states.DELETE_ALLOWED_STATES): + not in delete_allowed_states): msg = (_('Can not delete node "%(node)s" while it is in ' 'provision state "%(state)s". Valid provision states ' 'to perform deletion are: "%(valid_states)s", ' 'or set the node into maintenance mode') % {'node': node.uuid, 'state': node.provision_state, - 'valid_states': states.DELETE_ALLOWED_STATES}) + 'valid_states': delete_allowed_states}) raise exception.InvalidState(msg) if node.console_enabled: notify_utils.emit_console_notification( diff --git a/ironic/conf/conductor.py b/ironic/conf/conductor.py index 965e5c1415..4984073dae 100644 --- a/ironic/conf/conductor.py +++ b/ironic/conf/conductor.py @@ -194,6 +194,10 @@ opts = [ '255 characters and is case insensitive. This ' 'conductor will only manage nodes with a matching ' '"conductor_group" field set on the node.')), + cfg.BoolOpt('allow_deleting_available_nodes', + default=True, + help=_('Allow deleting nodes which are in state ' + '\'available\'. Defaults to True.')), ] diff --git a/ironic/releasenotes/notes/add-protection-for-available-nodes-25f163d69782ef63.yaml b/ironic/releasenotes/notes/add-protection-for-available-nodes-25f163d69782ef63.yaml new file mode 100644 index 0000000000..7b38229acd --- /dev/null +++ b/ironic/releasenotes/notes/add-protection-for-available-nodes-25f163d69782ef63.yaml @@ -0,0 +1,12 @@ +--- +features: + - Adds option 'allow_deleting_available_nodes' to control whether nodes in + state 'available' should be deletable (which is and stays the default). + Setting this option to False will remove 'available' from the list of + states in which nodes can be deleted from ironic. It hence provides + protection against accidental removal of nodes which are ready for + allocation (and is meant as a safeguard for the operational effort to + bring nodes into this state). For backwards compatibility reasons, the + default value for this option is True. The other states in which nodes + can be deleted from ironic ('manageable', 'enroll', and 'adoptfail') + remain unchanged. diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 836678f8b6..d4e4c2c4f7 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -5068,6 +5068,22 @@ class DestroyNodeTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): node.refresh() self.assertIsNone(node.reservation) + def test_destroy_node_protected_provision_state_available(self): + CONF.set_override('allow_deleting_available_nodes', + False, group='conductor') + self._start_service() + node = obj_utils.create_test_node(self.context, + provision_state=states.AVAILABLE) + + exc = self.assertRaises(messaging.rpc.ExpectedException, + self.service.destroy_node, + self.context, node.uuid) + # Compare true exception hidden by @messaging.expected_exceptions + self.assertEqual(exception.InvalidState, exc.exc_info[0]) + # Verify reservation was released. + node.refresh() + self.assertIsNone(node.reservation) + def test_destroy_node_protected(self): self._start_service() node = obj_utils.create_test_node(self.context,