diff --git a/ironic_inspector/common/ironic.py b/ironic_inspector/common/ironic.py index 42256ca3a..c8ba563c3 100644 --- a/ironic_inspector/common/ironic.py +++ b/ironic_inspector/common/ironic.py @@ -27,8 +27,11 @@ CONF = cfg.CONF LOG = utils.getProcessingLogger(__name__) # See https://docs.openstack.org/ironic/latest/contributor/states.html # noqa -VALID_STATES = {'enroll', 'manageable', 'inspecting', 'inspect wait', - 'inspect failed'} +VALID_STATES = frozenset(['enroll', 'manageable', 'inspecting', 'inspect wait', + 'inspect failed']) + +# States where an instance is deployed and an admin may be doing something. +VALID_ACTIVE_STATES = frozenset(['active', 'rescue']) # 1.38 is the latest API version in the Queens release series, 10.1.0. # 1.46 is the latest API version in the Rocky release series, 11.1.0. @@ -135,11 +138,27 @@ def get_client(token=None, def check_provision_state(node): + """Sanity checks the provision state of the node. + + :param node: An API client returned node object describing + the baremetal node according to ironic's node + data model. + :returns: None if no action is to be taken, True if the + power node state should not be modified. + :raises: Error on an invalid state being detected. + """ state = node.provision_state.lower() if state not in VALID_STATES: + if (CONF.processing.permit_active_introspection + and state in VALID_ACTIVE_STATES): + # Hey, we can leave the power on! Lets return + # True to let the caller know. + return True + msg = _('Invalid provision state for introspection: ' '"%(state)s", valid states are "%(valid)s"') - raise utils.Error(msg % {'state': state, 'valid': list(VALID_STATES)}, + raise utils.Error(msg % {'state': state, + 'valid': list(VALID_STATES)}, node_info=node) diff --git a/ironic_inspector/conf/processing.py b/ironic_inspector/conf/processing.py index 4c00f467d..db86bb3fe 100644 --- a/ironic_inspector/conf/processing.py +++ b/ironic_inspector/conf/processing.py @@ -94,7 +94,15 @@ _OPTS = [ '{mac} - PXE booting MAC or "unknown".')), cfg.BoolOpt('power_off', default=True, - help=_('Whether to power off a node after introspection.')), + help=_('Whether to power off a node after introspection.' + 'Nodes in active or rescue states which submit ' + 'introspection data will be left on if the feature ' + 'is enabled via the \'permit_active_introspection\' ' + 'configuration option.')), + cfg.BoolOpt('permit_active_introspection', + default=False, + help=_('Whether to process nodes that are in running ' + 'states.')), ] diff --git a/ironic_inspector/process.py b/ironic_inspector/process.py index dddfd32be..cabd31f03 100644 --- a/ironic_inspector/process.py +++ b/ironic_inspector/process.py @@ -259,7 +259,8 @@ def _run_post_hooks(node_info, introspection_data): @node_cache.fsm_transition(istate.Events.process, reentrant=False) def _process_node(node_info, node, introspection_data): # NOTE(dtantsur): repeat the check in case something changed - ir_utils.check_provision_state(node) + keep_power_on = ir_utils.check_provision_state(node) + _run_post_hooks(node_info, introspection_data) store_introspection_data(node_info.uuid, introspection_data) @@ -271,8 +272,13 @@ def _process_node(node_info, node, introspection_data): resp = {'uuid': node.uuid} + # determine how to handle power + if keep_power_on: + power_action = False + else: + power_action = CONF.processing.power_off utils.executor().submit(_finish, node_info, ironic, introspection_data, - power_off=CONF.processing.power_off) + power_off=power_action) return resp diff --git a/ironic_inspector/test/unit/test_process.py b/ironic_inspector/test/unit/test_process.py index 66ef4998c..1e119d096 100644 --- a/ironic_inspector/test/unit/test_process.py +++ b/ironic_inspector/test/unit/test_process.py @@ -433,6 +433,30 @@ class TestProcessNode(BaseTest): post_hook_mock.assert_called_once_with(self.data, self.node_info) finished_mock.assert_called_once_with(mock.ANY, istate.Events.finish) + @mock.patch.object(example_plugin.ExampleProcessingHook, 'before_update', + autospec=True) + @mock.patch.object(node_cache.NodeInfo, 'finished', autospec=True) + def test_ok_node_active(self, finished_mock, post_hook_mock): + self.node.provision_state = 'active' + CONF.set_override('permit_active_introspection', True, 'processing') + process._process_node(self.node_info, self.node, self.data) + + self.cli.port.create.assert_any_call(node_uuid=self.uuid, + address=self.macs[0], + extra={}, + pxe_enabled=True) + self.cli.port.create.assert_any_call(node_uuid=self.uuid, + address=self.macs[1], + extra={}, + pxe_enabled=False) + + self.cli.node.set_power_state.assert_not_called() + self.assertFalse(self.cli.node.validate.called) + + post_hook_mock.assert_called_once_with(mock.ANY, self.data, + self.node_info) + finished_mock.assert_called_once_with(mock.ANY, istate.Events.finish) + def test_port_failed(self): self.cli.port.create.side_effect = ( [exceptions.Conflict()] + self.ports[1:]) diff --git a/releasenotes/notes/active-introspection-949f4a50c9d5218a.yaml b/releasenotes/notes/active-introspection-949f4a50c9d5218a.yaml new file mode 100644 index 000000000..5a6fd1064 --- /dev/null +++ b/releasenotes/notes/active-introspection-949f4a50c9d5218a.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds the capability for introspection data to be posted to the API + when a baremetal node is in ``active`` or ``rescue`` states. This + feature may be useful for data center operators who wish to update + introspection data periodically. + + To enable this feature, set ``[processing]permit_active_introspection`` + to ``True``. When this is set, the value of ``[processing]power_off`` is + overridden for nodes in ``active`` or ``rescue`` states.