diff --git a/ironic/conductor/deployments.py b/ironic/conductor/deployments.py new file mode 100644 index 0000000000..470bed67b6 --- /dev/null +++ b/ironic/conductor/deployments.py @@ -0,0 +1,402 @@ +# 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. + +"""Functionality related to deploying and undeploying.""" + +import tempfile + +from ironic_lib import metrics_utils +from oslo_db import exception as db_exception +from oslo_log import log +from oslo_utils import excutils +from oslo_utils import versionutils + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.common import release_mappings as versions +from ironic.common import states +from ironic.common import swift +from ironic.conductor import notification_utils as notify_utils +from ironic.conductor import steps as conductor_steps +from ironic.conductor import task_manager +from ironic.conductor import utils +from ironic.conf import CONF +from ironic.objects import fields + +LOG = log.getLogger(__name__) + +METRICS = metrics_utils.get_metrics_logger(__name__) + +# NOTE(rloo) This is used to keep track of deprecation warnings that have +# already been issued for deploy drivers that do not use deploy steps. +_SEEN_NO_DEPLOY_STEP_DEPRECATIONS = set() + + +@METRICS.timer('do_node_deploy') +@task_manager.require_exclusive_lock +def do_node_deploy(task, conductor_id=None, configdrive=None): + """Prepare the environment and deploy a node.""" + node = task.node + + try: + if configdrive: + if isinstance(configdrive, dict): + configdrive = utils.build_configdrive(node, configdrive) + _store_configdrive(node, configdrive) + except (exception.SwiftOperationError, exception.ConfigInvalid) as e: + with excutils.save_and_reraise_exception(): + utils.deploying_error_handler( + task, + ('Error while uploading the configdrive for %(node)s ' + 'to Swift') % {'node': node.uuid}, + _('Failed to upload the configdrive to Swift. ' + 'Error: %s') % e, + clean_up=False) + except db_exception.DBDataError as e: + with excutils.save_and_reraise_exception(): + # NOTE(hshiina): This error happens when the configdrive is + # too large. Remove the configdrive from the + # object to update DB successfully in handling + # the failure. + node.obj_reset_changes() + utils.deploying_error_handler( + task, + ('Error while storing the configdrive for %(node)s into ' + 'the database: %(err)s') % {'node': node.uuid, 'err': e}, + _("Failed to store the configdrive in the database. " + "%s") % e, + clean_up=False) + except Exception as e: + with excutils.save_and_reraise_exception(): + utils.deploying_error_handler( + task, + ('Unexpected error while preparing the configdrive for ' + 'node %(node)s') % {'node': node.uuid}, + _("Failed to prepare the configdrive. Exception: %s") % e, + traceback=True, clean_up=False) + + try: + task.driver.deploy.prepare(task) + except exception.IronicException as e: + with excutils.save_and_reraise_exception(): + utils.deploying_error_handler( + task, + ('Error while preparing to deploy to node %(node)s: ' + '%(err)s') % {'node': node.uuid, 'err': e}, + _("Failed to prepare to deploy: %s") % e, + clean_up=False) + except Exception as e: + with excutils.save_and_reraise_exception(): + utils.deploying_error_handler( + task, + ('Unexpected error while preparing to deploy to node ' + '%(node)s') % {'node': node.uuid}, + _("Failed to prepare to deploy. Exception: %s") % e, + traceback=True, clean_up=False) + + try: + # This gets the deploy steps (if any) and puts them in the node's + # driver_internal_info['deploy_steps']. + conductor_steps.set_node_deployment_steps(task) + except exception.InstanceDeployFailure as e: + with excutils.save_and_reraise_exception(): + utils.deploying_error_handler( + task, + 'Error while getting deploy steps; cannot deploy to node ' + '%(node)s. Error: %(err)s' % {'node': node.uuid, 'err': e}, + _("Cannot get deploy steps; failed to deploy: %s") % e) + + steps = node.driver_internal_info.get('deploy_steps', []) + + new_rpc_version = True + release_ver = versions.RELEASE_MAPPING.get(CONF.pin_release_version) + if release_ver: + new_rpc_version = versionutils.is_compatible('1.45', + release_ver['rpc']) + + if not steps or not new_rpc_version: + # TODO(rloo): This if.. (and the above code wrt rpc version) + # can be deleted after the deprecation period when we no + # longer support drivers with no deploy steps. + # Note that after the deprecation period, there needs to be at least + # one deploy step. If none, the deployment fails. + + if steps: + info = node.driver_internal_info + info.pop('deploy_steps') + node.driver_internal_info = info + node.save() + + # We go back to using the old way, if: + # - out-of-tree driver hasn't yet converted to using deploy steps, or + # - we're in the middle of a rolling upgrade. This is to prevent the + # corner case of having new conductors with old conductors, and + # a node is deployed with a new conductor (via deploy steps), but + # after the deploy_wait, the node gets handled by an old conductor. + # To avoid this, we need to wait until all the conductors are new, + # signalled by the RPC API version being '1.45'. + _old_rest_of_do_node_deploy(task, conductor_id, not steps) + else: + do_next_deploy_step(task, 0, conductor_id) + + +def _old_rest_of_do_node_deploy(task, conductor_id, no_deploy_steps): + """The rest of the do_node_deploy() if not using deploy steps. + + To support out-of-tree drivers that have not yet migrated to using + deploy steps. + + :param no_deploy_steps: Boolean; True if there are no deploy steps. + """ + # TODO(rloo): This method can be deleted after the deprecation period + # for supporting drivers with no deploy steps. + + if no_deploy_steps: + deploy_driver_name = task.driver.deploy.__class__.__name__ + if deploy_driver_name not in _SEEN_NO_DEPLOY_STEP_DEPRECATIONS: + LOG.warning('Deploy driver %s does not support deploy steps; this ' + 'will be required starting with the Stein release.', + deploy_driver_name) + _SEEN_NO_DEPLOY_STEP_DEPRECATIONS.add(deploy_driver_name) + + node = task.node + try: + new_state = task.driver.deploy.deploy(task) + except exception.IronicException as e: + with excutils.save_and_reraise_exception(): + utils.deploying_error_handler( + task, + ('Error in deploy of node %(node)s: %(err)s' % + {'node': node.uuid, 'err': e}), + _("Failed to deploy: %s") % e) + except Exception as e: + with excutils.save_and_reraise_exception(): + utils.deploying_error_handler( + task, + ('Unexpected error while deploying node %(node)s' % + {'node': node.uuid}), + _("Failed to deploy. Exception: %s") % e, + traceback=True) + + # Update conductor_affinity to reference this conductor's ID + # since there may be local persistent state + node.conductor_affinity = conductor_id + + # NOTE(deva): Some drivers may return states.DEPLOYWAIT + # eg. if they are waiting for a callback + if new_state == states.DEPLOYDONE: + _start_console_in_deploy(task) + task.process_event('done') + LOG.info('Successfully deployed node %(node)s with ' + 'instance %(instance)s.', + {'node': node.uuid, 'instance': node.instance_uuid}) + elif new_state == states.DEPLOYWAIT: + task.process_event('wait') + else: + LOG.error('Unexpected state %(state)s returned while ' + 'deploying node %(node)s.', + {'state': new_state, 'node': node.uuid}) + node.save() + + +@task_manager.require_exclusive_lock +def do_next_deploy_step(task, step_index, conductor_id): + """Do deployment, starting from the specified deploy step. + + :param task: a TaskManager instance with an exclusive lock + :param step_index: The first deploy step in the list to execute. This + is the index (from 0) into the list of deploy steps in the node's + driver_internal_info['deploy_steps']. Is None if there are no steps + to execute. + """ + node = task.node + if step_index is None: + steps = [] + else: + steps = node.driver_internal_info['deploy_steps'][step_index:] + + LOG.info('Executing %(state)s on node %(node)s, remaining steps: ' + '%(steps)s', {'node': node.uuid, 'steps': steps, + 'state': node.provision_state}) + + # Execute each step until we hit an async step or run out of steps + for ind, step in enumerate(steps): + # Save which step we're about to start so we can restart + # if necessary + node.deploy_step = step + driver_internal_info = node.driver_internal_info + driver_internal_info['deploy_step_index'] = step_index + ind + node.driver_internal_info = driver_internal_info + node.save() + interface = getattr(task.driver, step.get('interface')) + LOG.info('Executing %(step)s on node %(node)s', + {'step': step, 'node': node.uuid}) + try: + result = interface.execute_deploy_step(task, step) + except exception.IronicException as e: + if isinstance(e, exception.AgentConnectionFailed): + if task.node.driver_internal_info.get('deployment_reboot'): + LOG.info('Agent is not yet running on node %(node)s after ' + 'deployment reboot, waiting for agent to come up ' + 'to run next deploy step %(step)s.', + {'node': node.uuid, 'step': step}) + driver_internal_info['skip_current_deploy_step'] = False + node.driver_internal_info = driver_internal_info + task.process_event('wait') + return + log_msg = ('Node %(node)s failed deploy step %(step)s. Error: ' + '%(err)s' % + {'node': node.uuid, 'step': node.deploy_step, 'err': e}) + utils.deploying_error_handler( + task, log_msg, + _("Failed to deploy: %s") % node.deploy_step) + return + except Exception as e: + log_msg = ('Node %(node)s failed deploy step %(step)s with ' + 'unexpected error: %(err)s' % + {'node': node.uuid, 'step': node.deploy_step, 'err': e}) + utils.deploying_error_handler( + task, log_msg, + _("Failed to deploy. Exception: %s") % e, traceback=True) + return + + if ind == 0: + # We've done the very first deploy step. + # Update conductor_affinity to reference this conductor's ID + # since there may be local persistent state + node.conductor_affinity = conductor_id + node.save() + + # Check if the step is done or not. The step should return + # states.DEPLOYWAIT if the step is still being executed, or + # None if the step is done. + # NOTE(deva): Some drivers may return states.DEPLOYWAIT + # eg. if they are waiting for a callback + if result == states.DEPLOYWAIT: + # Kill this worker, the async step will make an RPC call to + # continue_node_deploy() to continue deploying + LOG.info('Deploy step %(step)s on node %(node)s being ' + 'executed asynchronously, waiting for driver.', + {'node': node.uuid, 'step': step}) + task.process_event('wait') + return + elif result is not None: + # NOTE(rloo): This is an internal/dev error; shouldn't happen. + log_msg = (_('While executing deploy step %(step)s on node ' + '%(node)s, step returned unexpected state: %(val)s') + % {'step': step, 'node': node.uuid, 'val': result}) + utils.deploying_error_handler( + task, log_msg, + _("Failed to deploy: %s") % node.deploy_step) + return + + LOG.info('Node %(node)s finished deploy step %(step)s', + {'node': node.uuid, 'step': step}) + + # Finished executing the steps. Clear deploy_step. + node.deploy_step = None + driver_internal_info = node.driver_internal_info + driver_internal_info['deploy_steps'] = None + driver_internal_info.pop('deploy_step_index', None) + driver_internal_info.pop('deployment_reboot', None) + driver_internal_info.pop('deployment_polling', None) + # Remove the agent_url cached from the deployment. + driver_internal_info.pop('agent_url', None) + node.driver_internal_info = driver_internal_info + node.save() + + _start_console_in_deploy(task) + + task.process_event('done') + LOG.info('Successfully deployed node %(node)s with ' + 'instance %(instance)s.', + {'node': node.uuid, 'instance': node.instance_uuid}) + + +def _get_configdrive_obj_name(node): + """Generate the object name for the config drive.""" + return 'configdrive-%s' % node.uuid + + +def _store_configdrive(node, configdrive): + """Handle the storage of the config drive. + + If configured, the config drive data are uploaded to a swift endpoint. + The Node's instance_info is updated to include either the temporary + Swift URL from the upload, or if no upload, the actual config drive data. + + :param node: an Ironic node object. + :param configdrive: A gzipped and base64 encoded configdrive. + :raises: SwiftOperationError if an error occur when uploading the + config drive to the swift endpoint. + :raises: ConfigInvalid if required keystone authorization credentials + with swift are missing. + + + """ + if CONF.deploy.configdrive_use_object_store: + # NOTE(lucasagomes): No reason to use a different timeout than + # the one used for deploying the node + timeout = (CONF.conductor.configdrive_swift_temp_url_duration + or CONF.conductor.deploy_callback_timeout + # The documented default in ironic.conf.conductor + or 1800) + container = CONF.conductor.configdrive_swift_container + object_name = _get_configdrive_obj_name(node) + + object_headers = {'X-Delete-After': str(timeout)} + + with tempfile.NamedTemporaryFile(dir=CONF.tempdir) as fileobj: + fileobj.write(configdrive) + fileobj.flush() + + swift_api = swift.SwiftAPI() + swift_api.create_object(container, object_name, fileobj.name, + object_headers=object_headers) + configdrive = swift_api.get_temp_url(container, object_name, + timeout) + + i_info = node.instance_info + i_info['configdrive'] = configdrive + node.instance_info = i_info + node.save() + + +def _start_console_in_deploy(task): + """Start console at the end of deployment. + + Console is stopped at tearing down not to be exposed to an instance user. + Then, restart at deployment. + + :param task: a TaskManager instance with an exclusive lock + """ + + if not task.node.console_enabled: + return + + notify_utils.emit_console_notification( + task, 'console_restore', fields.NotificationStatus.START) + try: + task.driver.console.start_console(task) + except Exception as err: + msg = (_('Failed to start console while deploying the ' + 'node %(node)s: %(err)s.') % {'node': task.node.uuid, + 'err': err}) + LOG.error(msg) + task.node.last_error = msg + task.node.console_enabled = False + task.node.save() + notify_utils.emit_console_notification( + task, 'console_restore', fields.NotificationStatus.ERROR) + else: + notify_utils.emit_console_notification( + task, 'console_restore', fields.NotificationStatus.END) diff --git a/ironic/conductor/manager.py b/ironic/conductor/manager.py index 509fb2af38..11affcd764 100644 --- a/ironic/conductor/manager.py +++ b/ironic/conductor/manager.py @@ -43,18 +43,15 @@ notifying Neutron of a change, etc. import collections import datetime import queue -import tempfile import eventlet from futurist import periodics from futurist import waiters from ironic_lib import metrics_utils -from oslo_db import exception as db_exception from oslo_log import log import oslo_messaging as messaging from oslo_utils import excutils from oslo_utils import uuidutils -from oslo_utils import versionutils from ironic.common import driver_factory from ironic.common import exception @@ -64,11 +61,10 @@ from ironic.common.i18n import _ from ironic.common import images from ironic.common import network from ironic.common import nova -from ironic.common import release_mappings as versions from ironic.common import states -from ironic.common import swift from ironic.conductor import allocations from ironic.conductor import base_manager +from ironic.conductor import deployments from ironic.conductor import notification_utils as notify_utils from ironic.conductor import steps as conductor_steps from ironic.conductor import task_manager @@ -87,10 +83,6 @@ METRICS = metrics_utils.get_metrics_logger(__name__) SYNC_EXCLUDED_STATES = (states.DEPLOYWAIT, states.CLEANWAIT, states.ENROLL) -# NOTE(rloo) This is used to keep track of deprecation warnings that have -# already been issued for deploy drivers that do not use deploy steps. -_SEEN_NO_DEPLOY_STEP_DEPRECATIONS = set() - class ConductorManager(base_manager.BaseConductorManager): """Ironic Conductor manager main class.""" @@ -892,18 +884,14 @@ class ConductorManager(base_manager.BaseConductorManager): task.process_event( event, callback=self._spawn_worker, - call_args=(do_node_deploy, task, self.conductor.id, - configdrive), + call_args=(deployments.do_node_deploy, task, + self.conductor.id, configdrive), err_handler=utils.provisioning_error_handler) except exception.InvalidState: raise exception.InvalidStateRequested( action=event, node=task.node.uuid, state=task.node.provision_state) - def _get_node_next_deploy_steps(self, task, skip_current_step=True): - return self._get_node_next_steps(task, 'deploy', - skip_current_step=skip_current_step) - @METRICS.timer('ConductorManager.continue_node_deploy') def continue_node_deploy(self, context, node_id): """RPC method to continue deploying a node. @@ -954,7 +942,7 @@ class ConductorManager(base_manager.BaseConductorManager): node.driver_internal_info = info node.save() - next_step_index = self._get_node_next_deploy_steps( + next_step_index = utils.get_node_next_deploy_steps( task, skip_current_step=skip_current_step) # TODO(rloo): When deprecation period is over and node is in @@ -966,7 +954,7 @@ class ConductorManager(base_manager.BaseConductorManager): task.node) task.spawn_after( self._spawn_worker, - _do_next_deploy_step, + deployments.do_next_deploy_step, task, next_step_index, self.conductor.id) @METRICS.timer('ConductorManager.do_node_tear_down') @@ -1100,62 +1088,6 @@ class ConductorManager(base_manager.BaseConductorManager): task.process_event('clean') self._do_node_clean(task) - def _get_node_next_steps(self, task, step_type, - skip_current_step=True): - """Get the task's node's next steps. - - This determines what the next (remaining) steps are, and - returns the index into the steps list that corresponds to the - next step. The remaining steps are determined as follows: - - * If no steps have been started yet, all the steps - must be executed - * If skip_current_step is False, the remaining steps start - with the current step. Otherwise, the remaining steps - start with the step after the current one. - - All the steps are in node.driver_internal_info['_steps']. - node._step is the current step that was just executed - (or None, {} if no steps have been executed yet). - node.driver_internal_info['_step_index'] is the index - index into the steps list (or None, doesn't exist if no steps have - been executed yet) and corresponds to node._step. - - :param task: A TaskManager object - :param step_type: The type of steps to process: 'clean' or 'deploy'. - :param skip_current_step: True to skip the current step; False to - include it. - :returns: index of the next step; None if there are none to execute. - - """ - valid_types = set(['clean', 'deploy']) - if step_type not in valid_types: - # NOTE(rloo): No need to i18n this, since this would be a - # developer error; it isn't user-facing. - raise exception.Invalid( - 'step_type must be one of %(valid)s, not %(step)s' - % {'valid': valid_types, 'step': step_type}) - node = task.node - if not getattr(node, '%s_step' % step_type): - # first time through, all steps need to be done. Return the - # index of the first step in the list. - return 0 - - ind = node.driver_internal_info.get('%s_step_index' % step_type) - if ind is None: - return None - - if skip_current_step: - ind += 1 - if ind >= len(node.driver_internal_info['%s_steps' % step_type]): - # no steps left to do - ind = None - return ind - - def _get_node_next_clean_steps(self, task, skip_current_step=True): - return self._get_node_next_steps(task, 'clean', - skip_current_step=skip_current_step) - @METRICS.timer('ConductorManager.do_node_clean') @messaging.expected_exceptions(exception.InvalidParameterValue, exception.InvalidStateRequested, @@ -1276,7 +1208,7 @@ class ConductorManager(base_manager.BaseConductorManager): node.driver_internal_info = info node.save() - next_step_index = self._get_node_next_clean_steps( + next_step_index = utils.get_node_next_clean_steps( task, skip_current_step=skip_current_step) # If this isn't the final clean step in the cleaning operation @@ -3680,367 +3612,6 @@ def get_vendor_passthru_metadata(route_dict): return d -def _get_configdrive_obj_name(node): - """Generate the object name for the config drive.""" - return 'configdrive-%s' % node.uuid - - -def _store_configdrive(node, configdrive): - """Handle the storage of the config drive. - - If configured, the config drive data are uploaded to a swift endpoint. - The Node's instance_info is updated to include either the temporary - Swift URL from the upload, or if no upload, the actual config drive data. - - :param node: an Ironic node object. - :param configdrive: A gzipped and base64 encoded configdrive. - :raises: SwiftOperationError if an error occur when uploading the - config drive to the swift endpoint. - :raises: ConfigInvalid if required keystone authorization credentials - with swift are missing. - - - """ - if CONF.deploy.configdrive_use_object_store: - # NOTE(lucasagomes): No reason to use a different timeout than - # the one used for deploying the node - timeout = (CONF.conductor.configdrive_swift_temp_url_duration - or CONF.conductor.deploy_callback_timeout - # The documented default in ironic.conf.conductor - or 1800) - container = CONF.conductor.configdrive_swift_container - object_name = _get_configdrive_obj_name(node) - - object_headers = {'X-Delete-After': str(timeout)} - - with tempfile.NamedTemporaryFile(dir=CONF.tempdir) as fileobj: - fileobj.write(configdrive) - fileobj.flush() - - swift_api = swift.SwiftAPI() - swift_api.create_object(container, object_name, fileobj.name, - object_headers=object_headers) - configdrive = swift_api.get_temp_url(container, object_name, - timeout) - - i_info = node.instance_info - i_info['configdrive'] = configdrive - node.instance_info = i_info - node.save() - - -@METRICS.timer('do_node_deploy') -@task_manager.require_exclusive_lock -def do_node_deploy(task, conductor_id=None, configdrive=None): - """Prepare the environment and deploy a node.""" - node = task.node - - try: - if configdrive: - if isinstance(configdrive, dict): - configdrive = utils.build_configdrive(node, configdrive) - _store_configdrive(node, configdrive) - except (exception.SwiftOperationError, exception.ConfigInvalid) as e: - with excutils.save_and_reraise_exception(): - utils.deploying_error_handler( - task, - ('Error while uploading the configdrive for %(node)s ' - 'to Swift') % {'node': node.uuid}, - _('Failed to upload the configdrive to Swift. ' - 'Error: %s') % e, - clean_up=False) - except db_exception.DBDataError as e: - with excutils.save_and_reraise_exception(): - # NOTE(hshiina): This error happens when the configdrive is - # too large. Remove the configdrive from the - # object to update DB successfully in handling - # the failure. - node.obj_reset_changes() - utils.deploying_error_handler( - task, - ('Error while storing the configdrive for %(node)s into ' - 'the database: %(err)s') % {'node': node.uuid, 'err': e}, - _("Failed to store the configdrive in the database. " - "%s") % e, - clean_up=False) - except Exception as e: - with excutils.save_and_reraise_exception(): - utils.deploying_error_handler( - task, - ('Unexpected error while preparing the configdrive for ' - 'node %(node)s') % {'node': node.uuid}, - _("Failed to prepare the configdrive. Exception: %s") % e, - traceback=True, clean_up=False) - - try: - task.driver.deploy.prepare(task) - except exception.IronicException as e: - with excutils.save_and_reraise_exception(): - utils.deploying_error_handler( - task, - ('Error while preparing to deploy to node %(node)s: ' - '%(err)s') % {'node': node.uuid, 'err': e}, - _("Failed to prepare to deploy: %s") % e, - clean_up=False) - except Exception as e: - with excutils.save_and_reraise_exception(): - utils.deploying_error_handler( - task, - ('Unexpected error while preparing to deploy to node ' - '%(node)s') % {'node': node.uuid}, - _("Failed to prepare to deploy. Exception: %s") % e, - traceback=True, clean_up=False) - - try: - # This gets the deploy steps (if any) and puts them in the node's - # driver_internal_info['deploy_steps']. - conductor_steps.set_node_deployment_steps(task) - except exception.InstanceDeployFailure as e: - with excutils.save_and_reraise_exception(): - utils.deploying_error_handler( - task, - 'Error while getting deploy steps; cannot deploy to node ' - '%(node)s. Error: %(err)s' % {'node': node.uuid, 'err': e}, - _("Cannot get deploy steps; failed to deploy: %s") % e) - - steps = node.driver_internal_info.get('deploy_steps', []) - - new_rpc_version = True - release_ver = versions.RELEASE_MAPPING.get(CONF.pin_release_version) - if release_ver: - new_rpc_version = versionutils.is_compatible('1.45', - release_ver['rpc']) - - if not steps or not new_rpc_version: - # TODO(rloo): This if.. (and the above code wrt rpc version) - # can be deleted after the deprecation period when we no - # longer support drivers with no deploy steps. - # Note that after the deprecation period, there needs to be at least - # one deploy step. If none, the deployment fails. - - if steps: - info = node.driver_internal_info - info.pop('deploy_steps') - node.driver_internal_info = info - node.save() - - # We go back to using the old way, if: - # - out-of-tree driver hasn't yet converted to using deploy steps, or - # - we're in the middle of a rolling upgrade. This is to prevent the - # corner case of having new conductors with old conductors, and - # a node is deployed with a new conductor (via deploy steps), but - # after the deploy_wait, the node gets handled by an old conductor. - # To avoid this, we need to wait until all the conductors are new, - # signalled by the RPC API version being '1.45'. - _old_rest_of_do_node_deploy(task, conductor_id, not steps) - else: - _do_next_deploy_step(task, 0, conductor_id) - - -def _old_rest_of_do_node_deploy(task, conductor_id, no_deploy_steps): - """The rest of the do_node_deploy() if not using deploy steps. - - To support out-of-tree drivers that have not yet migrated to using - deploy steps. - - :param no_deploy_steps: Boolean; True if there are no deploy steps. - """ - # TODO(rloo): This method can be deleted after the deprecation period - # for supporting drivers with no deploy steps. - - if no_deploy_steps: - deploy_driver_name = task.driver.deploy.__class__.__name__ - if deploy_driver_name not in _SEEN_NO_DEPLOY_STEP_DEPRECATIONS: - LOG.warning('Deploy driver %s does not support deploy steps; this ' - 'will be required starting with the Stein release.', - deploy_driver_name) - _SEEN_NO_DEPLOY_STEP_DEPRECATIONS.add(deploy_driver_name) - - node = task.node - try: - new_state = task.driver.deploy.deploy(task) - except exception.IronicException as e: - with excutils.save_and_reraise_exception(): - utils.deploying_error_handler( - task, - ('Error in deploy of node %(node)s: %(err)s' % - {'node': node.uuid, 'err': e}), - _("Failed to deploy: %s") % e) - except Exception as e: - with excutils.save_and_reraise_exception(): - utils.deploying_error_handler( - task, - ('Unexpected error while deploying node %(node)s' % - {'node': node.uuid}), - _("Failed to deploy. Exception: %s") % e, - traceback=True) - - # Update conductor_affinity to reference this conductor's ID - # since there may be local persistent state - node.conductor_affinity = conductor_id - - # NOTE(deva): Some drivers may return states.DEPLOYWAIT - # eg. if they are waiting for a callback - if new_state == states.DEPLOYDONE: - _start_console_in_deploy(task) - task.process_event('done') - LOG.info('Successfully deployed node %(node)s with ' - 'instance %(instance)s.', - {'node': node.uuid, 'instance': node.instance_uuid}) - elif new_state == states.DEPLOYWAIT: - task.process_event('wait') - else: - LOG.error('Unexpected state %(state)s returned while ' - 'deploying node %(node)s.', - {'state': new_state, 'node': node.uuid}) - node.save() - - -@task_manager.require_exclusive_lock -def _do_next_deploy_step(task, step_index, conductor_id): - """Do deployment, starting from the specified deploy step. - - :param task: a TaskManager instance with an exclusive lock - :param step_index: The first deploy step in the list to execute. This - is the index (from 0) into the list of deploy steps in the node's - driver_internal_info['deploy_steps']. Is None if there are no steps - to execute. - """ - node = task.node - if step_index is None: - steps = [] - else: - steps = node.driver_internal_info['deploy_steps'][step_index:] - - LOG.info('Executing %(state)s on node %(node)s, remaining steps: ' - '%(steps)s', {'node': node.uuid, 'steps': steps, - 'state': node.provision_state}) - - # Execute each step until we hit an async step or run out of steps - for ind, step in enumerate(steps): - # Save which step we're about to start so we can restart - # if necessary - node.deploy_step = step - driver_internal_info = node.driver_internal_info - driver_internal_info['deploy_step_index'] = step_index + ind - node.driver_internal_info = driver_internal_info - node.save() - interface = getattr(task.driver, step.get('interface')) - LOG.info('Executing %(step)s on node %(node)s', - {'step': step, 'node': node.uuid}) - try: - result = interface.execute_deploy_step(task, step) - except exception.IronicException as e: - if isinstance(e, exception.AgentConnectionFailed): - if task.node.driver_internal_info.get('deployment_reboot'): - LOG.info('Agent is not yet running on node %(node)s after ' - 'deployment reboot, waiting for agent to come up ' - 'to run next deploy step %(step)s.', - {'node': node.uuid, 'step': step}) - driver_internal_info['skip_current_deploy_step'] = False - node.driver_internal_info = driver_internal_info - task.process_event('wait') - return - log_msg = ('Node %(node)s failed deploy step %(step)s. Error: ' - '%(err)s' % - {'node': node.uuid, 'step': node.deploy_step, 'err': e}) - utils.deploying_error_handler( - task, log_msg, - _("Failed to deploy: %s") % node.deploy_step) - return - except Exception as e: - log_msg = ('Node %(node)s failed deploy step %(step)s with ' - 'unexpected error: %(err)s' % - {'node': node.uuid, 'step': node.deploy_step, 'err': e}) - utils.deploying_error_handler( - task, log_msg, - _("Failed to deploy. Exception: %s") % e, traceback=True) - return - - if ind == 0: - # We've done the very first deploy step. - # Update conductor_affinity to reference this conductor's ID - # since there may be local persistent state - node.conductor_affinity = conductor_id - node.save() - - # Check if the step is done or not. The step should return - # states.DEPLOYWAIT if the step is still being executed, or - # None if the step is done. - # NOTE(deva): Some drivers may return states.DEPLOYWAIT - # eg. if they are waiting for a callback - if result == states.DEPLOYWAIT: - # Kill this worker, the async step will make an RPC call to - # continue_node_deploy() to continue deploying - LOG.info('Deploy step %(step)s on node %(node)s being ' - 'executed asynchronously, waiting for driver.', - {'node': node.uuid, 'step': step}) - task.process_event('wait') - return - elif result is not None: - # NOTE(rloo): This is an internal/dev error; shouldn't happen. - log_msg = (_('While executing deploy step %(step)s on node ' - '%(node)s, step returned unexpected state: %(val)s') - % {'step': step, 'node': node.uuid, 'val': result}) - utils.deploying_error_handler( - task, log_msg, - _("Failed to deploy: %s") % node.deploy_step) - return - - LOG.info('Node %(node)s finished deploy step %(step)s', - {'node': node.uuid, 'step': step}) - - # Finished executing the steps. Clear deploy_step. - node.deploy_step = None - driver_internal_info = node.driver_internal_info - driver_internal_info['deploy_steps'] = None - driver_internal_info.pop('deploy_step_index', None) - driver_internal_info.pop('deployment_reboot', None) - driver_internal_info.pop('deployment_polling', None) - # Remove the agent_url cached from the deployment. - driver_internal_info.pop('agent_url', None) - node.driver_internal_info = driver_internal_info - node.save() - - _start_console_in_deploy(task) - - task.process_event('done') - LOG.info('Successfully deployed node %(node)s with ' - 'instance %(instance)s.', - {'node': node.uuid, 'instance': node.instance_uuid}) - - -def _start_console_in_deploy(task): - """Start console at the end of deployment. - - Console is stopped at tearing down not to be exposed to an instance user. - Then, restart at deployment. - - :param task: a TaskManager instance with an exclusive lock - """ - - if not task.node.console_enabled: - return - - notify_utils.emit_console_notification( - task, 'console_restore', fields.NotificationStatus.START) - try: - task.driver.console.start_console(task) - except Exception as err: - msg = (_('Failed to start console while deploying the ' - 'node %(node)s: %(err)s.') % {'node': task.node.uuid, - 'err': err}) - LOG.error(msg) - task.node.last_error = msg - task.node.console_enabled = False - task.node.save() - notify_utils.emit_console_notification( - task, 'console_restore', fields.NotificationStatus.ERROR) - else: - notify_utils.emit_console_notification( - task, 'console_restore', fields.NotificationStatus.END) - - @task_manager.require_exclusive_lock def handle_sync_power_state_max_retries_exceeded(task, actual_power_state, exception=None): diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py index 907b2f9915..9a64151d32 100644 --- a/ironic/conductor/utils.py +++ b/ironic/conductor/utils.py @@ -943,3 +943,65 @@ def remove_agent_url(node): info = node.driver_internal_info info.pop('agent_url', None) node.driver_internal_info = info + + +def _get_node_next_steps(task, step_type, skip_current_step=True): + """Get the task's node's next steps. + + This determines what the next (remaining) steps are, and + returns the index into the steps list that corresponds to the + next step. The remaining steps are determined as follows: + + * If no steps have been started yet, all the steps + must be executed + * If skip_current_step is False, the remaining steps start + with the current step. Otherwise, the remaining steps + start with the step after the current one. + + All the steps are in node.driver_internal_info['_steps']. + node._step is the current step that was just executed + (or None, {} if no steps have been executed yet). + node.driver_internal_info['_step_index'] is the index + index into the steps list (or None, doesn't exist if no steps have + been executed yet) and corresponds to node._step. + + :param task: A TaskManager object + :param step_type: The type of steps to process: 'clean' or 'deploy'. + :param skip_current_step: True to skip the current step; False to + include it. + :returns: index of the next step; None if there are none to execute. + + """ + valid_types = set(['clean', 'deploy']) + if step_type not in valid_types: + # NOTE(rloo): No need to i18n this, since this would be a + # developer error; it isn't user-facing. + raise exception.Invalid( + 'step_type must be one of %(valid)s, not %(step)s' + % {'valid': valid_types, 'step': step_type}) + node = task.node + if not getattr(node, '%s_step' % step_type): + # first time through, all steps need to be done. Return the + # index of the first step in the list. + return 0 + + ind = node.driver_internal_info.get('%s_step_index' % step_type) + if ind is None: + return None + + if skip_current_step: + ind += 1 + if ind >= len(node.driver_internal_info['%s_steps' % step_type]): + # no steps left to do + ind = None + return ind + + +def get_node_next_clean_steps(task, skip_current_step=True): + return _get_node_next_steps(task, 'clean', + skip_current_step=skip_current_step) + + +def get_node_next_deploy_steps(task, skip_current_step=True): + return _get_node_next_steps(task, 'deploy', + skip_current_step=skip_current_step) diff --git a/ironic/tests/unit/conductor/test_deployments.py b/ironic/tests/unit/conductor/test_deployments.py new file mode 100644 index 0000000000..ba8b327fec --- /dev/null +++ b/ironic/tests/unit/conductor/test_deployments.py @@ -0,0 +1,1017 @@ +# +# 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. + +"""Tests for deployment aspects of the conductor.""" + +import mock +from oslo_config import cfg +from oslo_db import exception as db_exception +from oslo_utils import uuidutils + +from ironic.common import exception +from ironic.common import states +from ironic.common import swift +from ironic.conductor import deployments +from ironic.conductor import steps as conductor_steps +from ironic.conductor import task_manager +from ironic.conductor import utils as conductor_utils +from ironic.db import api as dbapi +from ironic.drivers.modules import fake +from ironic.tests.unit.conductor import mgr_utils +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils + +CONF = cfg.CONF + + +@mgr_utils.mock_record_keepalive +class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') + def test__do_node_deploy_driver_raises_prepare_error(self, mock_prepare, + mock_deploy): + self._start_service() + # test when driver.deploy.prepare raises an ironic error + mock_prepare.side_effect = exception.InstanceDeployFailure('test') + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + self.assertRaises(exception.InstanceDeployFailure, + deployments.do_node_deploy, task, + self.service.conductor.id) + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + # NOTE(deva): failing a deploy does not clear the target state + # any longer. Instead, it is cleared when the instance + # is deleted. + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertIsNotNone(node.last_error) + self.assertTrue(mock_prepare.called) + self.assertFalse(mock_deploy.called) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') + def test__do_node_deploy_unexpected_prepare_error(self, mock_prepare, + mock_deploy): + self._start_service() + # test when driver.deploy.prepare raises an exception + mock_prepare.side_effect = RuntimeError('test') + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + self.assertRaises(RuntimeError, + deployments.do_node_deploy, task, + self.service.conductor.id) + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + # NOTE(deva): failing a deploy does not clear the target state + # any longer. Instead, it is cleared when the instance + # is deleted. + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertIsNotNone(node.last_error) + self.assertTrue(mock_prepare.called) + self.assertFalse(mock_deploy.called) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test__do_node_deploy_driver_raises_error_old(self, mock_deploy): + # TODO(rloo): delete this after the deprecation period for supporting + # non deploy_steps. + # Mocking FakeDeploy.deploy before starting the service, causes + # it not to be a deploy_step. + self._start_service() + # test when driver.deploy.deploy raises an ironic error + mock_deploy.side_effect = exception.InstanceDeployFailure('test') + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + self.assertRaises(exception.InstanceDeployFailure, + deployments.do_node_deploy, task, + self.service.conductor.id) + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + # NOTE(deva): failing a deploy does not clear the target state + # any longer. Instead, it is cleared when the instance + # is deleted. + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertIsNotNone(node.last_error) + mock_deploy.assert_called_once_with(mock.ANY) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test__do_node_deploy_driver_unexpected_exception_old(self, + mock_deploy): + # TODO(rloo): delete this after the deprecation period for supporting + # non deploy_steps. + # Mocking FakeDeploy.deploy before starting the service, causes + # it not to be a deploy_step. + self._start_service() + # test when driver.deploy.deploy raises an exception + mock_deploy.side_effect = RuntimeError('test') + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + self.assertRaises(RuntimeError, + deployments.do_node_deploy, task, + self.service.conductor.id) + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + # NOTE(deva): failing a deploy does not clear the target state + # any longer. Instead, it is cleared when the instance + # is deleted. + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertIsNotNone(node.last_error) + mock_deploy.assert_called_once_with(mock.ANY) + + def _test__do_node_deploy_driver_exception(self, exc, unexpected=False): + self._start_service() + with mock.patch.object(fake.FakeDeploy, + 'deploy', autospec=True) as mock_deploy: + # test when driver.deploy.deploy() raises an exception + mock_deploy.side_effect = exc + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + deployments.do_node_deploy(task, self.service.conductor.id) + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + # NOTE(deva): failing a deploy does not clear the target state + # any longer. Instead, it is cleared when the instance + # is deleted. + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertIsNotNone(node.last_error) + if unexpected: + self.assertIn('Exception', node.last_error) + else: + self.assertNotIn('Exception', node.last_error) + + mock_deploy.assert_called_once_with(mock.ANY, task) + + def test__do_node_deploy_driver_ironic_exception(self): + self._test__do_node_deploy_driver_exception( + exception.InstanceDeployFailure('test')) + + def test__do_node_deploy_driver_unexpected_exception(self): + self._test__do_node_deploy_driver_exception(RuntimeError('test'), + unexpected=True) + + @mock.patch.object(deployments, '_store_configdrive', autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test__do_node_deploy_ok_old(self, mock_deploy, mock_store): + # TODO(rloo): delete this after the deprecation period for supporting + # non deploy_steps. + # Mocking FakeDeploy.deploy before starting the service, causes + # it not to be a deploy_step. + self._start_service() + # test when driver.deploy.deploy returns DEPLOYDONE + mock_deploy.return_value = states.DEPLOYDONE + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + deployments.do_node_deploy(task, self.service.conductor.id) + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertIsNone(node.last_error) + mock_deploy.assert_called_once_with(mock.ANY) + # assert _store_configdrive wasn't invoked + self.assertFalse(mock_store.called) + + @mock.patch.object(deployments, '_store_configdrive', autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test__do_node_deploy_ok_configdrive_old(self, mock_deploy, mock_store): + # TODO(rloo): delete this after the deprecation period for supporting + # non deploy_steps. + # Mocking FakeDeploy.deploy before starting the service, causes + # it not to be a deploy_step. + self._start_service() + # test when driver.deploy.deploy returns DEPLOYDONE + mock_deploy.return_value = states.DEPLOYDONE + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + configdrive = 'foo' + + deployments.do_node_deploy(task, self.service.conductor.id, + configdrive=configdrive) + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertIsNone(node.last_error) + mock_deploy.assert_called_once_with(mock.ANY) + mock_store.assert_called_once_with(task.node, configdrive) + + @mock.patch.object(deployments, '_store_configdrive', autospec=True) + def _test__do_node_deploy_ok(self, mock_store, configdrive=None, + expected_configdrive=None): + expected_configdrive = expected_configdrive or configdrive + self._start_service() + with mock.patch.object(fake.FakeDeploy, + 'deploy', autospec=True) as mock_deploy: + mock_deploy.return_value = None + self.node = obj_utils.create_test_node( + self.context, driver='fake-hardware', name=None, + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, self.node.uuid) + + deployments.do_node_deploy(task, self.service.conductor.id, + configdrive=configdrive) + self.node.refresh() + self.assertEqual(states.ACTIVE, self.node.provision_state) + self.assertEqual(states.NOSTATE, self.node.target_provision_state) + self.assertIsNone(self.node.last_error) + mock_deploy.assert_called_once_with(mock.ANY, mock.ANY) + if configdrive: + mock_store.assert_called_once_with(task.node, + expected_configdrive) + else: + self.assertFalse(mock_store.called) + + def test__do_node_deploy_ok(self): + self._test__do_node_deploy_ok() + + def test__do_node_deploy_ok_configdrive(self): + configdrive = 'foo' + self._test__do_node_deploy_ok(configdrive=configdrive) + + @mock.patch('openstack.baremetal.configdrive.build') + def test__do_node_deploy_configdrive_as_dict(self, mock_cd): + mock_cd.return_value = 'foo' + configdrive = {'user_data': 'abcd'} + self._test__do_node_deploy_ok(configdrive=configdrive, + expected_configdrive='foo') + mock_cd.assert_called_once_with({'uuid': self.node.uuid}, + network_data=None, + user_data=b'abcd', + vendor_data=None) + + @mock.patch('openstack.baremetal.configdrive.build') + def test__do_node_deploy_configdrive_as_dict_with_meta_data(self, mock_cd): + mock_cd.return_value = 'foo' + configdrive = {'meta_data': {'uuid': uuidutils.generate_uuid(), + 'name': 'new-name', + 'hostname': 'example.com'}} + self._test__do_node_deploy_ok(configdrive=configdrive, + expected_configdrive='foo') + mock_cd.assert_called_once_with(configdrive['meta_data'], + network_data=None, + user_data=None, + vendor_data=None) + + @mock.patch('openstack.baremetal.configdrive.build') + def test__do_node_deploy_configdrive_with_network_data(self, mock_cd): + mock_cd.return_value = 'foo' + configdrive = {'network_data': {'links': []}} + self._test__do_node_deploy_ok(configdrive=configdrive, + expected_configdrive='foo') + mock_cd.assert_called_once_with({'uuid': self.node.uuid}, + network_data={'links': []}, + user_data=None, + vendor_data=None) + + @mock.patch('openstack.baremetal.configdrive.build') + def test__do_node_deploy_configdrive_and_user_data_as_dict(self, mock_cd): + mock_cd.return_value = 'foo' + configdrive = {'user_data': {'user': 'data'}} + self._test__do_node_deploy_ok(configdrive=configdrive, + expected_configdrive='foo') + mock_cd.assert_called_once_with({'uuid': self.node.uuid}, + network_data=None, + user_data=b'{"user": "data"}', + vendor_data=None) + + @mock.patch('openstack.baremetal.configdrive.build') + def test__do_node_deploy_configdrive_with_vendor_data(self, mock_cd): + mock_cd.return_value = 'foo' + configdrive = {'vendor_data': {'foo': 'bar'}} + self._test__do_node_deploy_ok(configdrive=configdrive, + expected_configdrive='foo') + mock_cd.assert_called_once_with({'uuid': self.node.uuid}, + network_data=None, + user_data=None, + vendor_data={'foo': 'bar'}) + + @mock.patch.object(swift, 'SwiftAPI') + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') + def test__do_node_deploy_configdrive_swift_error(self, mock_prepare, + mock_swift): + CONF.set_override('configdrive_use_object_store', True, + group='deploy') + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + mock_swift.side_effect = exception.SwiftOperationError('error') + self.assertRaises(exception.SwiftOperationError, + deployments.do_node_deploy, task, + self.service.conductor.id, + configdrive=b'fake config drive') + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertIsNotNone(node.last_error) + self.assertFalse(mock_prepare.called) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') + def test__do_node_deploy_configdrive_db_error(self, mock_prepare): + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + task.node.save() + expected_instance_info = dict(node.instance_info) + with mock.patch.object(dbapi.IMPL, 'update_node') as mock_db: + db_node = self.dbapi.get_node_by_uuid(node.uuid) + mock_db.side_effect = [db_exception.DBDataError('DB error'), + db_node, db_node, db_node] + self.assertRaises(db_exception.DBDataError, + deployments.do_node_deploy, task, + self.service.conductor.id, + configdrive=b'fake config drive') + expected_instance_info.update(configdrive=b'fake config drive') + expected_calls = [ + mock.call(node.uuid, + {'version': mock.ANY, + 'instance_info': expected_instance_info}), + mock.call(node.uuid, + {'version': mock.ANY, + 'last_error': mock.ANY}), + mock.call(node.uuid, + {'version': mock.ANY, + 'deploy_step': {}, + 'driver_internal_info': mock.ANY}), + mock.call(node.uuid, + {'version': mock.ANY, + 'provision_state': states.DEPLOYFAIL, + 'target_provision_state': states.ACTIVE}), + ] + self.assertEqual(expected_calls, mock_db.mock_calls) + self.assertFalse(mock_prepare.called) + + @mock.patch.object(deployments, '_store_configdrive', autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') + def test__do_node_deploy_configdrive_unexpected_error(self, mock_prepare, + mock_store): + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=states.ACTIVE) + task = task_manager.TaskManager(self.context, node.uuid) + + mock_store.side_effect = RuntimeError('unexpected') + self.assertRaises(RuntimeError, + deployments.do_node_deploy, task, + self.service.conductor.id, + configdrive=b'fake config drive') + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertIsNotNone(node.last_error) + self.assertFalse(mock_prepare.called) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') + def test__do_node_deploy_ok_2_old(self, mock_deploy): + # TODO(rloo): delete this after the deprecation period for supporting + # non deploy_steps. + # Mocking FakeDeploy.deploy before starting the service, causes + # it not to be a deploy_step. + # NOTE(rloo): a different way of testing for the same thing as in + # test__do_node_deploy_ok() + self._start_service() + # test when driver.deploy.deploy returns DEPLOYDONE + mock_deploy.return_value = states.DEPLOYDONE + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_node_deploy(task, self.service.conductor.id) + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertIsNone(node.last_error) + mock_deploy.assert_called_once_with(mock.ANY) + + def test__do_node_deploy_ok_2(self): + # NOTE(rloo): a different way of testing for the same thing as in + # test__do_node_deploy_ok(). Instead of specifying the provision & + # target_provision_states when creating the node, we call + # task.process_event() to "set the stage" (err "states"). + self._start_service() + with mock.patch.object(fake.FakeDeploy, + 'deploy', autospec=True) as mock_deploy: + # test when driver.deploy.deploy() returns None + mock_deploy.return_value = None + node = obj_utils.create_test_node(self.context, + driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_node_deploy(task, self.service.conductor.id) + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertIsNone(node.last_error) + mock_deploy.assert_called_once_with(mock.ANY, mock.ANY) + + @mock.patch.object(deployments, 'do_next_deploy_step', autospec=True) + @mock.patch.object(deployments, '_old_rest_of_do_node_deploy', + autospec=True) + @mock.patch.object(conductor_steps, 'set_node_deployment_steps', + autospec=True) + def test_do_node_deploy_deprecated(self, mock_set_steps, mock_old_way, + mock_deploy_step): + # TODO(rloo): no deploy steps; delete this when we remove support + # for handling no deploy steps. + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_node_deploy(task, self.service.conductor.id) + mock_set_steps.assert_called_once_with(task) + mock_old_way.assert_called_once_with(task, self.service.conductor.id, + True) + self.assertFalse(mock_deploy_step.called) + self.assertNotIn('deploy_steps', task.node.driver_internal_info) + + @mock.patch.object(deployments, 'do_next_deploy_step', autospec=True) + @mock.patch.object(deployments, '_old_rest_of_do_node_deploy', + autospec=True) + @mock.patch.object(conductor_steps, 'set_node_deployment_steps', + autospec=True) + def test_do_node_deploy_steps(self, mock_set_steps, mock_old_way, + mock_deploy_step): + # these are not real steps... + fake_deploy_steps = ['step1', 'step2'] + + def add_steps(task): + info = task.node.driver_internal_info + info['deploy_steps'] = fake_deploy_steps + task.node.driver_internal_info = info + task.node.save() + + mock_set_steps.side_effect = add_steps + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_node_deploy(task, self.service.conductor.id) + mock_set_steps.assert_called_once_with(task) + self.assertFalse(mock_old_way.called) + mock_set_steps.assert_called_once_with(task) + self.assertEqual(fake_deploy_steps, + task.node.driver_internal_info['deploy_steps']) + + @mock.patch.object(deployments, 'do_next_deploy_step', autospec=True) + @mock.patch.object(deployments, '_old_rest_of_do_node_deploy', + autospec=True) + @mock.patch.object(conductor_steps, 'set_node_deployment_steps', + autospec=True) + def test_do_node_deploy_steps_old_rpc(self, mock_set_steps, mock_old_way, + mock_deploy_step): + # TODO(rloo): old RPC; delete this when we remove support for drivers + # with no deploy steps. + CONF.set_override('pin_release_version', '11.0') + # these are not real steps... + fake_deploy_steps = ['step1', 'step2'] + + def add_steps(task): + info = task.node.driver_internal_info + info['deploy_steps'] = fake_deploy_steps + task.node.driver_internal_info = info + task.node.save() + + mock_set_steps.side_effect = add_steps + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_node_deploy(task, self.service.conductor.id) + mock_set_steps.assert_called_once_with(task) + mock_old_way.assert_called_once_with(task, self.service.conductor.id, + False) + self.assertFalse(mock_deploy_step.called) + self.assertNotIn('deploy_steps', task.node.driver_internal_info) + + @mock.patch.object(deployments, '_SEEN_NO_DEPLOY_STEP_DEPRECATIONS', + autospec=True) + @mock.patch.object(deployments, 'LOG', autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy', autospec=True) + def test__old_rest_of_do_node_deploy_no_steps(self, mock_deploy, mock_log, + mock_deprecate): + # TODO(rloo): no deploy steps; delete this when we remove support + # for handling no deploy steps. + mock_deprecate.__contains__.side_effect = [False, True] + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments._old_rest_of_do_node_deploy( + task, self.service.conductor.id, True) + mock_deploy.assert_called_once_with(mock.ANY, task) + self.assertTrue(mock_log.warning.called) + self.assertEqual(self.service.conductor.id, + task.node.conductor_affinity) + mock_deprecate.__contains__.assert_called_once_with('FakeDeploy') + mock_deprecate.add.assert_called_once_with('FakeDeploy') + + # Make sure the deprecation warning isn't logged again + mock_log.reset_mock() + mock_deprecate.add.reset_mock() + deployments._old_rest_of_do_node_deploy( + task, self.service.conductor.id, True) + self.assertFalse(mock_log.warning.called) + mock_deprecate.__contains__.assert_has_calls( + [mock.call('FakeDeploy'), mock.call('FakeDeploy')]) + self.assertFalse(mock_deprecate.add.called) + + @mock.patch.object(deployments, 'LOG', autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy', autospec=True) + def test__old_rest_of_do_node_deploy_has_steps(self, mock_deploy, + mock_log): + # TODO(rloo): has steps but old RPC; delete this when we remove support + # for handling no deploy steps. + deployments._SEEN_NO_DEPLOY_STEP_DEPRECATIONS = set() + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments._old_rest_of_do_node_deploy( + task, self.service.conductor.id, False) + mock_deploy.assert_called_once_with(mock.ANY, task) + self.assertFalse(mock_log.warning.called) + self.assertEqual(self.service.conductor.id, + task.node.conductor_affinity) + + @mock.patch('ironic.conductor.deployments._start_console_in_deploy', + autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy', autospec=True) + def test__old_rest_of_do_node_deploy_console(self, mock_deploy, + mock_console): + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + mock_deploy.return_value = states.DEPLOYDONE + + deployments._old_rest_of_do_node_deploy( + task, self.service.conductor.id, True) + mock_deploy.assert_called_once_with(mock.ANY, task) + mock_console.assert_called_once_with(task) + self.assertEqual(self.service.conductor.id, + task.node.conductor_affinity) + + +@mgr_utils.mock_record_keepalive +class DoNextDeployStepTestCase(mgr_utils.ServiceSetUpMixin, + db_base.DbTestCase): + def setUp(self): + super(DoNextDeployStepTestCase, self).setUp() + self.deploy_start = { + 'step': 'deploy_start', 'priority': 50, 'interface': 'deploy'} + self.deploy_end = { + 'step': 'deploy_end', 'priority': 20, 'interface': 'deploy'} + self.deploy_steps = [self.deploy_start, self.deploy_end] + + @mock.patch.object(deployments, 'LOG', autospec=True) + def test__do_next_deploy_step_none(self, mock_log): + self._start_service() + node = obj_utils.create_test_node(self.context, driver='fake-hardware') + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_next_deploy_step(task, None, self.service.conductor.id) + + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(2, mock_log.info.call_count) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def test__do_next_deploy_step_async(self, mock_execute): + driver_internal_info = {'deploy_step_index': None, + 'deploy_steps': self.deploy_steps} + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + driver_internal_info=driver_internal_info, + deploy_step={}) + mock_execute.return_value = states.DEPLOYWAIT + expected_first_step = node.driver_internal_info['deploy_steps'][0] + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_next_deploy_step(task, 0, self.service.conductor.id) + + node.refresh() + self.assertEqual(states.DEPLOYWAIT, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertEqual(expected_first_step, node.deploy_step) + self.assertEqual(0, node.driver_internal_info['deploy_step_index']) + self.assertEqual(self.service.conductor.id, node.conductor_affinity) + mock_execute.assert_called_once_with(mock.ANY, task, + self.deploy_steps[0]) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def test__do_next_deploy_step_continue_from_last_step(self, mock_execute): + # Resume an in-progress deploy after the first async step + driver_internal_info = {'deploy_step_index': 0, + 'deploy_steps': self.deploy_steps} + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.DEPLOYWAIT, + target_provision_state=states.ACTIVE, + driver_internal_info=driver_internal_info, + deploy_step=self.deploy_steps[0]) + mock_execute.return_value = states.DEPLOYWAIT + + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('resume') + + deployments.do_next_deploy_step(task, 1, self.service.conductor.id) + node.refresh() + + self.assertEqual(states.DEPLOYWAIT, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertEqual(self.deploy_steps[1], node.deploy_step) + self.assertEqual(1, node.driver_internal_info['deploy_step_index']) + mock_execute.assert_called_once_with(mock.ANY, task, + self.deploy_steps[1]) + + @mock.patch('ironic.drivers.modules.fake.FakeConsole.start_console', + autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def _test__do_next_deploy_step_last_step_done(self, mock_execute, + mock_console, + console_enabled=False, + console_error=False): + # Resume where last_step is the last deploy step that was executed + driver_internal_info = {'deploy_step_index': 1, + 'deploy_steps': self.deploy_steps} + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.DEPLOYWAIT, + target_provision_state=states.ACTIVE, + driver_internal_info=driver_internal_info, + deploy_step=self.deploy_steps[1], + console_enabled=console_enabled) + mock_execute.return_value = None + if console_error: + mock_console.side_effect = exception.ConsoleError() + + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('resume') + + deployments.do_next_deploy_step(task, None, self.service.conductor.id) + node.refresh() + # Deploying should be complete without calling additional steps + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertEqual({}, node.deploy_step) + self.assertNotIn('deploy_step_index', node.driver_internal_info) + self.assertIsNone(node.driver_internal_info['deploy_steps']) + self.assertFalse(mock_execute.called) + if console_enabled: + mock_console.assert_called_once_with(mock.ANY, task) + else: + self.assertFalse(mock_console.called) + + def test__do_next_deploy_step_last_step_done(self): + self._test__do_next_deploy_step_last_step_done() + + def test__do_next_deploy_step_last_step_done_with_console(self): + self._test__do_next_deploy_step_last_step_done(console_enabled=True) + + def test__do_next_deploy_step_last_step_done_with_console_error(self): + self._test__do_next_deploy_step_last_step_done(console_enabled=True, + console_error=True) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def test__do_next_deploy_step_all(self, mock_execute): + # Run all steps from start to finish (all synchronous) + driver_internal_info = {'deploy_step_index': None, + 'deploy_steps': self.deploy_steps, + 'agent_url': 'url'} + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + driver_internal_info=driver_internal_info, + deploy_step={}) + mock_execute.return_value = None + + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_next_deploy_step(task, 1, self.service.conductor.id) + + # Deploying should be complete + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertEqual({}, node.deploy_step) + self.assertNotIn('deploy_step_index', node.driver_internal_info) + self.assertIsNone(node.driver_internal_info['deploy_steps']) + mock_execute.assert_has_calls = [mock.call(self.deploy_steps[0]), + mock.call(self.deploy_steps[1])] + self.assertNotIn('agent_url', node.driver_internal_info) + + @mock.patch.object(conductor_utils, 'LOG', autospec=True) + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def _do_next_deploy_step_execute_fail(self, exc, traceback, + mock_execute, mock_log): + # When a deploy step fails, go to DEPLOYFAIL + driver_internal_info = {'deploy_step_index': None, + 'deploy_steps': self.deploy_steps} + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + driver_internal_info=driver_internal_info, + deploy_step={}) + mock_execute.side_effect = exc + + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_next_deploy_step(task, 0, self.service.conductor.id) + + # Make sure we go to DEPLOYFAIL, clear deploy_steps + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertEqual({}, node.deploy_step) + self.assertNotIn('deploy_step_index', node.driver_internal_info) + self.assertIsNotNone(node.last_error) + self.assertFalse(node.maintenance) + mock_execute.assert_called_once_with(mock.ANY, mock.ANY, + self.deploy_steps[0]) + mock_log.error.assert_called_once_with(mock.ANY, exc_info=traceback) + + def test_do_next_deploy_step_execute_ironic_exception(self): + self._do_next_deploy_step_execute_fail( + exception.IronicException('foo'), False) + + def test_do_next_deploy_step_execute_exception(self): + self._do_next_deploy_step_execute_fail(Exception('foo'), True) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def test_do_next_deploy_step_no_steps(self, mock_execute): + + self._start_service() + for info in ({'deploy_steps': None, 'deploy_step_index': None}, + {'deploy_steps': None}): + # Resume where there are no steps, should be a noop + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + uuid=uuidutils.generate_uuid(), + last_error=None, + driver_internal_info=info, + deploy_step={}) + + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_next_deploy_step(task, None, + self.service.conductor.id) + + # Deploying should be complete without calling additional steps + node.refresh() + self.assertEqual(states.ACTIVE, node.provision_state) + self.assertEqual(states.NOSTATE, node.target_provision_state) + self.assertEqual({}, node.deploy_step) + self.assertNotIn('deploy_step_index', node.driver_internal_info) + self.assertFalse(mock_execute.called) + mock_execute.reset_mock() + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def test_do_next_deploy_step_bad_step_return_value(self, mock_execute): + # When a deploy step fails, go to DEPLOYFAIL + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + driver_internal_info={'deploy_steps': self.deploy_steps, + 'deploy_step_index': None}, + deploy_step={}) + mock_execute.return_value = "foo" + + task = task_manager.TaskManager(self.context, node.uuid) + task.process_event('deploy') + + deployments.do_next_deploy_step(task, 0, self.service.conductor.id) + + # Make sure we go to DEPLOYFAIL, clear deploy_steps + node.refresh() + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + self.assertEqual(states.ACTIVE, node.target_provision_state) + self.assertEqual({}, node.deploy_step) + self.assertNotIn('deploy_step_index', node.driver_internal_info) + self.assertIsNotNone(node.last_error) + self.assertFalse(node.maintenance) + mock_execute.assert_called_once_with(mock.ANY, mock.ANY, + self.deploy_steps[0]) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def test_do_next_deploy_step_oob_reboot(self, mock_execute): + # When a deploy step fails, go to DEPLOYWAIT + tgt_prov_state = states.ACTIVE + + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=tgt_prov_state, + last_error=None, + driver_internal_info={'deploy_steps': self.deploy_steps, + 'deploy_step_index': None, + 'deployment_reboot': True}, + clean_step={}) + mock_execute.side_effect = exception.AgentConnectionFailed( + reason='failed') + + with task_manager.acquire( + self.context, node.uuid, shared=False) as task: + deployments.do_next_deploy_step(task, 0, mock.ANY) + + self._stop_service() + node.refresh() + + # Make sure we go to CLEANWAIT + self.assertEqual(states.DEPLOYWAIT, node.provision_state) + self.assertEqual(tgt_prov_state, node.target_provision_state) + self.assertEqual(self.deploy_steps[0], node.deploy_step) + self.assertEqual(0, node.driver_internal_info['deploy_step_index']) + self.assertFalse(node.driver_internal_info['skip_current_deploy_step']) + mock_execute.assert_called_once_with( + mock.ANY, mock.ANY, self.deploy_steps[0]) + + @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', + autospec=True) + def test_do_next_deploy_step_oob_reboot_fail(self, mock_execute): + # When a deploy step fails with no reboot requested go to DEPLOYFAIL + tgt_prov_state = states.ACTIVE + + self._start_service() + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.DEPLOYING, + target_provision_state=tgt_prov_state, + last_error=None, + driver_internal_info={'deploy_steps': self.deploy_steps, + 'deploy_step_index': None}, + deploy_step={}) + mock_execute.side_effect = exception.AgentConnectionFailed( + reason='failed') + + with task_manager.acquire( + self.context, node.uuid, shared=False) as task: + deployments.do_next_deploy_step(task, 0, mock.ANY) + + self._stop_service() + node.refresh() + + # Make sure we go to DEPLOYFAIL, clear deploy_steps + self.assertEqual(states.DEPLOYFAIL, node.provision_state) + self.assertEqual(tgt_prov_state, node.target_provision_state) + self.assertEqual({}, node.deploy_step) + self.assertNotIn('deploy_step_index', node.driver_internal_info) + self.assertNotIn('skip_current_deploy_step', node.driver_internal_info) + self.assertIsNotNone(node.last_error) + mock_execute.assert_called_once_with( + mock.ANY, mock.ANY, self.deploy_steps[0]) + + +@mock.patch.object(swift, 'SwiftAPI') +class StoreConfigDriveTestCase(db_base.DbTestCase): + + def setUp(self): + super(StoreConfigDriveTestCase, self).setUp() + self.node = obj_utils.create_test_node(self.context, + driver='fake-hardware', + instance_info=None) + + def test_store_configdrive(self, mock_swift): + deployments._store_configdrive(self.node, 'foo') + expected_instance_info = {'configdrive': 'foo'} + self.node.refresh() + self.assertEqual(expected_instance_info, self.node.instance_info) + self.assertFalse(mock_swift.called) + + def test_store_configdrive_swift(self, mock_swift): + container_name = 'foo_container' + timeout = 123 + expected_obj_name = 'configdrive-%s' % self.node.uuid + expected_obj_header = {'X-Delete-After': str(timeout)} + expected_instance_info = {'configdrive': 'http://1.2.3.4'} + + # set configs and mocks + CONF.set_override('configdrive_use_object_store', True, + group='deploy') + CONF.set_override('configdrive_swift_container', container_name, + group='conductor') + CONF.set_override('deploy_callback_timeout', timeout, + group='conductor') + mock_swift.return_value.get_temp_url.return_value = 'http://1.2.3.4' + + deployments._store_configdrive(self.node, b'foo') + + mock_swift.assert_called_once_with() + mock_swift.return_value.create_object.assert_called_once_with( + container_name, expected_obj_name, mock.ANY, + object_headers=expected_obj_header) + mock_swift.return_value.get_temp_url.assert_called_once_with( + container_name, expected_obj_name, timeout) + self.node.refresh() + self.assertEqual(expected_instance_info, self.node.instance_info) + + def test_store_configdrive_swift_no_deploy_timeout(self, mock_swift): + container_name = 'foo_container' + expected_obj_name = 'configdrive-%s' % self.node.uuid + expected_obj_header = {'X-Delete-After': '1200'} + expected_instance_info = {'configdrive': 'http://1.2.3.4'} + + # set configs and mocks + CONF.set_override('configdrive_use_object_store', True, + group='deploy') + CONF.set_override('configdrive_swift_container', container_name, + group='conductor') + CONF.set_override('configdrive_swift_temp_url_duration', 1200, + group='conductor') + CONF.set_override('deploy_callback_timeout', 0, + group='conductor') + mock_swift.return_value.get_temp_url.return_value = 'http://1.2.3.4' + + deployments._store_configdrive(self.node, b'foo') + + mock_swift.assert_called_once_with() + mock_swift.return_value.create_object.assert_called_once_with( + container_name, expected_obj_name, mock.ANY, + object_headers=expected_obj_header) + mock_swift.return_value.get_temp_url.assert_called_once_with( + container_name, expected_obj_name, 1200) + self.node.refresh() + self.assertEqual(expected_instance_info, self.node.instance_info) + + def test_store_configdrive_swift_no_deploy_timeout_fallback(self, + mock_swift): + container_name = 'foo_container' + expected_obj_name = 'configdrive-%s' % self.node.uuid + expected_obj_header = {'X-Delete-After': '1800'} + expected_instance_info = {'configdrive': 'http://1.2.3.4'} + + # set configs and mocks + CONF.set_override('configdrive_use_object_store', True, + group='deploy') + CONF.set_override('configdrive_swift_container', container_name, + group='conductor') + CONF.set_override('deploy_callback_timeout', 0, + group='conductor') + mock_swift.return_value.get_temp_url.return_value = 'http://1.2.3.4' + + deployments._store_configdrive(self.node, b'foo') + + mock_swift.assert_called_once_with() + mock_swift.return_value.create_object.assert_called_once_with( + container_name, expected_obj_name, mock.ANY, + object_headers=expected_obj_header) + mock_swift.return_value.get_temp_url.assert_called_once_with( + container_name, expected_obj_name, 1800) + self.node.refresh() + self.assertEqual(expected_instance_info, self.node.instance_info) diff --git a/ironic/tests/unit/conductor/test_manager.py b/ironic/tests/unit/conductor/test_manager.py index b2a1d832bc..26b156d961 100644 --- a/ironic/tests/unit/conductor/test_manager.py +++ b/ironic/tests/unit/conductor/test_manager.py @@ -27,7 +27,6 @@ import eventlet from futurist import waiters import mock from oslo_config import cfg -from oslo_db import exception as db_exception import oslo_messaging as messaging from oslo_utils import uuidutils from oslo_versionedobjects import base as ovo_base @@ -39,7 +38,7 @@ from ironic.common import exception from ironic.common import images from ironic.common import nova from ironic.common import states -from ironic.common import swift +from ironic.conductor import deployments from ironic.conductor import manager from ironic.conductor import notification_utils from ironic.conductor import steps as conductor_steps @@ -1583,7 +1582,7 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, self.assertFalse(node.driver_internal_info['is_whole_disk_image']) def test_do_node_deploy_rebuild_active_state_error(self, mock_iwdi): - # Tests manager.do_node_deploy() & manager._do_next_deploy_step(), + # Tests manager.do_node_deploy() & deployments.do_next_deploy_step(), # when getting an unexpected state returned from a deploy_step. mock_iwdi.return_value = True self._start_service() @@ -1591,7 +1590,7 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, # decorator. With a decorator, when initialization is done, the # mocked deploy() method isn't considered a deploy step, causing # manager._old_rest_of_do_node_deploy() to be run instead of - # manager._do_next_deploy_step(). So we defer mock'ing until after + # deployments.do_next_deploy_step(). So we defer mock'ing until after # the init is done. with mock.patch.object(fake.FakeDeploy, 'deploy', autospec=True) as mock_deploy: @@ -1628,7 +1627,7 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, # decorator. With a decorator, when initialization is done, the # mocked deploy() method isn't considered a deploy step, causing # manager._old_rest_of_do_node_deploy() to be run instead of - # manager._do_next_deploy_step(). So we defer mock'ing until after + # deployments.do_next_deploy_step(). So we defer mock'ing until after # the init is done. with mock.patch.object(fake.FakeDeploy, 'deploy', autospec=True) as mock_deploy: @@ -1660,7 +1659,7 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, # decorator. With a decorator, when initialization is done, the # mocked deploy() method isn't considered a deploy step, causing # manager._old_rest_of_do_node_deploy() to be run instead of - # manager._do_next_deploy_step(). So we defer mock'ing until after + # deployments.do_next_deploy_step(). So we defer mock'ing until after # the init is done. with mock.patch.object(fake.FakeDeploy, 'deploy', autospec=True) as mock_deploy: @@ -1691,7 +1690,7 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, # decorator. With a decorator, when initialization is done, the # mocked deploy() method isn't considered a deploy step, causing # manager._old_rest_of_do_node_deploy() to be run instead of - # manager._do_next_deploy_step(). So we defer mock'ing until after + # deployments.do_next_deploy_step(). So we defer mock'ing until after # the init is done. with mock.patch.object(fake.FakeDeploy, 'deploy', autospec=True) as mock_deploy: @@ -1722,7 +1721,7 @@ class ServiceDoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, # decorator. With a decorator, when initialization is done, the # mocked deploy() method isn't considered a deploy step, causing # manager._old_rest_of_do_node_deploy() to be run instead of - # manager._do_next_deploy_step(). So we defer mock'ing until after + # deployments.do_next_deploy_step(). So we defer mock'ing until after # the init is done. with mock.patch.object(fake.FakeDeploy, 'deploy', autospec=True) as mock_deploy: @@ -1922,7 +1921,8 @@ class ContinueNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, node.refresh() self.assertEqual(states.DEPLOYING, node.provision_state) self.assertEqual(tgt_prv_state, node.target_provision_state) - mock_spawn.assert_called_with(mock.ANY, manager._do_next_deploy_step, + mock_spawn.assert_called_with(mock.ANY, + deployments.do_next_deploy_step, mock.ANY, 1, mock.ANY) @mock.patch.object(task_manager.TaskManager, 'process_event', @@ -1950,7 +1950,8 @@ class ContinueNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, node.refresh() self.assertEqual(states.DEPLOYING, node.provision_state) self.assertEqual(tgt_prv_state, node.target_provision_state) - mock_spawn.assert_called_with(mock.ANY, manager._do_next_deploy_step, + mock_spawn.assert_called_with(mock.ANY, + deployments.do_next_deploy_step, mock.ANY, 1, mock.ANY) self.assertFalse(mock_event.called) @@ -1977,7 +1978,8 @@ class ContinueNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, self.assertNotIn( 'skip_current_deploy_step', node.driver_internal_info) expected_step_index = 0 - mock_spawn.assert_called_with(mock.ANY, manager._do_next_deploy_step, + mock_spawn.assert_called_with(mock.ANY, + deployments.do_next_deploy_step, mock.ANY, expected_step_index, mock.ANY) def test_continue_node_deploy_skip_step(self): @@ -2003,938 +2005,10 @@ class ContinueNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, self._stop_service() node.refresh() self.assertNotIn('deployment_polling', node.driver_internal_info) - mock_spawn.assert_called_with(mock.ANY, manager._do_next_deploy_step, + mock_spawn.assert_called_with(mock.ANY, + deployments.do_next_deploy_step, mock.ANY, 1, mock.ANY) - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def test_do_next_deploy_step_oob_reboot(self, mock_execute): - # When a deploy step fails, go to DEPLOYWAIT - tgt_prov_state = states.ACTIVE - - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=tgt_prov_state, - last_error=None, - driver_internal_info={'deploy_steps': self.deploy_steps, - 'deploy_step_index': None, - 'deployment_reboot': True}, - clean_step={}) - mock_execute.side_effect = exception.AgentConnectionFailed( - reason='failed') - - with task_manager.acquire( - self.context, node.uuid, shared=False) as task: - manager._do_next_deploy_step(task, 0, mock.ANY) - - self._stop_service() - node.refresh() - - # Make sure we go to CLEANWAIT - self.assertEqual(states.DEPLOYWAIT, node.provision_state) - self.assertEqual(tgt_prov_state, node.target_provision_state) - self.assertEqual(self.deploy_steps[0], node.deploy_step) - self.assertEqual(0, node.driver_internal_info['deploy_step_index']) - self.assertFalse(node.driver_internal_info['skip_current_deploy_step']) - mock_execute.assert_called_once_with( - mock.ANY, mock.ANY, self.deploy_steps[0]) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def test_do_next_clean_step_oob_reboot_fail(self, - mock_execute): - # When a deploy step fails with no reboot requested go to DEPLOYFAIL - tgt_prov_state = states.ACTIVE - - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=tgt_prov_state, - last_error=None, - driver_internal_info={'deploy_steps': self.deploy_steps, - 'deploy_step_index': None}, - deploy_step={}) - mock_execute.side_effect = exception.AgentConnectionFailed( - reason='failed') - - with task_manager.acquire( - self.context, node.uuid, shared=False) as task: - manager._do_next_deploy_step(task, 0, mock.ANY) - - self._stop_service() - node.refresh() - - # Make sure we go to DEPLOYFAIL, clear deploy_steps - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - self.assertEqual(tgt_prov_state, node.target_provision_state) - self.assertEqual({}, node.deploy_step) - self.assertNotIn('deploy_step_index', node.driver_internal_info) - self.assertNotIn('skip_current_deploy_step', node.driver_internal_info) - self.assertIsNotNone(node.last_error) - mock_execute.assert_called_once_with( - mock.ANY, mock.ANY, self.deploy_steps[0]) - - -@mgr_utils.mock_record_keepalive -class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') - def test__do_node_deploy_driver_raises_prepare_error(self, mock_prepare, - mock_deploy): - self._start_service() - # test when driver.deploy.prepare raises an ironic error - mock_prepare.side_effect = exception.InstanceDeployFailure('test') - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - self.assertRaises(exception.InstanceDeployFailure, - manager.do_node_deploy, task, - self.service.conductor.id) - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - # NOTE(deva): failing a deploy does not clear the target state - # any longer. Instead, it is cleared when the instance - # is deleted. - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertIsNotNone(node.last_error) - self.assertTrue(mock_prepare.called) - self.assertFalse(mock_deploy.called) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') - def test__do_node_deploy_unexpected_prepare_error(self, mock_prepare, - mock_deploy): - self._start_service() - # test when driver.deploy.prepare raises an exception - mock_prepare.side_effect = RuntimeError('test') - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - self.assertRaises(RuntimeError, - manager.do_node_deploy, task, - self.service.conductor.id) - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - # NOTE(deva): failing a deploy does not clear the target state - # any longer. Instead, it is cleared when the instance - # is deleted. - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertIsNotNone(node.last_error) - self.assertTrue(mock_prepare.called) - self.assertFalse(mock_deploy.called) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test__do_node_deploy_driver_raises_error_old(self, mock_deploy): - # TODO(rloo): delete this after the deprecation period for supporting - # non deploy_steps. - # Mocking FakeDeploy.deploy before starting the service, causes - # it not to be a deploy_step. - self._start_service() - # test when driver.deploy.deploy raises an ironic error - mock_deploy.side_effect = exception.InstanceDeployFailure('test') - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - self.assertRaises(exception.InstanceDeployFailure, - manager.do_node_deploy, task, - self.service.conductor.id) - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - # NOTE(deva): failing a deploy does not clear the target state - # any longer. Instead, it is cleared when the instance - # is deleted. - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertIsNotNone(node.last_error) - mock_deploy.assert_called_once_with(mock.ANY) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test__do_node_deploy_driver_unexpected_exception_old(self, - mock_deploy): - # TODO(rloo): delete this after the deprecation period for supporting - # non deploy_steps. - # Mocking FakeDeploy.deploy before starting the service, causes - # it not to be a deploy_step. - self._start_service() - # test when driver.deploy.deploy raises an exception - mock_deploy.side_effect = RuntimeError('test') - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - self.assertRaises(RuntimeError, - manager.do_node_deploy, task, - self.service.conductor.id) - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - # NOTE(deva): failing a deploy does not clear the target state - # any longer. Instead, it is cleared when the instance - # is deleted. - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertIsNotNone(node.last_error) - mock_deploy.assert_called_once_with(mock.ANY) - - def _test__do_node_deploy_driver_exception(self, exc, unexpected=False): - self._start_service() - with mock.patch.object(fake.FakeDeploy, - 'deploy', autospec=True) as mock_deploy: - # test when driver.deploy.deploy() raises an exception - mock_deploy.side_effect = exc - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - manager.do_node_deploy(task, self.service.conductor.id) - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - # NOTE(deva): failing a deploy does not clear the target state - # any longer. Instead, it is cleared when the instance - # is deleted. - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertIsNotNone(node.last_error) - if unexpected: - self.assertIn('Exception', node.last_error) - else: - self.assertNotIn('Exception', node.last_error) - - mock_deploy.assert_called_once_with(mock.ANY, task) - - def test__do_node_deploy_driver_ironic_exception(self): - self._test__do_node_deploy_driver_exception( - exception.InstanceDeployFailure('test')) - - def test__do_node_deploy_driver_unexpected_exception(self): - self._test__do_node_deploy_driver_exception(RuntimeError('test'), - unexpected=True) - - @mock.patch.object(manager, '_store_configdrive') - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test__do_node_deploy_ok_old(self, mock_deploy, mock_store): - # TODO(rloo): delete this after the deprecation period for supporting - # non deploy_steps. - # Mocking FakeDeploy.deploy before starting the service, causes - # it not to be a deploy_step. - self._start_service() - # test when driver.deploy.deploy returns DEPLOYDONE - mock_deploy.return_value = states.DEPLOYDONE - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - manager.do_node_deploy(task, self.service.conductor.id) - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - self.assertIsNone(node.last_error) - mock_deploy.assert_called_once_with(mock.ANY) - # assert _store_configdrive wasn't invoked - self.assertFalse(mock_store.called) - - @mock.patch.object(manager, '_store_configdrive') - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test__do_node_deploy_ok_configdrive_old(self, mock_deploy, mock_store): - # TODO(rloo): delete this after the deprecation period for supporting - # non deploy_steps. - # Mocking FakeDeploy.deploy before starting the service, causes - # it not to be a deploy_step. - self._start_service() - # test when driver.deploy.deploy returns DEPLOYDONE - mock_deploy.return_value = states.DEPLOYDONE - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - configdrive = 'foo' - - manager.do_node_deploy(task, self.service.conductor.id, - configdrive=configdrive) - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - self.assertIsNone(node.last_error) - mock_deploy.assert_called_once_with(mock.ANY) - mock_store.assert_called_once_with(task.node, configdrive) - - @mock.patch.object(manager, '_store_configdrive') - def _test__do_node_deploy_ok(self, mock_store, configdrive=None, - expected_configdrive=None): - expected_configdrive = expected_configdrive or configdrive - self._start_service() - with mock.patch.object(fake.FakeDeploy, - 'deploy', autospec=True) as mock_deploy: - mock_deploy.return_value = None - self.node = obj_utils.create_test_node( - self.context, driver='fake-hardware', name=None, - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, self.node.uuid) - - manager.do_node_deploy(task, self.service.conductor.id, - configdrive=configdrive) - self.node.refresh() - self.assertEqual(states.ACTIVE, self.node.provision_state) - self.assertEqual(states.NOSTATE, self.node.target_provision_state) - self.assertIsNone(self.node.last_error) - mock_deploy.assert_called_once_with(mock.ANY, mock.ANY) - if configdrive: - mock_store.assert_called_once_with(task.node, - expected_configdrive) - else: - self.assertFalse(mock_store.called) - - def test__do_node_deploy_ok(self): - self._test__do_node_deploy_ok() - - def test__do_node_deploy_ok_configdrive(self): - configdrive = 'foo' - self._test__do_node_deploy_ok(configdrive=configdrive) - - @mock.patch('openstack.baremetal.configdrive.build') - def test__do_node_deploy_configdrive_as_dict(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'user_data': 'abcd'} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data=None, - user_data=b'abcd', - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build') - def test__do_node_deploy_configdrive_as_dict_with_meta_data(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'meta_data': {'uuid': uuidutils.generate_uuid(), - 'name': 'new-name', - 'hostname': 'example.com'}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with(configdrive['meta_data'], - network_data=None, - user_data=None, - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build') - def test__do_node_deploy_configdrive_with_network_data(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'network_data': {'links': []}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data={'links': []}, - user_data=None, - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build') - def test__do_node_deploy_configdrive_and_user_data_as_dict(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'user_data': {'user': 'data'}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data=None, - user_data=b'{"user": "data"}', - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build') - def test__do_node_deploy_configdrive_with_vendor_data(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'vendor_data': {'foo': 'bar'}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data=None, - user_data=None, - vendor_data={'foo': 'bar'}) - - @mock.patch.object(swift, 'SwiftAPI') - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') - def test__do_node_deploy_configdrive_swift_error(self, mock_prepare, - mock_swift): - CONF.set_override('configdrive_use_object_store', True, - group='deploy') - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - mock_swift.side_effect = exception.SwiftOperationError('error') - self.assertRaises(exception.SwiftOperationError, - manager.do_node_deploy, task, - self.service.conductor.id, - configdrive=b'fake config drive') - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertIsNotNone(node.last_error) - self.assertFalse(mock_prepare.called) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') - def test__do_node_deploy_configdrive_db_error(self, mock_prepare): - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - task.node.save() - expected_instance_info = dict(node.instance_info) - with mock.patch.object(dbapi.IMPL, 'update_node') as mock_db: - db_node = self.dbapi.get_node_by_uuid(node.uuid) - mock_db.side_effect = [db_exception.DBDataError('DB error'), - db_node, db_node, db_node] - self.assertRaises(db_exception.DBDataError, - manager.do_node_deploy, task, - self.service.conductor.id, - configdrive=b'fake config drive') - expected_instance_info.update(configdrive=b'fake config drive') - expected_calls = [ - mock.call(node.uuid, - {'version': mock.ANY, - 'instance_info': expected_instance_info}), - mock.call(node.uuid, - {'version': mock.ANY, - 'last_error': mock.ANY}), - mock.call(node.uuid, - {'version': mock.ANY, - 'deploy_step': {}, - 'driver_internal_info': mock.ANY}), - mock.call(node.uuid, - {'version': mock.ANY, - 'provision_state': states.DEPLOYFAIL, - 'target_provision_state': states.ACTIVE}), - ] - self.assertEqual(expected_calls, mock_db.mock_calls) - self.assertFalse(mock_prepare.called) - - @mock.patch.object(manager, '_store_configdrive') - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') - def test__do_node_deploy_configdrive_unexpected_error(self, mock_prepare, - mock_store): - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware', - provision_state=states.DEPLOYING, - target_provision_state=states.ACTIVE) - task = task_manager.TaskManager(self.context, node.uuid) - - mock_store.side_effect = RuntimeError('unexpected') - self.assertRaises(RuntimeError, - manager.do_node_deploy, task, - self.service.conductor.id, - configdrive=b'fake config drive') - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertIsNotNone(node.last_error) - self.assertFalse(mock_prepare.called) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy') - def test__do_node_deploy_ok_2_old(self, mock_deploy): - # TODO(rloo): delete this after the deprecation period for supporting - # non deploy_steps. - # Mocking FakeDeploy.deploy before starting the service, causes - # it not to be a deploy_step. - # NOTE(rloo): a different way of testing for the same thing as in - # test__do_node_deploy_ok() - self._start_service() - # test when driver.deploy.deploy returns DEPLOYDONE - mock_deploy.return_value = states.DEPLOYDONE - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager.do_node_deploy(task, self.service.conductor.id) - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - self.assertIsNone(node.last_error) - mock_deploy.assert_called_once_with(mock.ANY) - - def test__do_node_deploy_ok_2(self): - # NOTE(rloo): a different way of testing for the same thing as in - # test__do_node_deploy_ok(). Instead of specifying the provision & - # target_provision_states when creating the node, we call - # task.process_event() to "set the stage" (err "states"). - self._start_service() - with mock.patch.object(fake.FakeDeploy, - 'deploy', autospec=True) as mock_deploy: - # test when driver.deploy.deploy() returns None - mock_deploy.return_value = None - node = obj_utils.create_test_node(self.context, - driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager.do_node_deploy(task, self.service.conductor.id) - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - self.assertIsNone(node.last_error) - mock_deploy.assert_called_once_with(mock.ANY, mock.ANY) - - @mock.patch.object(manager, '_do_next_deploy_step', autospec=True) - @mock.patch.object(manager, '_old_rest_of_do_node_deploy', - autospec=True) - @mock.patch.object(conductor_steps, 'set_node_deployment_steps', - autospec=True) - def test_do_node_deploy_deprecated(self, mock_set_steps, mock_old_way, - mock_deploy_step): - # TODO(rloo): no deploy steps; delete this when we remove support - # for handling no deploy steps. - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager.do_node_deploy(task, self.service.conductor.id) - mock_set_steps.assert_called_once_with(task) - mock_old_way.assert_called_once_with(task, self.service.conductor.id, - True) - self.assertFalse(mock_deploy_step.called) - self.assertNotIn('deploy_steps', task.node.driver_internal_info) - - @mock.patch.object(manager, '_do_next_deploy_step', autospec=True) - @mock.patch.object(manager, '_old_rest_of_do_node_deploy', - autospec=True) - @mock.patch.object(conductor_steps, 'set_node_deployment_steps', - autospec=True) - def test_do_node_deploy_steps(self, mock_set_steps, mock_old_way, - mock_deploy_step): - # these are not real steps... - fake_deploy_steps = ['step1', 'step2'] - - def add_steps(task): - info = task.node.driver_internal_info - info['deploy_steps'] = fake_deploy_steps - task.node.driver_internal_info = info - task.node.save() - - mock_set_steps.side_effect = add_steps - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager.do_node_deploy(task, self.service.conductor.id) - mock_set_steps.assert_called_once_with(task) - self.assertFalse(mock_old_way.called) - mock_set_steps.assert_called_once_with(task) - self.assertEqual(fake_deploy_steps, - task.node.driver_internal_info['deploy_steps']) - - @mock.patch.object(manager, '_do_next_deploy_step', autospec=True) - @mock.patch.object(manager, '_old_rest_of_do_node_deploy', - autospec=True) - @mock.patch.object(conductor_steps, 'set_node_deployment_steps', - autospec=True) - def test_do_node_deploy_steps_old_rpc(self, mock_set_steps, mock_old_way, - mock_deploy_step): - # TODO(rloo): old RPC; delete this when we remove support for drivers - # with no deploy steps. - CONF.set_override('pin_release_version', '11.0') - # these are not real steps... - fake_deploy_steps = ['step1', 'step2'] - - def add_steps(task): - info = task.node.driver_internal_info - info['deploy_steps'] = fake_deploy_steps - task.node.driver_internal_info = info - task.node.save() - - mock_set_steps.side_effect = add_steps - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager.do_node_deploy(task, self.service.conductor.id) - mock_set_steps.assert_called_once_with(task) - mock_old_way.assert_called_once_with(task, self.service.conductor.id, - False) - self.assertFalse(mock_deploy_step.called) - self.assertNotIn('deploy_steps', task.node.driver_internal_info) - - @mock.patch.object(manager, '_SEEN_NO_DEPLOY_STEP_DEPRECATIONS', - autospec=True) - @mock.patch.object(manager, 'LOG', autospec=True) - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy', autospec=True) - def test__old_rest_of_do_node_deploy_no_steps(self, mock_deploy, mock_log, - mock_deprecate): - # TODO(rloo): no deploy steps; delete this when we remove support - # for handling no deploy steps. - mock_deprecate.__contains__.side_effect = [False, True] - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._old_rest_of_do_node_deploy(task, self.service.conductor.id, - True) - mock_deploy.assert_called_once_with(mock.ANY, task) - self.assertTrue(mock_log.warning.called) - self.assertEqual(self.service.conductor.id, - task.node.conductor_affinity) - mock_deprecate.__contains__.assert_called_once_with('FakeDeploy') - mock_deprecate.add.assert_called_once_with('FakeDeploy') - - # Make sure the deprecation warning isn't logged again - mock_log.reset_mock() - mock_deprecate.add.reset_mock() - manager._old_rest_of_do_node_deploy(task, self.service.conductor.id, - True) - self.assertFalse(mock_log.warning.called) - mock_deprecate.__contains__.assert_has_calls( - [mock.call('FakeDeploy'), mock.call('FakeDeploy')]) - self.assertFalse(mock_deprecate.add.called) - - @mock.patch.object(manager, 'LOG', autospec=True) - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy', autospec=True) - def test__old_rest_of_do_node_deploy_has_steps(self, mock_deploy, - mock_log): - # TODO(rloo): has steps but old RPC; delete this when we remove support - # for handling no deploy steps. - manager._SEEN_NO_DEPLOY_STEP_DEPRECATIONS = set() - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._old_rest_of_do_node_deploy(task, self.service.conductor.id, - False) - mock_deploy.assert_called_once_with(mock.ANY, task) - self.assertFalse(mock_log.warning.called) - self.assertEqual(self.service.conductor.id, - task.node.conductor_affinity) - - @mock.patch('ironic.conductor.manager._start_console_in_deploy', - autospec=True) - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.deploy', autospec=True) - def test__old_rest_of_do_node_deploy_console(self, mock_deploy, - mock_console): - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - mock_deploy.return_value = states.DEPLOYDONE - - manager._old_rest_of_do_node_deploy(task, self.service.conductor.id, - True) - mock_deploy.assert_called_once_with(mock.ANY, task) - mock_console.assert_called_once_with(task) - self.assertEqual(self.service.conductor.id, - task.node.conductor_affinity) - - -@mgr_utils.mock_record_keepalive -class DoNextDeployStepTestCase(mgr_utils.ServiceSetUpMixin, - db_base.DbTestCase): - def setUp(self): - super(DoNextDeployStepTestCase, self).setUp() - self.deploy_start = { - 'step': 'deploy_start', 'priority': 50, 'interface': 'deploy'} - self.deploy_end = { - 'step': 'deploy_end', 'priority': 20, 'interface': 'deploy'} - self.deploy_steps = [self.deploy_start, self.deploy_end] - - @mock.patch.object(manager, 'LOG', autospec=True) - def test__do_next_deploy_step_none(self, mock_log): - self._start_service() - node = obj_utils.create_test_node(self.context, driver='fake-hardware') - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._do_next_deploy_step(task, None, self.service.conductor.id) - - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(2, mock_log.info.call_count) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def test__do_next_deploy_step_async(self, mock_execute): - driver_internal_info = {'deploy_step_index': None, - 'deploy_steps': self.deploy_steps} - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - driver_internal_info=driver_internal_info, - deploy_step={}) - mock_execute.return_value = states.DEPLOYWAIT - expected_first_step = node.driver_internal_info['deploy_steps'][0] - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._do_next_deploy_step(task, 0, self.service.conductor.id) - - node.refresh() - self.assertEqual(states.DEPLOYWAIT, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertEqual(expected_first_step, node.deploy_step) - self.assertEqual(0, node.driver_internal_info['deploy_step_index']) - self.assertEqual(self.service.conductor.id, node.conductor_affinity) - mock_execute.assert_called_once_with(mock.ANY, task, - self.deploy_steps[0]) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def test__do_next_deploy_step_continue_from_last_step(self, mock_execute): - # Resume an in-progress deploy after the first async step - driver_internal_info = {'deploy_step_index': 0, - 'deploy_steps': self.deploy_steps} - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.DEPLOYWAIT, - target_provision_state=states.ACTIVE, - driver_internal_info=driver_internal_info, - deploy_step=self.deploy_steps[0]) - mock_execute.return_value = states.DEPLOYWAIT - - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('resume') - - manager._do_next_deploy_step(task, 1, self.service.conductor.id) - node.refresh() - - self.assertEqual(states.DEPLOYWAIT, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertEqual(self.deploy_steps[1], node.deploy_step) - self.assertEqual(1, node.driver_internal_info['deploy_step_index']) - mock_execute.assert_called_once_with(mock.ANY, task, - self.deploy_steps[1]) - - @mock.patch('ironic.drivers.modules.fake.FakeConsole.start_console', - autospec=True) - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def _test__do_next_deploy_step_last_step_done(self, mock_execute, - mock_console, - console_enabled=False, - console_error=False): - # Resume where last_step is the last deploy step that was executed - driver_internal_info = {'deploy_step_index': 1, - 'deploy_steps': self.deploy_steps} - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.DEPLOYWAIT, - target_provision_state=states.ACTIVE, - driver_internal_info=driver_internal_info, - deploy_step=self.deploy_steps[1], - console_enabled=console_enabled) - mock_execute.return_value = None - if console_error: - mock_console.side_effect = exception.ConsoleError() - - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('resume') - - manager._do_next_deploy_step(task, None, self.service.conductor.id) - node.refresh() - # Deploying should be complete without calling additional steps - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - self.assertEqual({}, node.deploy_step) - self.assertNotIn('deploy_step_index', node.driver_internal_info) - self.assertIsNone(node.driver_internal_info['deploy_steps']) - self.assertFalse(mock_execute.called) - if console_enabled: - mock_console.assert_called_once_with(mock.ANY, task) - else: - self.assertFalse(mock_console.called) - - def test__do_next_deploy_step_last_step_done(self): - self._test__do_next_deploy_step_last_step_done() - - def test__do_next_deploy_step_last_step_done_with_console(self): - self._test__do_next_deploy_step_last_step_done(console_enabled=True) - - def test__do_next_deploy_step_last_step_done_with_console_error(self): - self._test__do_next_deploy_step_last_step_done(console_enabled=True, - console_error=True) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def test__do_next_deploy_step_all(self, mock_execute): - # Run all steps from start to finish (all synchronous) - driver_internal_info = {'deploy_step_index': None, - 'deploy_steps': self.deploy_steps, - 'agent_url': 'url'} - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - driver_internal_info=driver_internal_info, - deploy_step={}) - mock_execute.return_value = None - - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._do_next_deploy_step(task, 1, self.service.conductor.id) - - # Deploying should be complete - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - self.assertEqual({}, node.deploy_step) - self.assertNotIn('deploy_step_index', node.driver_internal_info) - self.assertIsNone(node.driver_internal_info['deploy_steps']) - mock_execute.assert_has_calls = [mock.call(self.deploy_steps[0]), - mock.call(self.deploy_steps[1])] - self.assertNotIn('agent_url', node.driver_internal_info) - - @mock.patch.object(conductor_utils, 'LOG', autospec=True) - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def _do_next_deploy_step_execute_fail(self, exc, traceback, - mock_execute, mock_log): - # When a deploy step fails, go to DEPLOYFAIL - driver_internal_info = {'deploy_step_index': None, - 'deploy_steps': self.deploy_steps} - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - driver_internal_info=driver_internal_info, - deploy_step={}) - mock_execute.side_effect = exc - - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._do_next_deploy_step(task, 0, self.service.conductor.id) - - # Make sure we go to DEPLOYFAIL, clear deploy_steps - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertEqual({}, node.deploy_step) - self.assertNotIn('deploy_step_index', node.driver_internal_info) - self.assertIsNotNone(node.last_error) - self.assertFalse(node.maintenance) - mock_execute.assert_called_once_with(mock.ANY, mock.ANY, - self.deploy_steps[0]) - mock_log.error.assert_called_once_with(mock.ANY, exc_info=traceback) - - def test_do_next_deploy_step_execute_ironic_exception(self): - self._do_next_deploy_step_execute_fail( - exception.IronicException('foo'), False) - - def test_do_next_deploy_step_execute_exception(self): - self._do_next_deploy_step_execute_fail(Exception('foo'), True) - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def test_do_next_deploy_step_no_steps(self, mock_execute): - - self._start_service() - for info in ({'deploy_steps': None, 'deploy_step_index': None}, - {'deploy_steps': None}): - # Resume where there are no steps, should be a noop - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - uuid=uuidutils.generate_uuid(), - last_error=None, - driver_internal_info=info, - deploy_step={}) - - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._do_next_deploy_step(task, None, self.service.conductor.id) - - # Deploying should be complete without calling additional steps - node.refresh() - self.assertEqual(states.ACTIVE, node.provision_state) - self.assertEqual(states.NOSTATE, node.target_provision_state) - self.assertEqual({}, node.deploy_step) - self.assertNotIn('deploy_step_index', node.driver_internal_info) - self.assertFalse(mock_execute.called) - mock_execute.reset_mock() - - @mock.patch('ironic.drivers.modules.fake.FakeDeploy.execute_deploy_step', - autospec=True) - def test_do_next_deploy_step_bad_step_return_value(self, mock_execute): - # When a deploy step fails, go to DEPLOYFAIL - self._start_service() - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - driver_internal_info={'deploy_steps': self.deploy_steps, - 'deploy_step_index': None}, - deploy_step={}) - mock_execute.return_value = "foo" - - task = task_manager.TaskManager(self.context, node.uuid) - task.process_event('deploy') - - manager._do_next_deploy_step(task, 0, self.service.conductor.id) - - # Make sure we go to DEPLOYFAIL, clear deploy_steps - node.refresh() - self.assertEqual(states.DEPLOYFAIL, node.provision_state) - self.assertEqual(states.ACTIVE, node.target_provision_state) - self.assertEqual({}, node.deploy_step) - self.assertNotIn('deploy_step_index', node.driver_internal_info) - self.assertIsNotNone(node.last_error) - self.assertFalse(node.maintenance) - mock_execute.assert_called_once_with(mock.ANY, mock.ANY, - self.deploy_steps[0]) - - def _test__get_node_next_deploy_steps(self, skip=True): - driver_internal_info = {'deploy_steps': self.deploy_steps, - 'deploy_step_index': 0} - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.DEPLOYWAIT, - target_provision_state=states.ACTIVE, - driver_internal_info=driver_internal_info, - last_error=None, - deploy_step=self.deploy_steps[0]) - - with task_manager.acquire(self.context, node.uuid) as task: - step_index = self.service._get_node_next_deploy_steps( - task, skip_current_step=skip) - expected_index = 1 if skip else 0 - self.assertEqual(expected_index, step_index) - - def test__get_node_next_deploy_steps(self): - self._test__get_node_next_deploy_steps() - - def test__get_node_next_deploy_steps_no_skip(self): - self._test__get_node_next_deploy_steps(skip=False) - - def test__get_node_next_deploy_steps_unset_deploy_step(self): - driver_internal_info = {'deploy_steps': self.deploy_steps, - 'deploy_step_index': None} - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.DEPLOYWAIT, - target_provision_state=states.ACTIVE, - driver_internal_info=driver_internal_info, - last_error=None, - deploy_step=None) - - with task_manager.acquire(self.context, node.uuid) as task: - step_index = self.service._get_node_next_deploy_steps(task) - self.assertEqual(0, step_index) - - def test__get_node_next_steps_exception(self): - node = obj_utils.create_test_node(self.context) - - with task_manager.acquire(self.context, node.uuid) as task: - self.assertRaises(exception.Invalid, - self.service._get_node_next_steps, task, 'foo') - @mgr_utils.mock_record_keepalive class CheckTimeoutsTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): @@ -4719,44 +3793,6 @@ class DoNodeCleanTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): def test__do_next_clean_step_manual_bad_step_return_value(self): self._do_next_clean_step_bad_step_return_value(manual=True) - def __get_node_next_clean_steps(self, skip=True): - driver_internal_info = {'clean_steps': self.clean_steps, - 'clean_step_index': 0} - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.CLEANWAIT, - target_provision_state=states.AVAILABLE, - driver_internal_info=driver_internal_info, - last_error=None, - clean_step=self.clean_steps[0]) - - with task_manager.acquire(self.context, node.uuid) as task: - step_index = self.service._get_node_next_clean_steps( - task, skip_current_step=skip) - expected_index = 1 if skip else 0 - self.assertEqual(expected_index, step_index) - - def test__get_node_next_clean_steps(self): - self.__get_node_next_clean_steps() - - def test__get_node_next_clean_steps_no_skip(self): - self.__get_node_next_clean_steps(skip=False) - - def test__get_node_next_clean_steps_unset_clean_step(self): - driver_internal_info = {'clean_steps': self.clean_steps, - 'clean_step_index': None} - node = obj_utils.create_test_node( - self.context, driver='fake-hardware', - provision_state=states.CLEANWAIT, - target_provision_state=states.AVAILABLE, - driver_internal_info=driver_internal_info, - last_error=None, - clean_step=None) - - with task_manager.acquire(self.context, node.uuid) as task: - step_index = self.service._get_node_next_clean_steps(task) - self.assertEqual(0, step_index) - class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): @@ -7935,105 +6971,6 @@ class ManagerSyncLocalStateTestCase(mgr_utils.CommonMixIn, db_base.DbTestCase): self.service._do_takeover, self.task) -@mock.patch.object(swift, 'SwiftAPI') -class StoreConfigDriveTestCase(db_base.DbTestCase): - - def setUp(self): - super(StoreConfigDriveTestCase, self).setUp() - self.node = obj_utils.create_test_node(self.context, - driver='fake-hardware', - instance_info=None) - - def test_store_configdrive(self, mock_swift): - manager._store_configdrive(self.node, 'foo') - expected_instance_info = {'configdrive': 'foo'} - self.node.refresh() - self.assertEqual(expected_instance_info, self.node.instance_info) - self.assertFalse(mock_swift.called) - - def test_store_configdrive_swift(self, mock_swift): - container_name = 'foo_container' - timeout = 123 - expected_obj_name = 'configdrive-%s' % self.node.uuid - expected_obj_header = {'X-Delete-After': str(timeout)} - expected_instance_info = {'configdrive': 'http://1.2.3.4'} - - # set configs and mocks - CONF.set_override('configdrive_use_object_store', True, - group='deploy') - CONF.set_override('configdrive_swift_container', container_name, - group='conductor') - CONF.set_override('deploy_callback_timeout', timeout, - group='conductor') - mock_swift.return_value.get_temp_url.return_value = 'http://1.2.3.4' - - manager._store_configdrive(self.node, b'foo') - - mock_swift.assert_called_once_with() - mock_swift.return_value.create_object.assert_called_once_with( - container_name, expected_obj_name, mock.ANY, - object_headers=expected_obj_header) - mock_swift.return_value.get_temp_url.assert_called_once_with( - container_name, expected_obj_name, timeout) - self.node.refresh() - self.assertEqual(expected_instance_info, self.node.instance_info) - - def test_store_configdrive_swift_no_deploy_timeout(self, mock_swift): - container_name = 'foo_container' - expected_obj_name = 'configdrive-%s' % self.node.uuid - expected_obj_header = {'X-Delete-After': '1200'} - expected_instance_info = {'configdrive': 'http://1.2.3.4'} - - # set configs and mocks - CONF.set_override('configdrive_use_object_store', True, - group='deploy') - CONF.set_override('configdrive_swift_container', container_name, - group='conductor') - CONF.set_override('configdrive_swift_temp_url_duration', 1200, - group='conductor') - CONF.set_override('deploy_callback_timeout', 0, - group='conductor') - mock_swift.return_value.get_temp_url.return_value = 'http://1.2.3.4' - - manager._store_configdrive(self.node, b'foo') - - mock_swift.assert_called_once_with() - mock_swift.return_value.create_object.assert_called_once_with( - container_name, expected_obj_name, mock.ANY, - object_headers=expected_obj_header) - mock_swift.return_value.get_temp_url.assert_called_once_with( - container_name, expected_obj_name, 1200) - self.node.refresh() - self.assertEqual(expected_instance_info, self.node.instance_info) - - def test_store_configdrive_swift_no_deploy_timeout_fallback(self, - mock_swift): - container_name = 'foo_container' - expected_obj_name = 'configdrive-%s' % self.node.uuid - expected_obj_header = {'X-Delete-After': '1800'} - expected_instance_info = {'configdrive': 'http://1.2.3.4'} - - # set configs and mocks - CONF.set_override('configdrive_use_object_store', True, - group='deploy') - CONF.set_override('configdrive_swift_container', container_name, - group='conductor') - CONF.set_override('deploy_callback_timeout', 0, - group='conductor') - mock_swift.return_value.get_temp_url.return_value = 'http://1.2.3.4' - - manager._store_configdrive(self.node, b'foo') - - mock_swift.assert_called_once_with() - mock_swift.return_value.create_object.assert_called_once_with( - container_name, expected_obj_name, mock.ANY, - object_headers=expected_obj_header) - mock_swift.return_value.get_temp_url.assert_called_once_with( - container_name, expected_obj_name, 1800) - self.node.refresh() - self.assertEqual(expected_instance_info, self.node.instance_info) - - @mgr_utils.mock_record_keepalive class NodeInspectHardware(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): diff --git a/ironic/tests/unit/conductor/test_utils.py b/ironic/tests/unit/conductor/test_utils.py index 86d51b7055..482387ddfd 100644 --- a/ironic/tests/unit/conductor/test_utils.py +++ b/ironic/tests/unit/conductor/test_utils.py @@ -1915,3 +1915,106 @@ class FastTrackTestCase(db_base.DbTestCase): with task_manager.acquire( self.context, self.node.uuid, shared=False) as task: self.assertFalse(conductor_utils.is_fast_track(task)) + + +class GetNodeNextStepsTestCase(db_base.DbTestCase): + def setUp(self): + super(GetNodeNextStepsTestCase, self).setUp() + self.power_update = { + 'step': 'update_firmware', 'priority': 10, 'interface': 'power'} + self.deploy_update = { + 'step': 'update_firmware', 'priority': 10, 'interface': 'deploy'} + self.deploy_erase = { + 'step': 'erase_disks', 'priority': 20, 'interface': 'deploy'} + # Automated cleaning should be executed in this order + self.clean_steps = [self.deploy_erase, self.power_update, + self.deploy_update] + self.deploy_start = { + 'step': 'deploy_start', 'priority': 50, 'interface': 'deploy'} + self.deploy_end = { + 'step': 'deploy_end', 'priority': 20, 'interface': 'deploy'} + self.deploy_steps = [self.deploy_start, self.deploy_end] + + def _test_get_node_next_deploy_steps(self, skip=True): + driver_internal_info = {'deploy_steps': self.deploy_steps, + 'deploy_step_index': 0} + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.DEPLOYWAIT, + target_provision_state=states.ACTIVE, + driver_internal_info=driver_internal_info, + last_error=None, + deploy_step=self.deploy_steps[0]) + + with task_manager.acquire(self.context, node.uuid) as task: + step_index = conductor_utils.get_node_next_deploy_steps( + task, skip_current_step=skip) + expected_index = 1 if skip else 0 + self.assertEqual(expected_index, step_index) + + def test_get_node_next_deploy_steps(self): + self._test_get_node_next_deploy_steps() + + def test_get_node_next_deploy_steps_no_skip(self): + self._test_get_node_next_deploy_steps(skip=False) + + def test_get_node_next_deploy_steps_unset_deploy_step(self): + driver_internal_info = {'deploy_steps': self.deploy_steps, + 'deploy_step_index': None} + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.DEPLOYWAIT, + target_provision_state=states.ACTIVE, + driver_internal_info=driver_internal_info, + last_error=None, + deploy_step=None) + + with task_manager.acquire(self.context, node.uuid) as task: + step_index = conductor_utils.get_node_next_deploy_steps(task) + self.assertEqual(0, step_index) + + def test_get_node_next_steps_exception(self): + node = obj_utils.create_test_node(self.context) + + with task_manager.acquire(self.context, node.uuid) as task: + self.assertRaises(exception.Invalid, + conductor_utils._get_node_next_steps, + task, 'foo') + + def _test_get_node_next_clean_steps(self, skip=True): + driver_internal_info = {'clean_steps': self.clean_steps, + 'clean_step_index': 0} + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.CLEANWAIT, + target_provision_state=states.AVAILABLE, + driver_internal_info=driver_internal_info, + last_error=None, + clean_step=self.clean_steps[0]) + + with task_manager.acquire(self.context, node.uuid) as task: + step_index = conductor_utils.get_node_next_clean_steps( + task, skip_current_step=skip) + expected_index = 1 if skip else 0 + self.assertEqual(expected_index, step_index) + + def test_get_node_next_clean_steps(self): + self._test_get_node_next_clean_steps() + + def test_get_node_next_clean_steps_no_skip(self): + self._test_get_node_next_clean_steps(skip=False) + + def test_get_node_next_clean_steps_unset_clean_step(self): + driver_internal_info = {'clean_steps': self.clean_steps, + 'clean_step_index': None} + node = obj_utils.create_test_node( + self.context, driver='fake-hardware', + provision_state=states.CLEANWAIT, + target_provision_state=states.AVAILABLE, + driver_internal_info=driver_internal_info, + last_error=None, + clean_step=None) + + with task_manager.acquire(self.context, node.uuid) as task: + step_index = conductor_utils.get_node_next_clean_steps(task) + self.assertEqual(0, step_index)