diff options
author | Kaifeng Wang <kaifeng.w@gmail.com> | 2018-03-22 17:35:00 +0800 |
---|---|---|
committer | Kaifeng Wang <kaifeng.w@gmail.com> | 2018-04-10 11:21:46 +0800 |
commit | 6df82ee2bcc702168e80ec7c81e5573d766f9c71 (patch) | |
tree | be92ca71643c21378fcbde57d35d992a75ccdc7f /ironic | |
parent | c03dfbf2eb4eb4242c3848c40f3faf42c142e8a9 (diff) | |
download | ironic-6df82ee2bcc702168e80ec7c81e5573d766f9c71.tar.gz |
Implementation of inspect wait state
This patch provides implementations to feature of adding inspect wait state.
Changes covered in this patch:
* Added state and transitions, state diagram regenerated.
* inspector and oneview inspect interface now return INSPECTWAIT instead of
INSPECTING. Move node to inspect wait if inspect interface returns
INSPECTING or INSPECTWAIT.
* Add a timeout option to conductor, and a periodic task to check timeout
in the inspect wait state.
Story: #1725211
Task: #10630
Partial-Bug: #1725211
Change-Id: Ie76bfdad5966014a4dae826919ff5705462c743b
Diffstat (limited to 'ironic')
-rw-r--r-- | ironic/api/controllers/v1/node.py | 11 | ||||
-rw-r--r-- | ironic/api/controllers/v1/port.py | 10 | ||||
-rw-r--r-- | ironic/api/controllers/v1/portgroup.py | 9 | ||||
-rw-r--r-- | ironic/api/controllers/v1/utils.py | 9 | ||||
-rw-r--r-- | ironic/api/controllers/v1/versions.py | 5 | ||||
-rw-r--r-- | ironic/common/release_mappings.py | 2 | ||||
-rw-r--r-- | ironic/common/states.py | 30 | ||||
-rw-r--r-- | ironic/conductor/manager.py | 44 | ||||
-rw-r--r-- | ironic/conf/conductor.py | 3 | ||||
-rw-r--r-- | ironic/drivers/modules/inspector.py | 8 | ||||
-rw-r--r-- | ironic/drivers/modules/oneview/inspect.py | 4 | ||||
-rw-r--r-- | ironic/tests/unit/api/controllers/v1/test_node.py | 37 | ||||
-rw-r--r-- | ironic/tests/unit/api/controllers/v1/test_port.py | 26 | ||||
-rw-r--r-- | ironic/tests/unit/api/controllers/v1/test_portgroup.py | 32 | ||||
-rw-r--r-- | ironic/tests/unit/conductor/test_manager.py | 108 | ||||
-rw-r--r-- | ironic/tests/unit/drivers/modules/test_inspector.py | 10 |
16 files changed, 290 insertions, 58 deletions
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index c32f483fa..bd8a0fa41 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -173,6 +173,10 @@ def update_state_in_older_versions(obj): if (pecan.request.version.minor < versions.MINOR_2_AVAILABLE_STATE and obj.provision_state == ir_states.AVAILABLE): obj.provision_state = ir_states.NOSTATE + # if requested version < 1.39, convert INSPECTWAIT to INSPECTING + if (not api_utils.allow_inspect_wait_state() and + obj.provision_state == ir_states.INSPECTWAIT): + obj.provision_state = ir_states.INSPECTING class BootDeviceController(rest.RestController): @@ -1928,6 +1932,13 @@ class NodesController(rest.RestController): "is in progress.") raise wsme.exc.ClientSideError( msg % node_ident, status_code=http_client.CONFLICT) + elif (rpc_node.provision_state == ir_states.INSPECTING and + api_utils.allow_inspect_wait_state()): + msg = _('Cannot update node "%(node)s" while it is in state ' + '"%(state)s".') % {'node': rpc_node.uuid, + 'state': ir_states.INSPECTING} + raise wsme.exc.ClientSideError(msg, + status_code=http_client.CONFLICT) names = api_utils.get_patch_values(patch, '/name') if len(names): diff --git a/ironic/api/controllers/v1/port.py b/ironic/api/controllers/v1/port.py index 6f8b27492..2b4edcee6 100644 --- a/ironic/api/controllers/v1/port.py +++ b/ironic/api/controllers/v1/port.py @@ -34,6 +34,7 @@ from ironic.api import expose from ironic.common import exception from ironic.common.i18n import _ from ironic.common import policy +from ironic.common import states as ir_states from ironic import objects METRICS = metrics_utils.get_metrics_logger(__name__) @@ -668,6 +669,15 @@ class PortsController(rest.RestController): rpc_port[field] = patch_val rpc_node = objects.Node.get_by_id(context, rpc_port.node_id) + if (rpc_node.provision_state == ir_states.INSPECTING and + api_utils.allow_inspect_wait_state()): + msg = _('Cannot update port "%(port)s" on "%(node)s" while it is ' + 'in state "%(state)s".') % {'port': rpc_port.uuid, + 'node': rpc_node.uuid, + 'state': ir_states.INSPECTING} + raise wsme.exc.ClientSideError(msg, + status_code=http_client.CONFLICT) + notify_extra = {'node_uuid': rpc_node.uuid, 'portgroup_uuid': port.portgroup_uuid} notify.emit_start_notification(context, rpc_port, 'update', diff --git a/ironic/api/controllers/v1/portgroup.py b/ironic/api/controllers/v1/portgroup.py index 09c4f409c..b86559a84 100644 --- a/ironic/api/controllers/v1/portgroup.py +++ b/ironic/api/controllers/v1/portgroup.py @@ -30,6 +30,7 @@ from ironic.api import expose from ironic.common import exception from ironic.common.i18n import _ from ironic.common import policy +from ironic.common import states as ir_states from ironic import objects METRICS = metrics_utils.get_metrics_logger(__name__) @@ -543,6 +544,14 @@ class PortgroupsController(pecan.rest.RestController): rpc_portgroup[field] = patch_val rpc_node = objects.Node.get_by_id(context, rpc_portgroup.node_id) + if (rpc_node.provision_state == ir_states.INSPECTING and + api_utils.allow_inspect_wait_state()): + msg = _('Cannot update portgroup "%(portgroup)s" on node ' + '"%(node)s" while it is in state "%(state)s".') % { + 'portgroup': rpc_portgroup.uuid, 'node': rpc_node.uuid, + 'state': ir_states.INSPECTING} + raise wsme.exc.ClientSideError(msg, + status_code=http_client.CONFLICT) notify.emit_start_notification(context, rpc_portgroup, 'update', node_uuid=rpc_node.uuid) diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py index 6f6c268a5..86e4bd093 100644 --- a/ironic/api/controllers/v1/utils.py +++ b/ironic/api/controllers/v1/utils.py @@ -721,6 +721,15 @@ def allow_traits(): return pecan.request.version.minor >= versions.MINOR_37_NODE_TRAITS +def allow_inspect_wait_state(): + """Check if inspect wait is allowed for the node. + + Version 1.39 of the API adds 'inspect wait' state to substitute + 'inspecting' state during asynchronous hardware inspection. + """ + return pecan.request.version.minor >= versions.MINOR_39_INSPECT_WAIT + + def handle_post_port_like_extra_vif(p_dict): """Handle a Post request that sets .extra['vif_port_id']. diff --git a/ironic/api/controllers/v1/versions.py b/ironic/api/controllers/v1/versions.py index 7166bafb5..87be879f5 100644 --- a/ironic/api/controllers/v1/versions.py +++ b/ironic/api/controllers/v1/versions.py @@ -75,7 +75,7 @@ BASE_VERSION = 1 # v1.36: Add Ironic Python Agent version support. # v1.37: Add node traits. # v1.38: Add rescue and unrescue provision states -# Add rescue_interface to the node object +# v1.39: Add inspect wait provision state. MINOR_0_JUNO = 0 MINOR_1_INITIAL_VERSION = 1 @@ -116,6 +116,7 @@ MINOR_35_REBUILD_CONFIG_DRIVE = 35 MINOR_36_AGENT_VERSION_HEARTBEAT = 36 MINOR_37_NODE_TRAITS = 37 MINOR_38_RESCUE_INTERFACE = 38 +MINOR_39_INSPECT_WAIT = 39 # When adding another version, update: # - MINOR_MAX_VERSION @@ -123,7 +124,7 @@ MINOR_38_RESCUE_INTERFACE = 38 # explanation of what changed in the new version # - common/release_mappings.py, RELEASE_MAPPING['master']['api'] -MINOR_MAX_VERSION = MINOR_38_RESCUE_INTERFACE +MINOR_MAX_VERSION = MINOR_39_INSPECT_WAIT # String representations of the minor and maximum versions _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) diff --git a/ironic/common/release_mappings.py b/ironic/common/release_mappings.py index 3079b827b..92d07a0c8 100644 --- a/ironic/common/release_mappings.py +++ b/ironic/common/release_mappings.py @@ -100,7 +100,7 @@ RELEASE_MAPPING = { } }, 'master': { - 'api': '1.38', + 'api': '1.39', 'rpc': '1.44', 'objects': { 'Node': ['1.23'], diff --git a/ironic/common/states.py b/ironic/common/states.py index 0525f29d9..a0d26405f 100644 --- a/ironic/common/states.py +++ b/ironic/common/states.py @@ -164,13 +164,20 @@ INSPECTING = 'inspecting' """ Node is under inspection. This is the provision state used when inspection is started. A successfully -inspected node shall transition to MANAGEABLE state. +inspected node shall transition to MANAGEABLE state. For asynchronous +inspection, node shall transition to INSPECTWAIT state. """ - INSPECTFAIL = 'inspect failed' """ Node inspection failed. """ +INSPECTWAIT = 'inspect wait' +""" Node is under inspection. + +This is the provision state used when an asynchronous inspection is in +progress. A successfully inspected node shall transition to MANAGEABLE state. +""" + ADOPTING = 'adopting' """ Node is being adopted. @@ -209,8 +216,9 @@ UNRESCUEFAIL = 'unrescue failed' UNRESCUING = 'unrescuing' """ Node is being restored from rescue mode (to active state). """ -UPDATE_ALLOWED_STATES = (DEPLOYFAIL, INSPECTING, INSPECTFAIL, CLEANFAIL, ERROR, - VERIFYING, ADOPTFAIL, RESCUEFAIL, UNRESCUEFAIL) +UPDATE_ALLOWED_STATES = (DEPLOYFAIL, INSPECTING, INSPECTFAIL, INSPECTWAIT, + CLEANFAIL, ERROR, VERIFYING, ADOPTFAIL, RESCUEFAIL, + UNRESCUEFAIL) """Transitional states in which we allow updating a node.""" DELETE_ALLOWED_STATES = (AVAILABLE, MANAGEABLE, ENROLL, ADOPTFAIL) @@ -220,8 +228,8 @@ STABLE_STATES = (ENROLL, MANAGEABLE, AVAILABLE, ACTIVE, ERROR, RESCUE) """States that will not transition unless receiving a request.""" UNSTABLE_STATES = (DEPLOYING, DEPLOYWAIT, CLEANING, CLEANWAIT, VERIFYING, - DELETING, INSPECTING, ADOPTING, RESCUING, RESCUEWAIT, - UNRESCUING) + DELETING, INSPECTING, INSPECTWAIT, ADOPTING, RESCUING, + RESCUEWAIT, UNRESCUING) """States that can be changed without external request.""" ############## @@ -292,6 +300,7 @@ machine.add_transition(AVAILABLE, DEPLOYING, 'deploy') # Add inspect* states. machine.add_state(INSPECTING, target=MANAGEABLE, **watchers) machine.add_state(INSPECTFAIL, target=MANAGEABLE, **watchers) +machine.add_state(INSPECTWAIT, target=MANAGEABLE, **watchers) # Add adopt* states machine.add_state(ADOPTING, target=ACTIVE, **watchers) @@ -392,6 +401,15 @@ machine.add_transition(INSPECTING, MANAGEABLE, 'done') # Inspection may fail. machine.add_transition(INSPECTING, INSPECTFAIL, 'fail') +# Transition for asynchronous inspection +machine.add_transition(INSPECTING, INSPECTWAIT, 'wait') + +# Inspection is done +machine.add_transition(INSPECTWAIT, MANAGEABLE, 'done') + +# Inspection failed. +machine.add_transition(INSPECTWAIT, INSPECTFAIL, 'fail') + # Move the node to manageable state for any other # action. machine.add_transition(INSPECTFAIL, MANAGEABLE, 'manage') diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 95eb8e1db..8bf242078 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -170,7 +170,8 @@ class ConductorManager(base_manager.BaseConductorManager): # TODO(dtantsur): reconsider allowing changing some (but not all) # interfaces for active nodes in the future. allowed_update_states = [states.ENROLL, states.INSPECTING, - states.MANAGEABLE, states.AVAILABLE] + states.INSPECTWAIT, states.MANAGEABLE, + states.AVAILABLE] action = _("Node %(node)s can not have %(field)s " "updated unless it is in one of allowed " "(%(allowed)s) states or in maintenance mode.") @@ -2231,7 +2232,7 @@ class ConductorManager(base_manager.BaseConductorManager): registered on another port already. :raises: InvalidState if port connectivity attributes are updated while node not in a MANAGEABLE or ENROLL or - INSPECTING state or not in MAINTENANCE mode. + INSPECTING or INSPECTWAIT state or not in MAINTENANCE mode. :raises: Conflict if trying to set extra/vif_port_id or pxe_enabled=True on port which is a member of portgroup with standalone_ports_supported=False. @@ -2268,6 +2269,7 @@ class ConductorManager(base_manager.BaseConductorManager): 'physical_network'} allowed_update_states = [states.ENROLL, states.INSPECTING, + states.INSPECTWAIT, states.MANAGEABLE] if (set(port_obj.obj_what_changed()) & connectivity_attr and not (node.provision_state in allowed_update_states @@ -2312,8 +2314,8 @@ class ConductorManager(base_manager.BaseConductorManager): :raises: PortgroupMACAlreadyExists if the update is setting a MAC which is registered on another portgroup already. :raises: InvalidState if portgroup-node association is updated while - node not in a MANAGEABLE or ENROLL or INSPECTING state or not - in MAINTENANCE mode. + node not in a MANAGEABLE or ENROLL or INSPECTING or + INSPECTWAIT state or not in MAINTENANCE mode. :raises: PortgroupNotEmpty if there are ports associated with this portgroup. :raises: Conflict when trying to set standalone_ports_supported=False @@ -2332,10 +2334,11 @@ class ConductorManager(base_manager.BaseConductorManager): if 'node_id' in portgroup_obj.obj_what_changed(): # NOTE(zhenguo): If portgroup update is modifying the # portgroup-node association then node should be in - # MANAGEABLE/INSPECTING/ENROLL provisioning state or in - # maintenance mode, otherwise InvalidState is raised. + # MANAGEABLE/INSPECTING/INSPECTWAIT/ENROLL provisioning state + # or in maintenance mode, otherwise InvalidState is raised. allowed_update_states = [states.ENROLL, states.INSPECTING, + states.INSPECTWAIT, states.MANAGEABLE] if (node.provision_state not in allowed_update_states and not node.maintenance): @@ -2771,24 +2774,24 @@ class ConductorManager(base_manager.BaseConductorManager): action='inspect', node=task.node.uuid, state=task.node.provision_state) - @METRICS.timer('ConductorManager._check_inspect_timeouts') + @METRICS.timer('ConductorManager._check_inspect_wait_timeouts') @periodics.periodic(spacing=CONF.conductor.check_provision_state_interval) - def _check_inspect_timeouts(self, context): - """Periodically checks inspect_timeout and fails upon reaching it. + def _check_inspect_wait_timeouts(self, context): + """Periodically checks inspect_wait_timeout and fails upon reaching it. :param: context: request context """ - callback_timeout = CONF.conductor.inspect_timeout + callback_timeout = CONF.conductor.inspect_wait_timeout if not callback_timeout: return filters = {'reserved': False, - 'provision_state': states.INSPECTING, + 'provision_state': states.INSPECTWAIT, 'inspection_started_before': callback_timeout} sort_key = 'inspection_started_at' last_error = _("timeout reached while inspecting the node") - self._fail_if_in_state(context, filters, states.INSPECTING, + self._fail_if_in_state(context, filters, states.INSPECTWAIT, sort_key, last_error=last_error) @METRICS.timer('ConductorManager.set_target_raid_config') @@ -3484,7 +3487,7 @@ def _do_inspect_hardware(task): :param: task: a TaskManager instance with an exclusive lock on its node. :raises: HardwareInspectionFailure if driver doesn't - return the state as states.MANAGEABLE or + return the state as states.MANAGEABLE, states.INSPECTWAIT or states.INSPECTING. """ @@ -3512,7 +3515,20 @@ def _do_inspect_hardware(task): task.process_event('done') LOG.info('Successfully inspected node %(node)s', {'node': node.uuid}) - elif new_state != states.INSPECTING: + # TODO(kaifeng): remove INSPECTING support during S* cycle. + elif new_state in (states.INSPECTING, states.INSPECTWAIT): + task.process_event('wait') + if new_state == states.INSPECTING: + inspect_intf_name = task.driver.inspect.__class__.__name__ + LOG.warning('Received INSPECTING state from %(intf)s. Returning ' + 'INSPECTING from InspectInterface.inspect_hardware ' + 'is deprecated, and will cause node be moved to ' + 'INSPECTFAIL state after deprecation period. Please ' + 'return INSPECTWAIT instead if the inspection process ' + 'is asynchronous.', {'intf': inspect_intf_name}) + LOG.info('Successfully started introspection on node %(node)s', + {'node': node.uuid}) + else: error = (_("During inspection, driver returned unexpected " "state %(state)s") % {'state': new_state}) handle_failure(error) diff --git a/ironic/conf/conductor.py b/ironic/conf/conductor.py index d3174bf29..1d1175c31 100644 --- a/ironic/conf/conductor.py +++ b/ironic/conf/conductor.py @@ -122,8 +122,9 @@ opts = [ help=_('Name of the Swift container to store config drive ' 'data. Used when configdrive_use_object_store is ' 'True.')), - cfg.IntOpt('inspect_timeout', + cfg.IntOpt('inspect_wait_timeout', default=1800, + deprecated_name='inspect_timeout', help=_('Timeout (seconds) for waiting for node inspection. ' '0 - unlimited.')), cfg.BoolOpt('automated_clean', diff --git a/ironic/drivers/modules/inspector.py b/ironic/drivers/modules/inspector.py index 2642a83a6..25104ee84 100644 --- a/ironic/drivers/modules/inspector.py +++ b/ironic/drivers/modules/inspector.py @@ -119,7 +119,7 @@ class Inspector(base.InspectInterface): ironic-inspector. Results will be checked in a periodic task. :param task: a task from TaskManager. - :returns: states.INSPECTING + :returns: states.INSPECTWAIT """ LOG.debug('Starting inspection for node %(uuid)s using ' 'ironic-inspector', {'uuid': task.node.uuid}) @@ -128,13 +128,13 @@ class Inspector(base.InspectInterface): # we can release a lock as soon as possible and allow ironic-inspector # to operate on a node. eventlet.spawn_n(_start_inspection, task.node.uuid, task.context) - return states.INSPECTING + return states.INSPECTWAIT @periodics.periodic(spacing=CONF.inspector.status_check_period, enabled=CONF.inspector.enabled) def _periodic_check_result(self, manager, context): """Periodic task checking results of inspection.""" - filters = {'provision_state': states.INSPECTING} + filters = {'provision_state': states.INSPECTWAIT} node_iter = manager.iter_nodes(filters=filters) for node_uuid, driver in node_iter: @@ -171,7 +171,7 @@ def _start_inspection(node_uuid, context): def _check_status(task): """Check inspection status for node given by a task.""" node = task.node - if node.provision_state != states.INSPECTING: + if node.provision_state != states.INSPECTWAIT: return if not isinstance(task.driver.inspect, Inspector): return diff --git a/ironic/drivers/modules/oneview/inspect.py b/ironic/drivers/modules/oneview/inspect.py index c3d66e0b9..96313e59c 100644 --- a/ironic/drivers/modules/oneview/inspect.py +++ b/ironic/drivers/modules/oneview/inspect.py @@ -66,7 +66,7 @@ class OneViewInspect(inspector.Inspector): @periodics.periodic(spacing=CONF.inspector.status_check_period, enabled=CONF.inspector.enabled) def _periodic_check_result(self, manager, context): - filters = {'provision_state': states.INSPECTING} + filters = {'provision_state': states.INSPECTWAIT} node_iter = manager.iter_nodes(filters=filters) for node_uuid, driver in node_iter: @@ -87,7 +87,7 @@ class OneViewInspect(inspector.Inspector): state_after = task.node.provision_state # inspection finished - if state_before == states.INSPECTING and state_after in [ + if state_before == states.INSPECTWAIT and state_after in [ states.MANAGEABLE, states.INSPECTFAIL ]: deploy_utils.deallocate_server_hardware_from_ironic(task.node) diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py index 5f781bb0f..14c5a9141 100644 --- a/ironic/tests/unit/api/controllers/v1/test_node.py +++ b/ironic/tests/unit/api/controllers/v1/test_node.py @@ -227,6 +227,20 @@ class TestListNodes(test_api_base.BaseApiTest): headers={api_base.Version.string: '1.36'}) self.assertNotIn('traits', data) + def test_node_inspect_wait_state_between_api_versions(self): + node = obj_utils.create_test_node(self.context, + provision_state='inspect wait') + lower_version_data = self.get_json( + '/nodes/%s' % node.uuid, + headers={api_base.Version.string: '1.38'}) + self.assertEqual('inspecting', lower_version_data['provision_state']) + + higher_version_data = self.get_json( + '/nodes/%s' % node.uuid, + headers={api_base.Version.string: '1.39'}) + self.assertEqual('inspect wait', + higher_version_data['provision_state']) + def test_get_one_custom_fields(self): node = obj_utils.create_test_node(self.context, chassis_id=self.chassis.id) @@ -1641,6 +1655,29 @@ class TestPatch(test_api_base.BaseApiTest): 'op': 'remove'}]) self.assertEqual(http_client.OK, response.status_code) + def test_update_in_inspecting_not_allowed(self): + node = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + provision_state=states.INSPECTING) + self.mock_update_node.return_value = node + response = self.patch_json('/nodes/%s' % node.uuid, + [{'path': '/instance_uuid', + 'op': 'remove'}], + headers={api_base.Version.string: "1.39"}, + expect_errors=True) + self.assertEqual(http_client.CONFLICT, response.status_code) + + def test_update_in_inspecting_allowed(self): + node = obj_utils.create_test_node(self.context, + uuid=uuidutils.generate_uuid(), + provision_state=states.INSPECTING) + self.mock_update_node.return_value = node + response = self.patch_json('/nodes/%s' % node.uuid, + [{'path': '/instance_uuid', + 'op': 'remove'}], + headers={api_base.Version.string: "1.38"}) + self.assertEqual(http_client.OK, response.status_code) + def test_add_state_in_deployfail(self): node = obj_utils.create_test_node(self.context, uuid=uuidutils.generate_uuid(), diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py index c627daea4..372a30bc7 100644 --- a/ironic/tests/unit/api/controllers/v1/test_port.py +++ b/ironic/tests/unit/api/controllers/v1/test_port.py @@ -33,6 +33,7 @@ from ironic.api.controllers.v1 import port as api_port from ironic.api.controllers.v1 import utils as api_utils from ironic.api.controllers.v1 import versions from ironic.common import exception +from ironic.common import states from ironic.common import utils as common_utils from ironic.conductor import rpcapi from ironic import objects @@ -1504,6 +1505,31 @@ class TestPatch(test_api_base.BaseApiTest): self.assertEqual(internal_info, response.json['internal_info']) self.assertTrue(mock_warn.called) + def test_update_in_inspecting_not_allowed(self, mock_upd): + self.node.provision_state = states.INSPECTING + self.node.save() + response = self.patch_json('/ports/%s' % self.port.uuid, + [{'path': '/extra/foo', + 'value': 'bar', + 'op': 'add'}], + headers={api_base.Version.string: "1.39"}, + expect_errors=True) + self.assertEqual(http_client.CONFLICT, response.status_code) + self.assertFalse(mock_upd.called) + + def test_update_in_inspecting_allowed(self, mock_upd): + self.node.provision_state = states.INSPECTING + self.node.save() + extra = {'foo': 'bar'} + response = self.patch_json('/ports/%s' % self.port.uuid, + [{'path': '/extra/foo', + 'value': 'bar', + 'op': 'add'}], + headers={api_base.Version.string: "1.38"}) + self.assertEqual(http_client.OK, response.status_code) + self.assertEqual(extra, response.json['extra']) + self.assertTrue(mock_upd.called) + @mock.patch.object(rpcapi.ConductorAPI, 'create_port', autospec=True, side_effect=_rpcapi_create_port) diff --git a/ironic/tests/unit/api/controllers/v1/test_portgroup.py b/ironic/tests/unit/api/controllers/v1/test_portgroup.py index 431393810..006d433d7 100644 --- a/ironic/tests/unit/api/controllers/v1/test_portgroup.py +++ b/ironic/tests/unit/api/controllers/v1/test_portgroup.py @@ -31,6 +31,7 @@ from ironic.api.controllers.v1 import notification_utils from ironic.api.controllers.v1 import portgroup as api_portgroup from ironic.api.controllers.v1 import utils as api_utils from ironic.common import exception +from ironic.common import states from ironic.common import utils as common_utils from ironic.conductor import rpcapi from ironic import objects @@ -974,6 +975,37 @@ class TestPatch(test_api_base.BaseApiTest): self.assertTrue(response.json['error_message']) self.assertFalse(mock_upd.called) + def test_update_in_inspecting_not_allowed(self, mock_upd): + self.node.provision_state = states.INSPECTING + self.node.save() + address = 'AA:BB:CC:DD:EE:FF' + response = self.patch_json('/portgroups/%s' % self.portgroup.uuid, + [{'path': '/address', + 'value': address, + 'op': 'replace'}], + expect_errors=True, + headers={api_base.Version.string: "1.39"}) + self.assertEqual(http_client.CONFLICT, response.status_code) + self.assertFalse(mock_upd.called) + + def test_update_in_inspecting_allowed(self, mock_upd): + self.node.provision_state = states.INSPECTING + self.node.save() + address = 'AA:BB:CC:DD:EE:FF' + mock_upd.return_value = self.portgroup + mock_upd.return_value.address = address.lower() + response = self.patch_json('/portgroups/%s' % self.portgroup.uuid, + [{'path': '/address', + 'value': address, + 'op': 'replace'}], + expect_errors=True, + headers={api_base.Version.string: "1.38"}) + self.assertEqual(http_client.OK, response.status_int) + self.assertEqual(address.lower(), response.json['address']) + self.assertTrue(mock_upd.called) + kargs = mock_upd.call_args[0][1] + self.assertEqual(address.lower(), kargs.address) + @mock.patch.object(rpcapi.ConductorAPI, 'update_portgroup', autospec=True, side_effect=_rpcapi_update_portgroup) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index 3a6648547..516f86d9e 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -613,7 +613,7 @@ class UpdateNodeTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): def test_update_node_interface_in_allowed_state(self): for state in [states.ENROLL, states.MANAGEABLE, states.INSPECTING, - states.AVAILABLE]: + states.INSPECTWAIT, states.AVAILABLE]: self._test_update_node_interface_in_allowed_state(state) def test_update_node_interface_in_maintenance(self): @@ -4013,6 +4013,22 @@ class UpdatePortTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): @mock.patch.object(n_flat.FlatNetwork, 'port_changed', autospec=True) @mock.patch.object(n_flat.FlatNetwork, 'validate', autospec=True) + def test_update_port_to_node_in_inspect_wait_state(self, mock_val, + mock_pc): + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.INSPECTWAIT) + port = obj_utils.create_test_port(self.context, + node_id=node.id, + extra={'foo': 'bar'}) + port.pxe_enabled = True + self.service.update_port(self.context, port) + port.refresh() + self.assertEqual(True, port.pxe_enabled) + mock_val.assert_called_once_with(mock.ANY, mock.ANY) + mock_pc.assert_called_once_with(mock.ANY, mock.ANY, port) + + @mock.patch.object(n_flat.FlatNetwork, 'port_changed', autospec=True) + @mock.patch.object(n_flat.FlatNetwork, 'validate', autospec=True) def test_update_port_node_active_state_and_maintenance(self, mock_val, mock_pc): node = obj_utils.create_test_node(self.context, driver='fake', @@ -4640,6 +4656,32 @@ class UpdatePortgroupTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): @mock.patch.object(dbapi.IMPL, 'get_ports_by_portgroup_id') @mock.patch.object(n_flat.FlatNetwork, 'portgroup_changed', autospec=True) @mock.patch.object(n_flat.FlatNetwork, 'validate', autospec=True) + def test_update_portgroup_to_node_in_inspect_wait_state(self, mock_val, + mock_pgc, + mock_get_ports): + node = obj_utils.create_test_node(self.context, driver='fake') + portgroup = obj_utils.create_test_portgroup(self.context, + node_id=node.id, + extra={'foo': 'bar'}) + update_node = obj_utils.create_test_node( + self.context, driver='fake', + provision_state=states.INSPECTWAIT, + uuid=uuidutils.generate_uuid()) + mock_get_ports.return_value = [] + + self._start_service() + + portgroup.node_id = update_node.id + self.service.update_portgroup(self.context, portgroup) + portgroup.refresh() + self.assertEqual(update_node.id, portgroup.node_id) + mock_get_ports.assert_called_once_with(portgroup.uuid) + mock_val.assert_called_once_with(mock.ANY, mock.ANY) + mock_pgc.assert_called_once_with(mock.ANY, mock.ANY, portgroup) + + @mock.patch.object(dbapi.IMPL, 'get_ports_by_portgroup_id') + @mock.patch.object(n_flat.FlatNetwork, 'portgroup_changed', autospec=True) + @mock.patch.object(n_flat.FlatNetwork, 'validate', autospec=True) def test_update_portgroup_to_node_in_active_state_and_maintenance( self, mock_val, mock_pgc, mock_get_ports): node = obj_utils.create_test_node(self.context, driver='fake') @@ -5893,8 +5935,22 @@ class NodeInspectHardware(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): mock_inspect.return_value = states.INSPECTING manager._do_inspect_hardware(task) node.refresh() - self.assertEqual(states.INSPECTING, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertEqual(states.INSPECTWAIT, node.provision_state) + self.assertEqual(states.MANAGEABLE, node.target_provision_state) + self.assertIsNone(node.last_error) + mock_inspect.assert_called_once_with(mock.ANY) + + @mock.patch('ironic.drivers.modules.fake.FakeInspect.inspect_hardware') + def test_inspect_hardware_return_inspect_wait(self, mock_inspect): + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake', + provision_state=states.INSPECTING) + task = task_manager.TaskManager(self.context, node.uuid) + mock_inspect.return_value = states.INSPECTWAIT + manager._do_inspect_hardware(task) + node.refresh() + self.assertEqual(states.INSPECTWAIT, node.provision_state) + self.assertEqual(states.MANAGEABLE, node.target_provision_state) self.assertIsNone(node.last_error) mock_inspect.assert_called_once_with(mock.ANY) @@ -5915,17 +5971,17 @@ class NodeInspectHardware(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): mock_inspect.assert_called_once_with(mock.ANY) self.assertTrue(log_mock.error.called) - def test__check_inspect_timeouts(self): + def test__check_inspect_wait_timeouts(self): self._start_service() - CONF.set_override('inspect_timeout', 1, group='conductor') + CONF.set_override('inspect_wait_timeout', 1, group='conductor') node = obj_utils.create_test_node( self.context, driver='fake', - provision_state=states.INSPECTING, + provision_state=states.INSPECTWAIT, target_provision_state=states.MANAGEABLE, provision_updated_at=datetime.datetime(2000, 1, 1, 0, 0), inspection_started_at=datetime.datetime(2000, 1, 1, 0, 0)) - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._stop_service() node.refresh() self.assertEqual(states.INSPECTFAIL, node.provision_state) @@ -6030,26 +6086,26 @@ class NodeInspectHardware(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): @mock.patch.object(task_manager, 'acquire') @mock.patch.object(manager.ConductorManager, '_mapped_to_this_conductor') @mock.patch.object(dbapi.IMPL, 'get_nodeinfo_list') -class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, - db_base.DbTestCase): +class ManagerCheckInspectWaitTimeoutsTestCase(mgr_utils.CommonMixIn, + db_base.DbTestCase): def setUp(self): - super(ManagerCheckInspectTimeoutsTestCase, self).setUp() - self.config(inspect_timeout=300, group='conductor') + super(ManagerCheckInspectWaitTimeoutsTestCase, self).setUp() + self.config(inspect_wait_timeout=300, group='conductor') self.service = manager.ConductorManager('hostname', 'test-topic') self.service.dbapi = self.dbapi - self.node = self._create_node(provision_state=states.INSPECTING, + self.node = self._create_node(provision_state=states.INSPECTWAIT, target_provision_state=states.MANAGEABLE) self.task = self._create_task(node=self.node) self.node2 = self._create_node( - provision_state=states.INSPECTING, + provision_state=states.INSPECTWAIT, target_provision_state=states.MANAGEABLE) self.task2 = self._create_task(node=self.node2) self.filters = {'reserved': False, 'inspection_started_before': 300, - 'provision_state': states.INSPECTING} + 'provision_state': states.INSPECTWAIT} self.columns = ['uuid', 'driver'] def _assert_get_nodeinfo_args(self, get_nodeinfo_mock): @@ -6059,9 +6115,9 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, def test__check_inspect_timeouts_disabled(self, get_nodeinfo_mock, mapped_mock, acquire_mock): - self.config(inspect_timeout=0, group='conductor') + self.config(inspect_wait_timeout=0, group='conductor') - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self.assertFalse(get_nodeinfo_mock.called) self.assertFalse(mapped_mock.called) @@ -6072,7 +6128,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, get_nodeinfo_mock.return_value = self._get_nodeinfo_list_response() mapped_mock.return_value = False - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) mapped_mock.assert_called_once_with(self.node.uuid, self.node.driver) @@ -6084,7 +6140,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, mapped_mock.return_value = True acquire_mock.side_effect = self._get_acquire_side_effect(self.task) - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) mapped_mock.assert_called_once_with(self.node.uuid, self.node.driver) @@ -6101,7 +6157,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, acquire_mock.side_effect = exception.NodeNotFound(node='fake') # Exception eaten - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) mapped_mock.assert_called_once_with(self.node.uuid, @@ -6121,7 +6177,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, host='fake') # Exception eaten - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) mapped_mock.assert_called_once_with(self.node.uuid, @@ -6142,7 +6198,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, mapped_mock.return_value = True acquire_mock.side_effect = self._get_acquire_side_effect(task) - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) mapped_mock.assert_called_once_with( @@ -6155,7 +6211,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, def test__check_inspect_timeouts_to_maintenance_after_lock( self, get_nodeinfo_mock, mapped_mock, acquire_mock): task = self._create_task( - node_attrs=dict(provision_state=states.INSPECTING, + node_attrs=dict(provision_state=states.INSPECTWAIT, target_provision_state=states.MANAGEABLE, maintenance=True, uuid=self.node.uuid)) @@ -6165,7 +6221,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, acquire_mock.side_effect = ( self._get_acquire_side_effect([task, self.task2])) - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) self.assertEqual([mock.call(self.node.uuid, task.node.driver), @@ -6190,7 +6246,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, [(self.task, exception.NoFreeConductorWorker()), self.task2]) # Exception should be nuked - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) # mapped should be only called for the first node as we should @@ -6212,7 +6268,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, # Should re-raise self.assertRaises(exception.IronicException, - self.service._check_inspect_timeouts, + self.service._check_inspect_wait_timeouts, self.context) self._assert_get_nodeinfo_args(get_nodeinfo_mock) @@ -6238,7 +6294,7 @@ class ManagerCheckInspectTimeoutsTestCase(mgr_utils.CommonMixIn, acquire_mock.side_effect = ( self._get_acquire_side_effect([self.task] * 3)) - self.service._check_inspect_timeouts(self.context) + self.service._check_inspect_wait_timeouts(self.context) # Should only have ran 2. self.assertEqual([mock.call(self.node.uuid, self.node.driver)] * 2, diff --git a/ironic/tests/unit/drivers/modules/test_inspector.py b/ironic/tests/unit/drivers/modules/test_inspector.py index 010d04b95..9cfe1f644 100644 --- a/ironic/tests/unit/drivers/modules/test_inspector.py +++ b/ironic/tests/unit/drivers/modules/test_inspector.py @@ -150,7 +150,7 @@ class CommonFunctionsTestCase(BaseTestCase): class InspectHardwareTestCase(BaseTestCase): def test_ok(self, mock_client): mock_introspect = mock_client.return_value.introspect - self.assertEqual(states.INSPECTING, + self.assertEqual(states.INSPECTWAIT, self.driver.inspect.inspect_hardware(self.task)) mock_introspect.assert_called_once_with(self.node.uuid) @@ -169,7 +169,7 @@ class InspectHardwareTestCase(BaseTestCase): class CheckStatusTestCase(BaseTestCase): def setUp(self): super(CheckStatusTestCase, self).setUp() - self.node.provision_state = states.INSPECTING + self.node.provision_state = states.INSPECTWAIT def test_not_inspecting(self, mock_client): mock_get = mock_client.return_value.get_status @@ -177,6 +177,12 @@ class CheckStatusTestCase(BaseTestCase): inspector._check_status(self.task) self.assertFalse(mock_get.called) + def test_not_check_inspecting(self, mock_client): + mock_get = mock_client.return_value.get_status + self.node.provision_state = states.INSPECTING + inspector._check_status(self.task) + self.assertFalse(mock_get.called) + def test_not_inspector(self, mock_client): mock_get = mock_client.return_value.get_status self.task.driver.inspect = object() |