diff --git a/doc/source/images/states.svg b/doc/source/images/states.svg index b2a70a23a..839ba1cb1 100644 --- a/doc/source/images/states.svg +++ b/doc/source/images/states.svg @@ -4,227 +4,240 @@ - - + + Ironic Inspector states - - + + -enrolling - -enrolling +aborting + +aborting error - -error + +error - + -enrolling->error - - -error +aborting->error + + +abort_end + + + +aborting->error + + +timeout + + + +error->error + + +abort + + + +error->error + + +error + + + +reapplying + +reapplying + + + +error->reapplying + + +reapply + + + +starting + +starting + + + +error->starting + + +start + + + +enrolling + +enrolling enrolling->error - - -timeout + + +error + + + +enrolling->error + + +timeout - + processing - -processing + +processing - -enrolling->processing - - -process - - -error->error - - -abort - - - -error->error - - -error - - - -reapplying - -reapplying - - - -error->reapplying - - -reapply - - - -starting - -starting - - - -error->starting - - -start - - - -processing->error - - -error +enrolling->processing + + +process processing->error - - -timeout + + +error + + + +processing->error + + +timeout - + finished - -finished + +finished - -processing->finished - - -finish - - -reapplying->error - - -error +processing->finished + + +finish - + reapplying->error - - -timeout + + +error + + + +reapplying->error + + +timeout - + reapplying->reapplying - - -reapply + + +reapply - + reapplying->finished - - -finish + + +finish - + starting->error - - -error + + +error - + starting->error - - -timeout - - - -starting->starting - - -start + + +timeout - + waiting - -waiting + +waiting - + starting->waiting - - -wait + + +wait - + finished->reapplying - - -reapply + + +reapply - + finished->starting - - -start + + +start - + finished->finished - - -finish + + +finish + + + +waiting->aborting + + +abort - + waiting->error - - -abort - - - -waiting->error - - -timeout + + +timeout - + waiting->processing - - -process + + +process - + waiting->starting - - -start + + +start diff --git a/ironic_inspector/introspect.py b/ironic_inspector/introspect.py index 418b02c40..a76799c05 100644 --- a/ironic_inspector/introspect.py +++ b/ironic_inspector/introspect.py @@ -56,22 +56,12 @@ def introspect(node_id, token=None): bmc_address=bmc_address, ironic=ironic) - def _handle_exceptions(fut): - try: - fut.result() - except utils.Error as exc: - # Logging has already happened in Error.__init__ - node_info.finished(error=str(exc)) - except Exception as exc: - msg = _('Unexpected exception in background introspection thread') - LOG.exception(msg, node_info=node_info) - node_info.finished(error=msg) - - future = utils.executor().submit(_background_introspect, ironic, node_info) - future.add_done_callback(_handle_exceptions) + utils.executor().submit(_background_introspect, node_info, ironic) -def _background_introspect(ironic, node_info): +@node_cache.release_lock +@node_cache.fsm_transition(istate.Events.wait) +def _background_introspect(node_info, ironic): global _LAST_INTROSPECTION_TIME LOG.debug('Attempting to acquire lock on last introspection time') @@ -85,13 +75,9 @@ def _background_introspect(ironic, node_info): _LAST_INTROSPECTION_TIME = time.time() node_info.acquire_lock() - try: - _background_introspect_locked(node_info, ironic) - finally: - node_info.release_lock() + _background_introspect_locked(node_info, ironic) -@node_cache.fsm_transition(istate.Events.wait) def _background_introspect_locked(node_info, ironic): # TODO(dtantsur): pagination macs = list(node_info.ports()) @@ -151,17 +137,10 @@ def abort(node_id, token=None): @node_cache.release_lock -@node_cache.fsm_transition(istate.Events.abort, reentrant=False) +@node_cache.fsm_event_before(istate.Events.abort) def _abort(node_info, ironic): # runs in background - if node_info.finished_at is not None: - # introspection already finished; nothing to do - LOG.info('Cannot abort introspection as it is already ' - 'finished', node_info=node_info) - node_info.release_lock() - return - # finish the introspection LOG.debug('Forcing power-off', node_info=node_info) try: ironic.node.set_power_state(node_info.uuid, 'off') @@ -169,7 +148,8 @@ def _abort(node_info, ironic): LOG.warning('Failed to power off node: %s', exc, node_info=node_info) - node_info.finished(error=_('Canceled by operator')) + node_info.finished(istate.Events.abort_end, + error=_('Canceled by operator')) # block this node from PXE Booting the introspection image try: diff --git a/ironic_inspector/introspection_state.py b/ironic_inspector/introspection_state.py index 579eb0a74..c25c336eb 100644 --- a/ironic_inspector/introspection_state.py +++ b/ironic_inspector/introspection_state.py @@ -18,6 +18,8 @@ from automaton import machines class States(object): """States of an introspection.""" + # received a request to abort the introspection + aborting = 'aborting' # received introspection data from a nonexistent node # active - the inspector performs an operation on the node enrolling = 'enrolling' @@ -44,7 +46,7 @@ class States(object): def all(cls): """Return a list of all states.""" return [cls.starting, cls.waiting, cls.processing, cls.finished, - cls.error, cls.reapplying, cls.enrolling] + cls.error, cls.reapplying, cls.enrolling, cls.aborting] class Events(object): @@ -52,6 +54,9 @@ class Events(object): # cancel a waiting node introspection # API, user abort = 'abort' + # finish the abort request + # internal + abort_end = 'abort_end' # mark an introspection failed # internal error = 'error' @@ -82,6 +87,13 @@ class Events(object): # Error transition is allowed in any state. State_space = [ + { + 'name': States.aborting, + 'next_states': { + Events.abort_end: States.error, + Events.timeout: States.error, + } + }, { 'name': States.enrolling, 'next_states': { @@ -135,7 +147,7 @@ State_space = [ { 'name': States.waiting, 'next_states': { - Events.abort: States.error, + Events.abort: States.aborting, Events.process: States.processing, Events.start: States.starting, Events.timeout: States.error, diff --git a/ironic_inspector/migrations/versions/18440d0834af_introducing_the_aborting_state.py b/ironic_inspector/migrations/versions/18440d0834af_introducing_the_aborting_state.py new file mode 100644 index 000000000..61ba6ba23 --- /dev/null +++ b/ironic_inspector/migrations/versions/18440d0834af_introducing_the_aborting_state.py @@ -0,0 +1,43 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Introducing the aborting state + +Revision ID: 18440d0834af +Revises: 882b2d84cb1b +Create Date: 2017-12-11 15:40:13.905554 + +""" + +# revision identifiers, used by Alembic. +revision = '18440d0834af' +down_revision = '882b2d84cb1b' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +from sqlalchemy import sql + +from ironic_inspector import introspection_state as istate + + +old_state = sa.Enum(*(set(istate.States.all()) - {istate.States.aborting}), + name='node_state') +new_state = sa.Enum(*istate.States.all(), name='node_state') +Node = sql.table('nodes', sql.column('state', old_state)) + + +def upgrade(): + with op.batch_alter_table('nodes') as batch_op: + batch_op.alter_column('state', existing_type=old_state, + type_=new_state) diff --git a/ironic_inspector/node_cache.py b/ironic_inspector/node_cache.py index 5919660f6..0a109e07e 100644 --- a/ironic_inspector/node_cache.py +++ b/ironic_inspector/node_cache.py @@ -211,8 +211,8 @@ class NodeInfo(object): """Update node_info.state based on a fsm.process_event(event) call. An AutomatonException triggers an error event. - If strict, node_info.finished(error=str(exc)) is called with the - AutomatonException instance and a EventError raised. + If strict, node_info.finished(istate.Events.error, error=str(exc)) + is called with the AutomatonException instance and a EventError raised. :param event: an event to process by the fsm :strict: whether to fail the introspection upon an invalid event @@ -229,8 +229,7 @@ class NodeInfo(object): if strict: LOG.error(msg, node_info=self) # assuming an error event is always possible - fsm.process_event(istate.Events.error) - self.finished(error=str(exc)) + self.finished(istate.Events.error, error=str(exc)) else: LOG.warning(msg, node_info=self) raise utils.NodeStateInvalidEvent(str(exc), node_info=self) @@ -273,19 +272,21 @@ class NodeInfo(object): db.Option(uuid=self.uuid, name=name, value=encoded).save( session) - def finished(self, error=None): - """Record status for this node. + def finished(self, event, error=None): + """Record status for this node and process a terminal transition. Also deletes look up attributes from the cache. + :param event: the event to process :param error: error message """ - self.release_lock() + self.release_lock() self.finished_at = timeutils.utcnow() self.error = error with db.ensure_transaction() as session: + self.fsm_event(event) self._commit(finished_at=self.finished_at, error=self.error) db.model_query(db.Attribute, session=session).filter_by( node_uuid=self.uuid).delete() @@ -553,7 +554,7 @@ def triggers_fsm_error_transition(errors=(Exception,), 'func': reflection.get_callable_name(func)}, node_info=node_info) # an error event should be possible from all states - node_info.fsm_event(istate.Events.error) + node_info.finished(istate.Events.error, error=str(exc)) return ret return inner return outer @@ -899,8 +900,8 @@ def clean_up(): 'while introspection in "%s" state', node_info.state, node_info=node_info) - node_info.fsm_event(istate.Events.timeout) - node_info.finished(error='Introspection timeout') + node_info.finished( + istate.Events.timeout, error='Introspection timeout') finally: node_info.release_lock() diff --git a/ironic_inspector/process.py b/ironic_inspector/process.py index ff11a9ae5..f21302f92 100644 --- a/ironic_inspector/process.py +++ b/ironic_inspector/process.py @@ -205,7 +205,7 @@ def process(introspection_data): msg = _('The following failures happened during running ' 'pre-processing hooks:\n%s') % '\n'.join(failures) if node_info is not None: - node_info.finished(error='\n'.join(failures)) + node_info.finished(istate.Events.error, error='\n'.join(failures)) _store_logs(introspection_data, node_info) raise utils.Error(msg, node_info=node_info, data=introspection_data) @@ -228,13 +228,13 @@ def process(introspection_data): node = node_info.node() except ir_utils.NotFound as exc: with excutils.save_and_reraise_exception(): - node_info.finished(error=str(exc)) + node_info.finished(istate.Events.error, error=str(exc)) _store_logs(introspection_data, node_info) try: result = _process_node(node_info, node, introspection_data) except utils.Error as exc: - node_info.finished(error=str(exc)) + node_info.finished(istate.Events.error, error=str(exc)) with excutils.save_and_reraise_exception(): _store_logs(introspection_data, node_info) except Exception as exc: @@ -242,7 +242,7 @@ def process(introspection_data): msg = _('Unexpected exception %(exc_class)s during processing: ' '%(error)s') % {'exc_class': exc.__class__.__name__, 'error': exc} - node_info.finished(error=msg) + node_info.finished(istate.Events.error, error=msg) _store_logs(introspection_data, node_info) raise utils.Error(msg, node_info=node_info, data=introspection_data, code=500) @@ -282,7 +282,7 @@ def _process_node(node_info, node, introspection_data): return resp -@node_cache.fsm_transition(istate.Events.finish) +@node_cache.triggers_fsm_error_transition() def _finish(node_info, ironic, introspection_data, power_off=True): if power_off: LOG.debug('Forcing power off of node %s', node_info.uuid) @@ -299,13 +299,12 @@ def _finish(node_info, ironic, introspection_data, power_off=True): 'its power management configuration: ' '%(exc)s') % {'node': node_info.uuid, 'exc': exc}) - node_info.finished(error=msg) raise utils.Error(msg, node_info=node_info, data=introspection_data) LOG.info('Node powered-off', node_info=node_info, data=introspection_data) - node_info.finished() + node_info.finished(istate.Events.finish) LOG.info('Introspection finished successfully', node_info=node_info, data=introspection_data) @@ -348,7 +347,7 @@ def _reapply(node_info): msg = (_('Unexpected exception %(exc_class)s while fetching ' 'unprocessed introspection data from Swift: %(error)s') % {'exc_class': exc.__class__.__name__, 'error': exc}) - node_info.finished(error=msg) + node_info.finished(istate.Events.error, error=msg) return try: @@ -357,14 +356,12 @@ def _reapply(node_info): msg = _('Encountered an exception while getting the Ironic client: ' '%s') % exc LOG.error(msg, node_info=node_info, data=introspection_data) - node_info.fsm_event(istate.Events.error) - node_info.finished(error=msg) + node_info.finished(istate.Events.error, error=msg) return try: _reapply_with_data(node_info, introspection_data) except Exception as exc: - node_info.finished(error=str(exc)) return _finish(node_info, ironic, introspection_data, diff --git a/ironic_inspector/test/unit/test_introspect.py b/ironic_inspector/test/unit/test_introspect.py index 0dfde8cb1..4c3db06a2 100644 --- a/ironic_inspector/test/unit/test_introspect.py +++ b/ironic_inspector/test/unit/test_introspect.py @@ -21,6 +21,7 @@ from oslo_config import cfg from ironic_inspector.common import ironic as ir_utils from ironic_inspector import introspect +from ironic_inspector import introspection_state as istate from ironic_inspector import node_cache from ironic_inspector.pxe_filter import base as pxe_filter from ironic_inspector.test import base as test_base @@ -135,7 +136,7 @@ class TestIntrospect(BaseTest): cli.node.set_power_state.assert_called_once_with(self.uuid, 'reboot') start_mock.return_value.finished.assert_called_once_with( - error=mock.ANY) + introspect.istate.Events.error, error=mock.ANY) self.node_info.acquire_lock.assert_called_once_with() self.node_info.release_lock.assert_called_once_with() @@ -153,7 +154,7 @@ class TestIntrospect(BaseTest): ironic=cli) self.assertFalse(cli.node.set_boot_device.called) start_mock.return_value.finished.assert_called_once_with( - error=mock.ANY) + introspect.istate.Events.error, error=mock.ANY) self.node_info.acquire_lock.assert_called_once_with() self.node_info.release_lock.assert_called_once_with() @@ -186,7 +187,8 @@ class TestIntrospect(BaseTest): introspect.introspect(self.uuid) self.node_info.ports.assert_called_once_with() - self.node_info.finished.assert_called_once_with(error=mock.ANY) + self.node_info.finished.assert_called_once_with( + introspect.istate.Events.error, error=mock.ANY) self.assertEqual(0, self.sync_filter_mock.call_count) self.assertEqual(0, cli.node.set_power_state.call_count) self.node_info.acquire_lock.assert_called_once_with() @@ -311,6 +313,10 @@ class TestAbort(BaseTest): super(TestAbort, self).setUp() self.node_info.started_at = None self.node_info.finished_at = None + # NOTE(milan): node_info.finished() is a mock; no fsm_event call, then + self.fsm_calls = [ + mock.call(istate.Events.abort, strict=False), + ] def test_ok(self, client_mock, get_mock): cli = self._prepare(client_mock) @@ -326,8 +332,9 @@ class TestAbort(BaseTest): self.node_info.acquire_lock.assert_called_once_with(blocking=False) self.sync_filter_mock.assert_called_once_with(cli) cli.node.set_power_state.assert_called_once_with(self.uuid, 'off') - self.node_info.finished.assert_called_once_with(error='Canceled ' - 'by operator') + self.node_info.finished.assert_called_once_with( + introspect.istate.Events.abort_end, error='Canceled by operator') + self.node_info.fsm_event.assert_has_calls(self.fsm_calls) def test_node_not_found(self, client_mock, get_mock): cli = self._prepare(client_mock) @@ -340,6 +347,7 @@ class TestAbort(BaseTest): self.assertEqual(0, self.sync_filter_mock.call_count) self.assertEqual(0, cli.node.set_power_state.call_count) self.assertEqual(0, self.node_info.finished.call_count) + self.assertEqual(0, self.node_info.fsm_event.call_count) def test_node_locked(self, client_mock, get_mock): cli = self._prepare(client_mock) @@ -353,19 +361,7 @@ class TestAbort(BaseTest): self.assertEqual(0, self.sync_filter_mock.call_count) self.assertEqual(0, cli.node.set_power_state.call_count) self.assertEqual(0, self.node_info.finshed.call_count) - - def test_introspection_already_finished(self, client_mock, get_mock): - cli = self._prepare(client_mock) - get_mock.return_value = self.node_info - self.node_info.acquire_lock.return_value = True - self.node_info.started_at = time.time() - self.node_info.finished_at = time.time() - - introspect.abort(self.uuid) - - self.assertEqual(0, self.sync_filter_mock.call_count) - self.assertEqual(0, cli.node.set_power_state.call_count) - self.assertEqual(0, self.node_info.finshed.call_count) + self.assertEqual(0, self.node_info.fsm_event.call_count) def test_firewall_update_exception(self, client_mock, get_mock): cli = self._prepare(client_mock) @@ -382,8 +378,9 @@ class TestAbort(BaseTest): self.node_info.acquire_lock.assert_called_once_with(blocking=False) self.sync_filter_mock.assert_called_once_with(cli) cli.node.set_power_state.assert_called_once_with(self.uuid, 'off') - self.node_info.finished.assert_called_once_with(error='Canceled ' - 'by operator') + self.node_info.finished.assert_called_once_with( + introspect.istate.Events.abort_end, error='Canceled by operator') + self.node_info.fsm_event.assert_has_calls(self.fsm_calls) def test_node_power_off_exception(self, client_mock, get_mock): cli = self._prepare(client_mock) @@ -400,5 +397,6 @@ class TestAbort(BaseTest): self.node_info.acquire_lock.assert_called_once_with(blocking=False) self.sync_filter_mock.assert_called_once_with(cli) cli.node.set_power_state.assert_called_once_with(self.uuid, 'off') - self.node_info.finished.assert_called_once_with(error='Canceled ' - 'by operator') + self.node_info.finished.assert_called_once_with( + introspect.istate.Events.abort_end, error='Canceled by operator') + self.node_info.fsm_event.assert_has_calls(self.fsm_calls) diff --git a/ironic_inspector/test/unit/test_node_cache.py b/ironic_inspector/test/unit/test_node_cache.py index 6ce9bab9b..47571ccec 100644 --- a/ironic_inspector/test/unit/test_node_cache.py +++ b/ironic_inspector/test/unit/test_node_cache.py @@ -497,7 +497,7 @@ class TestNodeInfoFinished(test_base.NodeTest): session) def test_success(self): - self.node_info.finished() + self.node_info.finished(istate.Events.finish) session = db.get_writer_session() with session.begin(): @@ -511,7 +511,7 @@ class TestNodeInfoFinished(test_base.NodeTest): session=session).all()) def test_error(self): - self.node_info.finished(error='boom') + self.node_info.finished(istate.Events.error, error='boom') self.assertEqual((datetime.datetime(1, 1, 1), 'boom'), tuple(db.model_query(db.Node.finished_at, @@ -521,7 +521,7 @@ class TestNodeInfoFinished(test_base.NodeTest): def test_release_lock(self): self.node_info.acquire_lock() - self.node_info.finished() + self.node_info.finished(istate.Events.finish) self.assertFalse(self.node_info._locked) diff --git a/ironic_inspector/test/unit/test_process.py b/ironic_inspector/test/unit/test_process.py index f7ec1e558..adb1d6043 100644 --- a/ironic_inspector/test/unit/test_process.py +++ b/ironic_inspector/test/unit/test_process.py @@ -140,7 +140,8 @@ class TestProcess(BaseProcessTest): process.process, self.data) self.cli.node.get.assert_called_once_with(self.uuid) self.assertFalse(self.process_mock.called) - self.node_info.finished.assert_called_once_with(error=mock.ANY) + self.node_info.finished.assert_called_once_with( + istate.Events.error, error=mock.ANY) def test_already_finished(self): self.node_info.finished_at = timeutils.utcnow() @@ -155,7 +156,8 @@ class TestProcess(BaseProcessTest): self.assertRaisesRegex(utils.Error, 'boom', process.process, self.data) - self.node_info.finished.assert_called_once_with(error='boom') + self.node_info.finished.assert_called_once_with( + istate.Events.error, error='boom') def test_unexpected_exception(self): self.process_mock.side_effect = RuntimeError('boom') @@ -166,6 +168,7 @@ class TestProcess(BaseProcessTest): self.assertEqual(500, ctx.exception.http_code) self.node_info.finished.assert_called_once_with( + istate.Events.error, error='Unexpected exception RuntimeError during processing: boom') def test_hook_unexpected_exceptions(self): @@ -179,7 +182,7 @@ class TestProcess(BaseProcessTest): process.process, self.data) self.node_info.finished.assert_called_once_with( - error=mock.ANY) + istate.Events.error, error=mock.ANY) error_message = self.node_info.finished.call_args[1]['error'] self.assertIn('RuntimeError', error_message) self.assertIn('boom', error_message) @@ -422,7 +425,7 @@ class TestProcessNode(BaseTest): self.assertFalse(self.cli.node.validate.called) post_hook_mock.assert_called_once_with(self.data, self.node_info) - finished_mock.assert_called_once_with(mock.ANY) + finished_mock.assert_called_once_with(mock.ANY, istate.Events.finish) def test_port_failed(self): self.cli.port.create.side_effect = ( @@ -445,9 +448,9 @@ class TestProcessNode(BaseTest): self.cli.node.set_power_state.assert_called_once_with(self.uuid, 'off') finished_mock.assert_called_once_with( - mock.ANY, + mock.ANY, istate.Events.error, error='Failed to power off node %s, check its power ' - 'management configuration: boom' % self.uuid + 'management configuration: boom' % self.uuid ) @mock.patch.object(example_plugin.ExampleProcessingHook, 'before_update') @@ -460,7 +463,8 @@ class TestProcessNode(BaseTest): self.assertTrue(post_hook_mock.called) self.assertTrue(self.cli.node.set_power_state.called) - finished_mock.assert_called_once_with(self.node_info) + finished_mock.assert_called_once_with( + self.node_info, istate.Events.finish) @mock.patch.object(node_cache.NodeInfo, 'finished', autospec=True) def test_no_power_off(self, finished_mock): @@ -468,7 +472,8 @@ class TestProcessNode(BaseTest): process._process_node(self.node_info, self.node, self.data) self.assertFalse(self.cli.node.set_power_state.called) - finished_mock.assert_called_once_with(self.node_info) + finished_mock.assert_called_once_with( + self.node_info, istate.Events.finish) @mock.patch.object(process.swift, 'SwiftAPI', autospec=True) def test_store_data(self, swift_mock): @@ -524,6 +529,7 @@ class TestReapply(BaseTest): pop_mock.return_value = node_cache.NodeInfo( uuid=self.node.uuid, started_at=self.started_at) + pop_mock.return_value.finished = mock.Mock() pop_mock.return_value.acquire_lock = mock.Mock() return func(self, pop_mock, *args, **kw) @@ -604,8 +610,7 @@ class TestReapplyNode(BaseTest): return wrapper @prepare_mocks - def test_ok(self, finished_mock, swift_mock, apply_mock, - post_hook_mock): + def test_ok(self, finished_mock, swift_mock, apply_mock, post_hook_mock): swift_name = 'inspector_data-%s' % self.uuid swift_mock.get_object.return_value = json.dumps(self.data) @@ -623,7 +628,8 @@ class TestReapplyNode(BaseTest): # assert no power operations were performed self.assertFalse(self.cli.node.set_power_state.called) - finished_mock.assert_called_once_with(self.node_info) + finished_mock.assert_called_once_with( + self.node_info, istate.Events.finish) # asserting validate_interfaces was called self.assertEqual(self.pxe_interfaces, swifted_data['interfaces']) @@ -639,9 +645,8 @@ class TestReapplyNode(BaseTest): ) @prepare_mocks - def test_get_incomming_data_exception(self, finished_mock, - swift_mock, apply_mock, - post_hook_mock): + def test_get_incomming_data_exception(self, finished_mock, swift_mock, + apply_mock, post_hook_mock): exc = Exception('Oops') expected_error = ('Unexpected exception Exception while fetching ' 'unprocessed introspection data from Swift: Oops') @@ -652,12 +657,12 @@ class TestReapplyNode(BaseTest): self.assertFalse(swift_mock.create_object.called) self.assertFalse(apply_mock.called) self.assertFalse(post_hook_mock.called) - finished_mock.assert_called_once_with(self.node_info, - expected_error) + finished_mock.assert_called_once_with( + self.node_info, istate.Events.error, error=expected_error) @prepare_mocks - def test_prehook_failure(self, finished_mock, swift_mock, - apply_mock, post_hook_mock): + def test_prehook_failure(self, finished_mock, swift_mock, apply_mock, + post_hook_mock): CONF.set_override('processing_hooks', 'example', 'processing') plugins_base._HOOKS_MGR = None @@ -676,23 +681,23 @@ class TestReapplyNode(BaseTest): 'preprocessing in hook example: %(error)s' % {'exc_class': type(exc).__name__, 'error': exc}) - finished_mock.assert_called_once_with(self.node_info, - error=exc_failure) + finished_mock.assert_called_once_with( + self.node_info, istate.Events.error, error=exc_failure) # assert _reapply ended having detected the failure self.assertFalse(swift_mock.create_object.called) self.assertFalse(apply_mock.called) self.assertFalse(post_hook_mock.called) @prepare_mocks - def test_generic_exception_creating_ports(self, finished_mock, - swift_mock, apply_mock, - post_hook_mock): + def test_generic_exception_creating_ports(self, finished_mock, swift_mock, + apply_mock, post_hook_mock): swift_mock.get_object.return_value = json.dumps(self.data) exc = Exception('Oops') self.cli.port.create.side_effect = exc self.call() - finished_mock.assert_called_once_with(self.node_info, error=str(exc)) + finished_mock.assert_called_once_with( + self.node_info, istate.Events.error, error=str(exc)) self.assertFalse(swift_mock.create_object.called) self.assertFalse(apply_mock.called) self.assertFalse(post_hook_mock.called) diff --git a/releasenotes/notes/db-status-consistency-enhancements-f97fbaccfc81a60b.yaml b/releasenotes/notes/db-status-consistency-enhancements-f97fbaccfc81a60b.yaml new file mode 100644 index 000000000..4f4594296 --- /dev/null +++ b/releasenotes/notes/db-status-consistency-enhancements-f97fbaccfc81a60b.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + A new state ``aborting`` was introduced to distinguish between the node + introspection abort precondition (being able to perform the state + transition from the ``waiting`` state) from the activities necessary to + abort an ongoing node introspection (power-off, set finished timestamp + etc.) +fixes: + - | + The ``node_info.finished(, error=)`` now updates node + state together with other status attributes in a single DB transaction.