"Fossies" - the Fresh Open Source Software Archive

Member "ironic-16.0.3/ironic/conductor/utils.py" (18 Jan 2021, 52047 Bytes) of package /linux/misc/openstack/ironic-16.0.3.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "utils.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.2_vs_16.0.3.

    1 # coding=utf-8
    2 
    3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    4 #    not use this file except in compliance with the License. You may obtain
    5 #    a copy of the License at
    6 #
    7 #         http://www.apache.org/licenses/LICENSE-2.0
    8 #
    9 #    Unless required by applicable law or agreed to in writing, software
   10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   12 #    License for the specific language governing permissions and limitations
   13 #    under the License.
   14 
   15 import contextlib
   16 import crypt
   17 import datetime
   18 import functools
   19 import os
   20 import secrets
   21 import time
   22 
   23 from openstack.baremetal import configdrive as os_configdrive
   24 from oslo_config import cfg
   25 from oslo_log import log
   26 from oslo_serialization import jsonutils
   27 from oslo_service import loopingcall
   28 from oslo_utils import excutils
   29 from oslo_utils import timeutils
   30 
   31 from ironic.common import boot_devices
   32 from ironic.common import exception
   33 from ironic.common import faults
   34 from ironic.common.i18n import _
   35 from ironic.common import network
   36 from ironic.common import nova
   37 from ironic.common import states
   38 from ironic.conductor import notification_utils as notify_utils
   39 from ironic.conductor import task_manager
   40 from ironic.objects import fields
   41 
   42 LOG = log.getLogger(__name__)
   43 CONF = cfg.CONF
   44 
   45 
   46 PASSWORD_HASH_FORMAT = {
   47     'sha256': crypt.METHOD_SHA256,
   48     'sha512': crypt.METHOD_SHA512,
   49 }
   50 
   51 
   52 @task_manager.require_exclusive_lock
   53 def node_set_boot_device(task, device, persistent=False):
   54     """Set the boot device for a node.
   55 
   56     If the node that the boot device change is being requested for
   57     is in ADOPTING state, the boot device will not be set as that
   58     change could potentially result in the future running state of
   59     an adopted node being modified erroneously.
   60 
   61     :param task: a TaskManager instance.
   62     :param device: Boot device. Values are vendor-specific.
   63     :param persistent: Whether to set next-boot, or make the change
   64         permanent. Default: False.
   65     :raises: InvalidParameterValue if the validation of the
   66         ManagementInterface fails.
   67 
   68     """
   69     task.driver.management.validate(task)
   70     if task.node.provision_state != states.ADOPTING:
   71         task.driver.management.set_boot_device(task,
   72                                                device=device,
   73                                                persistent=persistent)
   74 
   75 
   76 def node_get_boot_mode(task):
   77     """Read currently set boot mode from a node.
   78 
   79     Reads the boot mode for a node. If boot mode can't be discovered,
   80     `None` is returned.
   81 
   82     :param task: a TaskManager instance.
   83     :raises: DriverOperationError or its derivative in case
   84              of driver runtime error.
   85     :raises: UnsupportedDriverExtension if current driver does not have
   86              management interface or `get_boot_mode()` method is
   87              not supported.
   88     :returns: Boot mode. One of :mod:`ironic.common.boot_mode` or `None`
   89         if boot mode can't be discovered
   90     """
   91     task.driver.management.validate(task)
   92     return task.driver.management.get_boot_mode(task)
   93 
   94 
   95 # TODO(ietingof): remove `Sets the boot mode...` from the docstring
   96 # once classic drivers are gone
   97 @task_manager.require_exclusive_lock
   98 def node_set_boot_mode(task, mode):
   99     """Set the boot mode for a node.
  100 
  101     Sets the boot mode for a node if the node's driver interface
  102     contains a 'management' interface.
  103 
  104     If the node that the boot mode change is being requested for
  105     is in ADOPTING state, the boot mode will not be set as that
  106     change could potentially result in the future running state of
  107     an adopted node being modified erroneously.
  108 
  109     :param task: a TaskManager instance.
  110     :param mode: Boot mode. Values are one of
  111         :mod:`ironic.common.boot_modes`
  112     :raises: InvalidParameterValue if the validation of the
  113              ManagementInterface fails.
  114     :raises: DriverOperationError or its derivative in case
  115              of driver runtime error.
  116     :raises: UnsupportedDriverExtension if current driver does not have
  117              vendor interface or method is unsupported.
  118     """
  119     if task.node.provision_state == states.ADOPTING:
  120         return
  121 
  122     task.driver.management.validate(task)
  123 
  124     boot_modes = task.driver.management.get_supported_boot_modes(task)
  125 
  126     if mode not in boot_modes:
  127         msg = _("Unsupported boot mode %(mode)s specified for "
  128                 "node %(node_id)s. Supported boot modes are: "
  129                 "%(modes)s") % {'mode': mode,
  130                                 'modes': ', '.join(boot_modes),
  131                                 'node_id': task.node.uuid}
  132         raise exception.InvalidParameterValue(msg)
  133 
  134     task.driver.management.set_boot_mode(task, mode=mode)
  135 
  136 
  137 def node_wait_for_power_state(task, new_state, timeout=None):
  138     """Wait for node to be in new power state.
  139 
  140     :param task: a TaskManager instance.
  141     :param new_state: the desired new power state, one of the power states
  142         in :mod:`ironic.common.states`.
  143     :param timeout: number of seconds to wait before giving up. If not
  144         specified, uses the conductor.power_state_change_timeout config value.
  145     :raises: PowerStateFailure if timed out
  146     """
  147     retry_timeout = (timeout or CONF.conductor.power_state_change_timeout)
  148 
  149     def _wait():
  150         status = task.driver.power.get_power_state(task)
  151         if status == new_state:
  152             raise loopingcall.LoopingCallDone(retvalue=status)
  153         # NOTE(sambetts): Return False to trigger BackOffLoopingCall to start
  154         # backing off.
  155         return False
  156 
  157     try:
  158         timer = loopingcall.BackOffLoopingCall(_wait)
  159         return timer.start(initial_delay=1, timeout=retry_timeout).wait()
  160     except loopingcall.LoopingCallTimeOut:
  161         LOG.error('Timed out after %(retry_timeout)s secs waiting for '
  162                   '%(state)s on node %(node_id)s.',
  163                   {'retry_timeout': retry_timeout,
  164                    'state': new_state, 'node_id': task.node.uuid})
  165         raise exception.PowerStateFailure(pstate=new_state)
  166 
  167 
  168 def _calculate_target_state(new_state):
  169     if new_state in (states.POWER_ON, states.REBOOT, states.SOFT_REBOOT):
  170         target_state = states.POWER_ON
  171     elif new_state in (states.POWER_OFF, states.SOFT_POWER_OFF):
  172         target_state = states.POWER_OFF
  173     else:
  174         target_state = None
  175     return target_state
  176 
  177 
  178 def _can_skip_state_change(task, new_state):
  179     """Check if we can ignore the power state change request for the node.
  180 
  181     Check if we should ignore the requested power state change. This can occur
  182     if the requested power state is already the same as our current state. This
  183     only works for power on and power off state changes. More complex power
  184     state changes, like reboot, are not skipped.
  185 
  186     :param task: a TaskManager instance containing the node to act on.
  187     :param new_state: The requested power state to change to. This can be any
  188                       power state from ironic.common.states.
  189     :returns: True if should ignore the requested power state change. False
  190               otherwise
  191     """
  192     # We only ignore certain state changes. So if the desired new_state is not
  193     # one of them, then we can return early and not do an un-needed
  194     # get_power_state() call
  195     if new_state not in (states.POWER_ON, states.POWER_OFF,
  196                          states.SOFT_POWER_OFF):
  197         return False
  198 
  199     node = task.node
  200 
  201     def _not_going_to_change():
  202         # Neither the ironic service nor the hardware has erred. The
  203         # node is, for some reason, already in the requested state,
  204         # though we don't know why. eg, perhaps the user previously
  205         # requested the node POWER_ON, the network delayed those IPMI
  206         # packets, and they are trying again -- but the node finally
  207         # responds to the first request, and so the second request
  208         # gets to this check and stops.
  209         # This isn't an error, so we'll clear last_error field
  210         # (from previous operation), log a warning, and return.
  211         node['last_error'] = None
  212         # NOTE(dtantsur): under rare conditions we can get out of sync here
  213         node['power_state'] = curr_state
  214         node['target_power_state'] = states.NOSTATE
  215         node.save()
  216         notify_utils.emit_power_set_notification(
  217             task, fields.NotificationLevel.INFO,
  218             fields.NotificationStatus.END, new_state)
  219         LOG.warning("Not going to change node %(node)s power state because "
  220                     "current state = requested state = '%(state)s'.",
  221                     {'node': node.uuid, 'state': curr_state})
  222 
  223     try:
  224         curr_state = task.driver.power.get_power_state(task)
  225     except Exception as e:
  226         with excutils.save_and_reraise_exception():
  227             node['last_error'] = _(
  228                 "Failed to change power state to '%(target)s'. "
  229                 "Error: %(error)s") % {'target': new_state, 'error': e}
  230             node['target_power_state'] = states.NOSTATE
  231             node.save()
  232             notify_utils.emit_power_set_notification(
  233                 task, fields.NotificationLevel.ERROR,
  234                 fields.NotificationStatus.ERROR, new_state)
  235 
  236     if curr_state == states.POWER_ON:
  237         if new_state == states.POWER_ON:
  238             _not_going_to_change()
  239             return True
  240     elif curr_state == states.POWER_OFF:
  241         if new_state in (states.POWER_OFF, states.SOFT_POWER_OFF):
  242             _not_going_to_change()
  243             return True
  244     else:
  245         # if curr_state == states.ERROR:
  246         # be optimistic and continue action
  247         LOG.warning("Driver returns ERROR power state for node %s.",
  248                     node.uuid)
  249     return False
  250 
  251 
  252 @task_manager.require_exclusive_lock
  253 def node_power_action(task, new_state, timeout=None):
  254     """Change power state or reset for a node.
  255 
  256     Perform the requested power action if the transition is required.
  257 
  258     :param task: a TaskManager instance containing the node to act on.
  259     :param new_state: Any power state from ironic.common.states.
  260     :param timeout: timeout (in seconds) positive integer (> 0) for any
  261       power state. ``None`` indicates to use default timeout.
  262     :raises: InvalidParameterValue when the wrong state is specified
  263              or the wrong driver info is specified.
  264     :raises: StorageError when a failure occurs updating the node's
  265              storage interface upon setting power on.
  266     :raises: other exceptions by the node's power driver if something
  267              wrong occurred during the power action.
  268 
  269     """
  270     notify_utils.emit_power_set_notification(
  271         task, fields.NotificationLevel.INFO, fields.NotificationStatus.START,
  272         new_state)
  273     node = task.node
  274 
  275     if _can_skip_state_change(task, new_state):
  276         return
  277     target_state = _calculate_target_state(new_state)
  278 
  279     # Set the target_power_state and clear any last_error, if we're
  280     # starting a new operation. This will expose to other processes
  281     # and clients that work is in progress.
  282     node['target_power_state'] = target_state
  283     node['last_error'] = None
  284     driver_internal_info = node.driver_internal_info
  285     driver_internal_info['last_power_state_change'] = str(
  286         timeutils.utcnow().isoformat())
  287     node.driver_internal_info = driver_internal_info
  288     # NOTE(dtantsur): wipe token on shutting down, otherwise a reboot in
  289     # fast-track (or an accidentally booted agent) will cause subsequent
  290     # actions to fail.
  291     if new_state in (states.POWER_OFF, states.SOFT_POWER_OFF,
  292                      states.REBOOT, states.SOFT_REBOOT):
  293         wipe_internal_info_on_power_off(node)
  294     node.save()
  295 
  296     # take power action
  297     try:
  298         if (target_state == states.POWER_ON
  299                 and node.provision_state == states.ACTIVE):
  300             task.driver.storage.attach_volumes(task)
  301 
  302         if new_state != states.REBOOT:
  303             task.driver.power.set_power_state(task, new_state, timeout=timeout)
  304         else:
  305             # TODO(TheJulia): We likely ought to consider toggling
  306             # volume attachments, although we have no mechanism to
  307             # really verify what cinder has connector wise.
  308             task.driver.power.reboot(task, timeout=timeout)
  309     except Exception as e:
  310         with excutils.save_and_reraise_exception():
  311             node['target_power_state'] = states.NOSTATE
  312             node['last_error'] = _(
  313                 "Failed to change power state to '%(target_state)s' "
  314                 "by '%(new_state)s'. Error: %(error)s") % {
  315                     'target_state': target_state,
  316                     'new_state': new_state,
  317                     'error': e}
  318             node.save()
  319             notify_utils.emit_power_set_notification(
  320                 task, fields.NotificationLevel.ERROR,
  321                 fields.NotificationStatus.ERROR, new_state)
  322     else:
  323         # success!
  324         node['power_state'] = target_state
  325         node['target_power_state'] = states.NOSTATE
  326         node.save()
  327         if node.instance_uuid:
  328             nova.power_update(
  329                 task.context, node.instance_uuid, target_state)
  330         notify_utils.emit_power_set_notification(
  331             task, fields.NotificationLevel.INFO, fields.NotificationStatus.END,
  332             new_state)
  333         LOG.info('Successfully set node %(node)s power state to '
  334                  '%(target_state)s by %(new_state)s.',
  335                  {'node': node.uuid,
  336                   'target_state': target_state,
  337                   'new_state': new_state})
  338         # NOTE(TheJulia): Similarly to power-on, when we power-off
  339         # a node, we should detach any volume attachments.
  340         if (target_state == states.POWER_OFF
  341                 and node.provision_state == states.ACTIVE):
  342             try:
  343                 task.driver.storage.detach_volumes(task)
  344             except exception.StorageError as e:
  345                 LOG.warning("Volume detachment for node %(node)s "
  346                             "failed. Error: %(error)s",
  347                             {'node': node.uuid, 'error': e})
  348 
  349 
  350 @task_manager.require_exclusive_lock
  351 def cleanup_after_timeout(task):
  352     """Cleanup deploy task after timeout.
  353 
  354     :param task: a TaskManager instance.
  355     """
  356     msg = (_('Timeout reached while waiting for callback for node %s')
  357            % task.node.uuid)
  358     deploying_error_handler(task, msg, msg)
  359 
  360 
  361 def provisioning_error_handler(e, node, provision_state,
  362                                target_provision_state):
  363     """Set the node's provisioning states if error occurs.
  364 
  365     This hook gets called upon an exception being raised when spawning
  366     the worker to do some provisioning to a node like deployment, tear down,
  367     or cleaning.
  368 
  369     :param e: the exception object that was raised.
  370     :param node: an Ironic node object.
  371     :param provision_state: the provision state to be set on
  372         the node.
  373     :param target_provision_state: the target provision state to be
  374         set on the node.
  375 
  376     """
  377     if isinstance(e, exception.NoFreeConductorWorker):
  378         # NOTE(tenbrae): there is no need to clear conductor_affinity
  379         #             because it isn't updated on a failed deploy
  380         node.provision_state = provision_state
  381         node.target_provision_state = target_provision_state
  382         node.last_error = (_("No free conductor workers available"))
  383         node.save()
  384         LOG.warning("No free conductor workers available to perform "
  385                     "an action on node %(node)s, setting node's "
  386                     "provision_state back to %(prov_state)s and "
  387                     "target_provision_state to %(tgt_prov_state)s.",
  388                     {'node': node.uuid, 'prov_state': provision_state,
  389                      'tgt_prov_state': target_provision_state})
  390 
  391 
  392 def cleanup_cleanwait_timeout(task):
  393     """Cleanup a cleaning task after timeout.
  394 
  395     :param task: a TaskManager instance.
  396     """
  397     last_error = (_("Timeout reached while cleaning the node. Please "
  398                     "check if the ramdisk responsible for the cleaning is "
  399                     "running on the node. Failed on step %(step)s.") %
  400                   {'step': task.node.clean_step})
  401     logmsg = ("Cleaning for node %(node)s failed. %(error)s" %
  402               {'node': task.node.uuid, 'error': last_error})
  403     # NOTE(rloo): this is called from the periodic task for cleanwait timeouts,
  404     # via the task manager's process_event(). The node has already been moved
  405     # to CLEANFAIL, so the error handler doesn't need to set the fail state.
  406     cleaning_error_handler(task, logmsg, errmsg=last_error,
  407                            set_fail_state=False)
  408 
  409 
  410 def cleaning_error_handler(task, logmsg, errmsg=None, traceback=False,
  411                            tear_down_cleaning=True, set_fail_state=True,
  412                            set_maintenance=None):
  413     """Put a failed node in CLEANFAIL and maintenance (if needed).
  414 
  415     :param task: a TaskManager instance.
  416     :param logmsg: Message to be logged.
  417     :param errmsg: Message for the user. Optional, if not provided `logmsg` is
  418         used.
  419     :param traceback: Whether to log a traceback. Defaults to False.
  420     :param tear_down_cleaning: Whether to clean up the PXE and DHCP files after
  421         cleaning. Default to True.
  422     :param set_fail_state: Whether to set node to failed state. Default to
  423         True.
  424     :param set_maintenance: Whether to set maintenance mode. If None,
  425         maintenance mode will be set if and only if a clean step is being
  426         executed on a node.
  427     """
  428     if set_maintenance is None:
  429         set_maintenance = bool(task.node.clean_step)
  430 
  431     errmsg = errmsg or logmsg
  432     LOG.error(logmsg, exc_info=traceback)
  433     node = task.node
  434     if set_maintenance:
  435         node.fault = faults.CLEAN_FAILURE
  436         node.maintenance = True
  437 
  438     if tear_down_cleaning:
  439         try:
  440             task.driver.deploy.tear_down_cleaning(task)
  441         except Exception as e:
  442             msg2 = ('Failed to tear down cleaning on node %(uuid)s, '
  443                     'reason: %(err)s' % {'err': e, 'uuid': node.uuid})
  444             LOG.exception(msg2)
  445             errmsg = _('%s. Also failed to tear down cleaning.') % errmsg
  446 
  447     if node.provision_state in (
  448             states.CLEANING,
  449             states.CLEANWAIT,
  450             states.CLEANFAIL):
  451         # Clear clean step, msg should already include current step
  452         node.clean_step = {}
  453         info = node.driver_internal_info
  454         # Clear any leftover metadata about cleaning
  455         info.pop('clean_step_index', None)
  456         info.pop('cleaning_reboot', None)
  457         info.pop('cleaning_polling', None)
  458         info.pop('skip_current_clean_step', None)
  459         # We don't need to keep the old agent URL
  460         # as it should change upon the next cleaning attempt.
  461         info.pop('agent_url', None)
  462         node.driver_internal_info = info
  463     # For manual cleaning, the target provision state is MANAGEABLE, whereas
  464     # for automated cleaning, it is AVAILABLE.
  465     manual_clean = node.target_provision_state == states.MANAGEABLE
  466     node.last_error = errmsg
  467     # NOTE(dtantsur): avoid overwriting existing maintenance_reason
  468     if not node.maintenance_reason and set_maintenance:
  469         node.maintenance_reason = errmsg
  470     node.save()
  471 
  472     if set_fail_state and node.provision_state != states.CLEANFAIL:
  473         target_state = states.MANAGEABLE if manual_clean else None
  474         task.process_event('fail', target_state=target_state)
  475 
  476 
  477 def wipe_internal_info_on_power_off(node):
  478     """Wipe information that should not survive reboot/power off."""
  479     driver_internal_info = node.driver_internal_info
  480     # DHCP may result in a new IP next time.
  481     driver_internal_info.pop('agent_url', None)
  482     if not is_agent_token_pregenerated(node):
  483         # Wipe the token if it's not pre-generated, otherwise we'll refuse to
  484         # generate it again for the newly booted agent.
  485         driver_internal_info.pop('agent_secret_token', False)
  486     # Wipe cached steps since they may change after reboot.
  487     driver_internal_info.pop('agent_cached_deploy_steps', None)
  488     driver_internal_info.pop('agent_cached_clean_steps', None)
  489     # Remove TLS certificate since it's regenerated on each run.
  490     driver_internal_info.pop('agent_verify_ca', None)
  491     node.driver_internal_info = driver_internal_info
  492 
  493 
  494 def wipe_token_and_url(task):
  495     """Remove agent URL and token from the task."""
  496     info = task.node.driver_internal_info
  497     info.pop('agent_secret_token', None)
  498     info.pop('agent_secret_token_pregenerated', None)
  499     # Remove agent_url since it will be re-asserted
  500     # upon the next deployment attempt.
  501     info.pop('agent_url', None)
  502     # Remove TLS certificate since it's regenerated on each run.
  503     info.pop('agent_verify_ca', None)
  504     task.node.driver_internal_info = info
  505 
  506 
  507 def wipe_deploy_internal_info(task):
  508     """Remove temporary deployment fields from driver_internal_info."""
  509     if not fast_track_able(task):
  510         wipe_token_and_url(task)
  511     # Clear any leftover metadata about deployment.
  512     info = task.node.driver_internal_info
  513     info['deploy_steps'] = None
  514     info.pop('agent_cached_deploy_steps', None)
  515     info.pop('deploy_step_index', None)
  516     info.pop('deployment_reboot', None)
  517     info.pop('deployment_polling', None)
  518     info.pop('skip_current_deploy_step', None)
  519     info.pop('steps_validated', None)
  520     task.node.driver_internal_info = info
  521 
  522 
  523 def wipe_cleaning_internal_info(task):
  524     """Remove temporary cleaning fields from driver_internal_info."""
  525     if not fast_track_able(task):
  526         wipe_token_and_url(task)
  527     info = task.node.driver_internal_info
  528     info['clean_steps'] = None
  529     info.pop('agent_cached_clean_steps', None)
  530     info.pop('clean_step_index', None)
  531     info.pop('cleaning_reboot', None)
  532     info.pop('cleaning_polling', None)
  533     info.pop('skip_current_clean_step', None)
  534     info.pop('steps_validated', None)
  535     task.node.driver_internal_info = info
  536 
  537 
  538 def deploying_error_handler(task, logmsg, errmsg=None, traceback=False,
  539                             clean_up=True):
  540     """Put a failed node in DEPLOYFAIL.
  541 
  542     :param task: the task
  543     :param logmsg: message to be logged
  544     :param errmsg: message for the user
  545     :param traceback: Boolean; True to log a traceback
  546     :param clean_up: Boolean; True to clean up
  547     """
  548     errmsg = errmsg or logmsg
  549     node = task.node
  550     LOG.error(logmsg, exc_info=traceback)
  551     node.last_error = errmsg
  552     node.save()
  553 
  554     cleanup_err = None
  555     if clean_up:
  556         try:
  557             task.driver.deploy.clean_up(task)
  558         except Exception as e:
  559             msg = ('Cleanup failed for node %(node)s; reason: %(err)s'
  560                    % {'node': node.uuid, 'err': e})
  561             LOG.exception(msg)
  562             if isinstance(e, exception.IronicException):
  563                 addl = _('Also failed to clean up due to: %s') % e
  564             else:
  565                 addl = _('An unhandled exception was encountered while '
  566                          'aborting. More information may be found in the log '
  567                          'file.')
  568             cleanup_err = '%(err)s. %(add)s' % {'err': errmsg, 'add': addl}
  569 
  570     node.refresh()
  571     if node.provision_state in (
  572             states.DEPLOYING,
  573             states.DEPLOYWAIT,
  574             states.DEPLOYFAIL):
  575         # Clear deploy step; we leave the list of deploy steps
  576         # in node.driver_internal_info for debugging purposes.
  577         node.deploy_step = {}
  578         wipe_deploy_internal_info(task)
  579 
  580     if cleanup_err:
  581         node.last_error = cleanup_err
  582     node.save()
  583 
  584     # NOTE(tenbrae): there is no need to clear conductor_affinity
  585     task.process_event('fail')
  586 
  587 
  588 def fail_on_error(error_callback, msg, *error_args, **error_kwargs):
  589     """A decorator for failing operation on failure."""
  590     def wrapper(func):
  591         @functools.wraps(func)
  592         def wrapped(task, *args, **kwargs):
  593             try:
  594                 return func(task, *args, **kwargs)
  595             except Exception as exc:
  596                 errmsg = "%s. %s: %s" % (msg, exc.__class__.__name__, exc)
  597                 error_callback(task, errmsg, *error_args, **error_kwargs)
  598 
  599         return wrapped
  600     return wrapper
  601 
  602 
  603 @task_manager.require_exclusive_lock
  604 def abort_on_conductor_take_over(task):
  605     """Set node's state when a task was aborted due to conductor take over.
  606 
  607     :param task: a TaskManager instance.
  608     """
  609     msg = _('Operation was aborted due to conductor take over')
  610     # By this time the "fail" even was processed, so we cannot end up in
  611     # CLEANING or CLEAN WAIT, only in CLEAN FAIL.
  612     if task.node.provision_state == states.CLEANFAIL:
  613         cleaning_error_handler(task, msg, set_fail_state=False)
  614     else:
  615         # For aborted deployment (and potentially other operations), just set
  616         # the last_error accordingly.
  617         task.node.last_error = msg
  618         task.node.save()
  619 
  620     LOG.warning('Aborted the current operation on node %s due to '
  621                 'conductor take over', task.node.uuid)
  622 
  623 
  624 def rescuing_error_handler(task, msg, set_fail_state=True):
  625     """Cleanup rescue task after timeout or failure.
  626 
  627     :param task: a TaskManager instance.
  628     :param msg: a message to set into node's last_error field
  629     :param set_fail_state: a boolean flag to indicate if node needs to be
  630                            transitioned to a failed state. By default node
  631                            would be transitioned to a failed state.
  632     """
  633     node = task.node
  634     try:
  635         node_power_action(task, states.POWER_OFF)
  636         task.driver.rescue.clean_up(task)
  637         remove_agent_url(node)
  638         node.last_error = msg
  639     except exception.IronicException as e:
  640         node.last_error = (_('Rescue operation was unsuccessful, clean up '
  641                              'failed for node: %(error)s') % {'error': e})
  642         LOG.error(('Rescue operation was unsuccessful, clean up failed for '
  643                    'node %(node)s: %(error)s'),
  644                   {'node': node.uuid, 'error': e})
  645     except Exception as e:
  646         node.last_error = (_('Rescue failed, but an unhandled exception was '
  647                              'encountered while aborting: %(error)s') %
  648                            {'error': e})
  649         LOG.exception('Rescue failed for node %(node)s, an exception was '
  650                       'encountered while aborting.', {'node': node.uuid})
  651     finally:
  652         remove_agent_url(node)
  653         node.save()
  654 
  655     if set_fail_state:
  656         try:
  657             task.process_event('fail')
  658         except exception.InvalidState:
  659             node = task.node
  660             LOG.error('Internal error. Node %(node)s in provision state '
  661                       '"%(state)s" could not transition to a failed state.',
  662                       {'node': node.uuid, 'state': node.provision_state})
  663 
  664 
  665 @task_manager.require_exclusive_lock
  666 def cleanup_rescuewait_timeout(task):
  667     """Cleanup rescue task after timeout.
  668 
  669     :param task: a TaskManager instance.
  670     """
  671     msg = _('Timeout reached while waiting for rescue ramdisk callback '
  672             'for node')
  673     errmsg = msg + ' %(node)s'
  674     LOG.error(errmsg, {'node': task.node.uuid})
  675     rescuing_error_handler(task, msg, set_fail_state=False)
  676 
  677 
  678 def _spawn_error_handler(e, node, operation):
  679     """Handle error while trying to spawn a process.
  680 
  681     Handle error while trying to spawn a process to perform an
  682     operation on a node.
  683 
  684     :param e: the exception object that was raised.
  685     :param node: an Ironic node object.
  686     :param operation: the operation being performed on the node.
  687     """
  688     if isinstance(e, exception.NoFreeConductorWorker):
  689         node.last_error = (_("No free conductor workers available"))
  690         node.save()
  691         LOG.warning("No free conductor workers available to perform "
  692                     "%(operation)s on node %(node)s",
  693                     {'operation': operation, 'node': node.uuid})
  694 
  695 
  696 def spawn_cleaning_error_handler(e, node):
  697     """Handle spawning error for node cleaning."""
  698     _spawn_error_handler(e, node, states.CLEANING)
  699 
  700 
  701 def spawn_deploying_error_handler(e, node):
  702     """Handle spawning error for node deploying."""
  703     _spawn_error_handler(e, node, states.DEPLOYING)
  704 
  705 
  706 def spawn_rescue_error_handler(e, node):
  707     """Handle spawning error for node rescue."""
  708     if isinstance(e, exception.NoFreeConductorWorker):
  709         remove_node_rescue_password(node, save=False)
  710     _spawn_error_handler(e, node, states.RESCUE)
  711 
  712 
  713 def power_state_error_handler(e, node, power_state):
  714     """Set the node's power states if error occurs.
  715 
  716     This hook gets called upon an exception being raised when spawning
  717     the worker thread to change the power state of a node.
  718 
  719     :param e: the exception object that was raised.
  720     :param node: an Ironic node object.
  721     :param power_state: the power state to set on the node.
  722 
  723     """
  724     # NOTE This error will not emit a power state change notification since
  725     # this is related to spawning the worker thread, not the power state change
  726     # itself.
  727     if isinstance(e, exception.NoFreeConductorWorker):
  728         node.power_state = power_state
  729         node.target_power_state = states.NOSTATE
  730         node.last_error = (_("No free conductor workers available"))
  731         node.save()
  732         LOG.warning("No free conductor workers available to perform "
  733                     "an action on node %(node)s, setting node's "
  734                     "power state back to %(power_state)s.",
  735                     {'node': node.uuid, 'power_state': power_state})
  736 
  737 
  738 @task_manager.require_exclusive_lock
  739 def validate_port_physnet(task, port_obj):
  740     """Validate the consistency of physical networks of ports in a portgroup.
  741 
  742     Validate the consistency of a port's physical network with other ports in
  743     the same portgroup.  All ports in a portgroup should have the same value
  744     (which may be None) for their physical_network field.
  745 
  746     During creation or update of a port in a portgroup we apply the
  747     following validation criteria:
  748 
  749     - If the portgroup has existing ports with different physical networks, we
  750       raise PortgroupPhysnetInconsistent. This shouldn't ever happen.
  751     - If the port has a physical network that is inconsistent with other
  752       ports in the portgroup, we raise exception.Conflict.
  753 
  754     If a port's physical network is None, this indicates that ironic's VIF
  755     attachment mapping algorithm should operate in a legacy (physical
  756     network unaware) mode for this port or portgroup. This allows existing
  757     ironic nodes to continue to function after an upgrade to a release
  758     including physical network support.
  759 
  760     :param task: a TaskManager instance
  761     :param port_obj: a port object to be validated.
  762     :raises: Conflict if the port is a member of a portgroup which is on a
  763              different physical network.
  764     :raises: PortgroupPhysnetInconsistent if the port's portgroup has
  765              ports which are not all assigned the same physical network.
  766     """
  767     if 'portgroup_id' not in port_obj or not port_obj.portgroup_id:
  768         return
  769 
  770     delta = port_obj.obj_what_changed()
  771     # We can skip this step if the port's portgroup membership or physical
  772     # network assignment is not being changed (during creation these will
  773     # appear changed).
  774     if not (delta & {'portgroup_id', 'physical_network'}):
  775         return
  776 
  777     # Determine the current physical network of the portgroup.
  778     pg_physnets = network.get_physnets_by_portgroup_id(task,
  779                                                        port_obj.portgroup_id,
  780                                                        exclude_port=port_obj)
  781 
  782     if not pg_physnets:
  783         return
  784 
  785     # Check that the port has the same physical network as any existing
  786     # member ports.
  787     pg_physnet = pg_physnets.pop()
  788     port_physnet = (port_obj.physical_network
  789                     if 'physical_network' in port_obj else None)
  790     if port_physnet != pg_physnet:
  791         portgroup = network.get_portgroup_by_id(task, port_obj.portgroup_id)
  792         msg = _("Port with physical network %(physnet)s cannot become a "
  793                 "member of port group %(portgroup)s which has ports in "
  794                 "physical network %(pg_physnet)s.")
  795         raise exception.Conflict(
  796             msg % {'portgroup': portgroup.uuid, 'physnet': port_physnet,
  797                    'pg_physnet': pg_physnet})
  798 
  799 
  800 def remove_node_rescue_password(node, save=True):
  801     """Helper to remove rescue password from a node.
  802 
  803     Removes rescue password from node. It saves node by default.
  804     If node should not be saved, then caller needs to explicitly
  805     indicate it.
  806 
  807     :param node: an Ironic node object.
  808     :param save: Boolean; True (default) to save the node; False
  809                  otherwise.
  810     """
  811     instance_info = node.instance_info
  812     if 'rescue_password' in instance_info:
  813         del instance_info['rescue_password']
  814 
  815     if 'hashed_rescue_password' in instance_info:
  816         del instance_info['hashed_rescue_password']
  817 
  818     node.instance_info = instance_info
  819     if save:
  820         node.save()
  821 
  822 
  823 def validate_instance_info_traits(node):
  824     """Validate traits in instance_info.
  825 
  826     All traits in instance_info must also exist as node traits.
  827 
  828     :param node: an Ironic node object.
  829     :raises: InvalidParameterValue if the instance traits are badly formatted,
  830         or contain traits that are not set on the node.
  831     """
  832 
  833     def invalid():
  834         err = (_("Error parsing traits from Node %(node)s instance_info "
  835                  "field. A list of strings is expected.")
  836                % {"node": node.uuid})
  837         raise exception.InvalidParameterValue(err)
  838 
  839     if not node.instance_info.get('traits'):
  840         return
  841     instance_traits = node.instance_info['traits']
  842     if not isinstance(instance_traits, list):
  843         invalid()
  844     if not all(isinstance(t, str) for t in instance_traits):
  845         invalid()
  846 
  847     node_traits = node.traits.get_trait_names()
  848     missing = set(instance_traits) - set(node_traits)
  849     if missing:
  850         err = (_("Cannot specify instance traits that are not also set on the "
  851                  "node. Node %(node)s is missing traits %(traits)s") %
  852                {"node": node.uuid, "traits": ", ".join(missing)})
  853         raise exception.InvalidParameterValue(err)
  854 
  855 
  856 def notify_conductor_resume_operation(task, operation):
  857     """Notify the conductor to resume an operation.
  858 
  859     :param task: the task
  860     :param operation: the operation, a string
  861     """
  862     LOG.debug('Sending RPC to conductor to resume %(op)s steps for node '
  863               '%(node)s', {'op': operation, 'node': task.node.uuid})
  864     method = 'continue_node_%s' % operation
  865     from ironic.conductor import rpcapi
  866     uuid = task.node.uuid
  867     rpc = rpcapi.ConductorAPI()
  868     topic = rpc.get_current_topic()
  869     # Need to release the lock to let the conductor take it
  870     task.release_resources()
  871     getattr(rpc, method)(task.context, uuid, topic=topic)
  872 
  873 
  874 def notify_conductor_resume_clean(task):
  875     notify_conductor_resume_operation(task, 'clean')
  876 
  877 
  878 def notify_conductor_resume_deploy(task):
  879     notify_conductor_resume_operation(task, 'deploy')
  880 
  881 
  882 def skip_automated_cleaning(node):
  883     """Checks if node cleaning needs to be skipped for an specific node.
  884 
  885     :param node: the node to consider
  886     """
  887     return not CONF.conductor.automated_clean and not node.automated_clean
  888 
  889 
  890 def power_on_node_if_needed(task):
  891     """Powers on node if it is powered off and has a Smart NIC port
  892 
  893     :param task: A TaskManager object
  894     :returns: the previous power state or None if no changes were made
  895     :raises: exception.NetworkError if agent status didn't match the required
  896         status after max retry attempts.
  897     """
  898     if not task.driver.network.need_power_on(task):
  899         return
  900 
  901     previous_power_state = task.driver.power.get_power_state(task)
  902     if previous_power_state == states.POWER_OFF:
  903         node_set_boot_device(
  904             task, boot_devices.BIOS, persistent=False)
  905         node_power_action(task, states.POWER_ON)
  906 
  907         # local import is necessary to avoid circular import
  908         from ironic.common import neutron
  909 
  910         host_id = None
  911         for port in task.ports:
  912             if neutron.is_smartnic_port(port):
  913                 link_info = port.local_link_connection
  914                 host_id = link_info['hostname']
  915                 break
  916 
  917         if host_id:
  918             LOG.debug('Waiting for host %(host)s agent to be down',
  919                       {'host': host_id})
  920 
  921             client = neutron.get_client(context=task.context)
  922             neutron.wait_for_host_agent(
  923                 client, host_id, target_state='down')
  924         return previous_power_state
  925 
  926 
  927 def restore_power_state_if_needed(task, power_state_to_restore):
  928     """Change the node's power state if power_state_to_restore is not None
  929 
  930     :param task: A TaskManager object
  931     :param power_state_to_restore: power state
  932     """
  933     if power_state_to_restore:
  934 
  935         # Sleep is required here in order to give neutron agent
  936         # a chance to apply the changes before powering off.
  937         # Using twice the polling interval of the agent
  938         # "CONF.AGENT.polling_interval" would give the agent
  939         # enough time to apply network changes.
  940         time.sleep(CONF.agent.neutron_agent_poll_interval * 2)
  941         node_power_action(task, power_state_to_restore)
  942 
  943 
  944 @contextlib.contextmanager
  945 def power_state_for_network_configuration(task):
  946     """Handle the power state for a node reconfiguration.
  947 
  948     Powers the node on if and only if it has a Smart NIC port. Yields for
  949     the actual reconfiguration, then restores the power state.
  950 
  951     :param task: A TaskManager object.
  952     """
  953     previous = power_on_node_if_needed(task)
  954     yield task
  955     restore_power_state_if_needed(task, previous)
  956 
  957 
  958 def build_configdrive(node, configdrive):
  959     """Build a configdrive from provided meta_data, network_data and user_data.
  960 
  961     If uuid or name are not provided in the meta_data, they're defauled to the
  962     node's uuid and name accordingly.
  963 
  964     :param node: an Ironic node object.
  965     :param configdrive: A configdrive as a dict with keys ``meta_data``,
  966         ``network_data``, ``user_data`` and ``vendor_data`` (all optional).
  967     :returns: A gzipped and base64 encoded configdrive as a string.
  968     """
  969     meta_data = configdrive.setdefault('meta_data', {})
  970     meta_data.setdefault('uuid', node.uuid)
  971     if node.name:
  972         meta_data.setdefault('name', node.name)
  973 
  974     user_data = configdrive.get('user_data')
  975     if isinstance(user_data, (dict, list)):
  976         user_data = jsonutils.dump_as_bytes(user_data)
  977     elif user_data:
  978         user_data = user_data.encode('utf-8')
  979 
  980     LOG.debug('Building a configdrive for node %s', node.uuid)
  981     return os_configdrive.build(meta_data, user_data=user_data,
  982                                 network_data=configdrive.get('network_data'),
  983                                 vendor_data=configdrive.get('vendor_data'))
  984 
  985 
  986 def fast_track_able(task):
  987     """Checks if the operation can be a streamlined deployment sequence.
  988 
  989     This is mainly focused on ensuring that we are able to quickly sequence
  990     through operations if we already have a ramdisk heartbeating through
  991     external means.
  992 
  993     :param task: Taskmanager object
  994     :returns: True if [deploy]fast_track is set to True, no iSCSI boot
  995               configuration is present, and no last_error is present for
  996               the node indicating that there was a recent failure.
  997     """
  998     return (CONF.deploy.fast_track
  999             # TODO(TheJulia): Network model aside, we should be able to
 1000             # fast-track through initial sequence to complete deployment.
 1001             # This needs to be validated.
 1002             # TODO(TheJulia): Do we need a secondary guard? To prevent
 1003             # driving through this we could query the API endpoint of
 1004             # the agent with a short timeout such as 10 seconds, which
 1005             # would help verify if the node is online.
 1006             # TODO(TheJulia): Should we check the provisioning/deployment
 1007             # networks match config wise? Do we care? #decisionsdecisions
 1008             and task.driver.storage.should_write_image(task)
 1009             and task.node.last_error is None)
 1010 
 1011 
 1012 def value_within_timeout(value, timeout):
 1013     """Checks if the time is within the previous timeout seconds from now.
 1014 
 1015     :param value: a string representing date and time or None.
 1016     :param timeout: timeout in seconds.
 1017     """
 1018     # use native datetime objects for conversion and compare
 1019     # slightly odd because py2 compatability :(
 1020     last = datetime.datetime.strptime(value or '1970-01-01T00:00:00.000000',
 1021                                       "%Y-%m-%dT%H:%M:%S.%f")
 1022     # If we found nothing, we assume that the time is essentially epoch.
 1023     time_delta = datetime.timedelta(seconds=timeout)
 1024     last_valid = timeutils.utcnow() - time_delta
 1025     return last_valid <= last
 1026 
 1027 
 1028 def agent_is_alive(node, timeout=None):
 1029     """Check that the agent is likely alive.
 1030 
 1031     The method then checks for the last agent heartbeat, and if it occured
 1032     within the timeout set by [deploy]fast_track_timeout, then agent is
 1033     presumed alive.
 1034 
 1035     :param node: A node object.
 1036     :param timeout: Heartbeat timeout, defaults to `fast_track_timeout`.
 1037     """
 1038     return value_within_timeout(
 1039         node.driver_internal_info.get('agent_last_heartbeat'),
 1040         timeout or CONF.deploy.fast_track_timeout)
 1041 
 1042 
 1043 def is_fast_track(task):
 1044     """Checks a fast track is available.
 1045 
 1046     This method first ensures that the node and conductor configuration
 1047     is valid to perform a fast track sequence meaning that we already
 1048     have a ramdisk running through another means like discovery.
 1049     If not valid, False is returned.
 1050 
 1051     The method then checks for the last agent heartbeat, and if it occured
 1052     within the timeout set by [deploy]fast_track_timeout and the power
 1053     state for the machine is POWER_ON, then fast track is permitted.
 1054 
 1055     :param task: Taskmanager object
 1056     :returns: True if the last heartbeat that was recorded was within
 1057               the [deploy]fast_track_timeout setting.
 1058     """
 1059     if (not fast_track_able(task)
 1060             or task.driver.power.get_power_state(task) != states.POWER_ON):
 1061         if task.node.last_error:
 1062             LOG.debug('Node %(node)s is not fast-track-able because it has '
 1063                       'an error: %(error)s',
 1064                       {'node': task.node.uuid, 'error': task.node.last_error})
 1065         return False
 1066 
 1067     if agent_is_alive(task.node):
 1068         return True
 1069     else:
 1070         LOG.debug('Node %(node)s should be fast-track-able, but the agent '
 1071                   'doesn\'t seem to be running. Last heartbeat: %(last)s',
 1072                   {'node': task.node.uuid,
 1073                    'last': task.node.driver_internal_info.get(
 1074                        'agent_last_heartbeat')})
 1075         return False
 1076 
 1077 
 1078 def remove_agent_url(node):
 1079     """Helper to remove the agent_url record."""
 1080     info = node.driver_internal_info
 1081     info.pop('agent_url', None)
 1082     node.driver_internal_info = info
 1083 
 1084 
 1085 def _get_node_next_steps(task, step_type, skip_current_step=True):
 1086     """Get the task's node's next steps.
 1087 
 1088     This determines what the next (remaining) steps are, and
 1089     returns the index into the steps list that corresponds to the
 1090     next step. The remaining steps are determined as follows:
 1091 
 1092     * If no steps have been started yet, all the steps
 1093       must be executed
 1094     * If skip_current_step is False, the remaining steps start
 1095       with the current step. Otherwise, the remaining steps
 1096       start with the step after the current one.
 1097 
 1098     All the steps are in node.driver_internal_info['<step_type>_steps'].
 1099     node.<step_type>_step is the current step that was just executed
 1100     (or None, {} if no steps have been executed yet).
 1101     node.driver_internal_info['<step_type>_step_index'] is the index
 1102     index into the steps list (or None, doesn't exist if no steps have
 1103     been executed yet) and corresponds to node.<step_type>_step.
 1104 
 1105     :param task: A TaskManager object
 1106     :param step_type: The type of steps to process: 'clean' or 'deploy'.
 1107     :param skip_current_step: True to skip the current step; False to
 1108                               include it.
 1109     :returns: index of the next step; None if there are none to execute.
 1110 
 1111     """
 1112     valid_types = set(['clean', 'deploy'])
 1113     if step_type not in valid_types:
 1114         # NOTE(rloo): No need to i18n this, since this would be a
 1115         # developer error; it isn't user-facing.
 1116         raise exception.Invalid(
 1117             'step_type must be one of %(valid)s, not %(step)s'
 1118             % {'valid': valid_types, 'step': step_type})
 1119     node = task.node
 1120     if not getattr(node, '%s_step' % step_type):
 1121         # first time through, all steps need to be done. Return the
 1122         # index of the first step in the list.
 1123         return 0
 1124 
 1125     ind = node.driver_internal_info.get('%s_step_index' % step_type)
 1126     if ind is None:
 1127         return None
 1128 
 1129     if skip_current_step:
 1130         ind += 1
 1131     if ind >= len(node.driver_internal_info['%s_steps' % step_type]):
 1132         # no steps left to do
 1133         ind = None
 1134     return ind
 1135 
 1136 
 1137 def get_node_next_clean_steps(task, skip_current_step=True):
 1138     return _get_node_next_steps(task, 'clean',
 1139                                 skip_current_step=skip_current_step)
 1140 
 1141 
 1142 def get_node_next_deploy_steps(task, skip_current_step=True):
 1143     return _get_node_next_steps(task, 'deploy',
 1144                                 skip_current_step=skip_current_step)
 1145 
 1146 
 1147 def add_secret_token(node, pregenerated=False):
 1148     """Adds a secret token to driver_internal_info for IPA verification.
 1149 
 1150     :param node: Node object
 1151     :param pregenerated: Boolean value, default False, which indicates if
 1152                          the token should be marked as "pregenerated" in
 1153                          order to facilitate virtual media booting where
 1154                          the token is embedded into the configuration.
 1155     """
 1156     token = secrets.token_urlsafe()
 1157     i_info = node.driver_internal_info
 1158     i_info['agent_secret_token'] = token
 1159     if pregenerated:
 1160         i_info['agent_secret_token_pregenerated'] = True
 1161     node.driver_internal_info = i_info
 1162 
 1163 
 1164 def is_agent_token_present(node):
 1165     """Determines if an agent token is present upon a node.
 1166 
 1167     :param node: Node object
 1168     :returns: True if an agent_secret_token value is present in a node
 1169               driver_internal_info field.
 1170     """
 1171     # TODO(TheJulia): we should likely record the time when we add the token
 1172     # and then compare if it was in the last ?hour? to act as an additional
 1173     # guard rail, but if we do that we will want to check the last heartbeat
 1174     # because the heartbeat overrides the age of the token.
 1175     # We may want to do this elsewhere or nowhere, just a thought for the
 1176     # future.
 1177     return node.driver_internal_info.get(
 1178         'agent_secret_token', None) is not None
 1179 
 1180 
 1181 def is_agent_token_valid(node, token):
 1182     """Validates if a supplied token is valid for the node.
 1183 
 1184     :param node: Node object
 1185     :token: A token value to validate against the driver_internal_info field
 1186             agent_sercret_token.
 1187     :returns: True if the supplied token matches the token recorded in the
 1188               supplied node object.
 1189     """
 1190     if token is None:
 1191         # No token is never valid.
 1192         return False
 1193     known_token = node.driver_internal_info.get('agent_secret_token', None)
 1194     return known_token == token
 1195 
 1196 
 1197 def is_agent_token_pregenerated(node):
 1198     """Determines if the token was generated for out of band configuration.
 1199 
 1200     Ironic supports the ability to provide configuration data to the agent
 1201     through the a virtual floppy or as part of the virtual media image
 1202     which is attached to the BMC.
 1203 
 1204     This method helps us identify WHEN we did so as we don't need to remove
 1205     records of the token prior to rebooting the token. This is important as
 1206     tokens provided through out of band means presist in the virtual media
 1207     image, are loaded as part of the agent ramdisk, and do not require
 1208     regeneration of the token upon the initial lookup, ultimately making
 1209     the overall usage of virtual media and pregenerated tokens far more
 1210     secure.
 1211 
 1212     :param node: Node Object
 1213     :returns: True if the token was pregenerated as indicated by the node's
 1214               driver_internal_info field.
 1215               False in all other cases.
 1216     """
 1217     return node.driver_internal_info.get(
 1218         'agent_secret_token_pregenerated', False)
 1219 
 1220 
 1221 def make_salt():
 1222     """Generate a random salt with the indicator tag for password type.
 1223 
 1224     :returns: a valid salt for use with crypt.crypt
 1225     """
 1226     return crypt.mksalt(
 1227         method=PASSWORD_HASH_FORMAT[
 1228             CONF.conductor.rescue_password_hash_algorithm])
 1229 
 1230 
 1231 def hash_password(password=''):
 1232     """Hashes a supplied password.
 1233 
 1234     :param value: Value to be hashed
 1235     """
 1236     return crypt.crypt(password, make_salt())
 1237 
 1238 
 1239 def get_attached_vif(port):
 1240     """Get any attached vif ID for the port
 1241 
 1242     :param port: The port object upon which to check for a vif
 1243                  record.
 1244     :returns: Returns a tuple of the vif if found and the use of
 1245               the vif in the form of a string, 'tenant', 'cleaning'
 1246               'provisioning', 'rescuing'.
 1247     :raises: InvalidState exception upon finding a port with a
 1248              transient state vif on the port.
 1249     """
 1250 
 1251     tenant_vif = port.internal_info.get('tenant_vif_port_id')
 1252     if tenant_vif:
 1253         return (tenant_vif, 'tenant')
 1254     clean_vif = port.internal_info.get('cleaning_vif_port_id')
 1255     if clean_vif:
 1256         return (clean_vif, 'cleaning')
 1257     prov_vif = port.internal_info.get('provisioning_vif_port_id')
 1258     if prov_vif:
 1259         return (prov_vif, 'provisioning')
 1260     rescue_vif = port.internal_info.get('rescuing_vif_port_id')
 1261     if rescue_vif:
 1262         return (rescue_vif, 'rescuing')
 1263     inspection_vif = port.internal_info.get('inspection_vif_port_id')
 1264     if inspection_vif:
 1265         return (inspection_vif, 'inspecting')
 1266     return (None, None)
 1267 
 1268 
 1269 def store_agent_certificate(node, agent_verify_ca):
 1270     """Store certificate received from the agent and return its path."""
 1271     existing_verify_ca = node.driver_internal_info.get(
 1272         'agent_verify_ca')
 1273     if existing_verify_ca:
 1274         if os.path.exists(existing_verify_ca):
 1275             try:
 1276                 with open(existing_verify_ca, 'rt') as fp:
 1277                     existing_text = fp.read()
 1278             except EnvironmentError:
 1279                 with excutils.save_and_reraise_exception():
 1280                     LOG.exception('Could not read the existing TLS certificate'
 1281                                   ' for node %s', node.uuid)
 1282 
 1283             if existing_text.strip() != agent_verify_ca.strip():
 1284                 LOG.error('Content mismatch for agent_verify_ca for '
 1285                           'node %s', node.uuid)
 1286                 raise exception.InvalidParameterValue(
 1287                     _('Detected change in ramdisk provided "agent_verify_ca"'))
 1288             else:
 1289                 return existing_verify_ca
 1290         else:
 1291             LOG.info('Current agent_verify_ca was not found for node '
 1292                      '%s, assuming take over and storing', node.uuid)
 1293 
 1294     fname = os.path.join(CONF.agent.certificates_path, '%s.crt' % node.uuid)
 1295     try:
 1296         # FIXME(dtantsur): it makes more sense to create this path on conductor
 1297         # start-up, but it requires reworking a ton of unit tests.
 1298         os.makedirs(CONF.agent.certificates_path, exist_ok=True)
 1299         with open(fname, 'wt') as fp:
 1300             fp.write(agent_verify_ca)
 1301     except EnvironmentError:
 1302         with excutils.save_and_reraise_exception():
 1303             LOG.exception('Could not save the TLS certificate for node %s',
 1304                           node.uuid)
 1305     else:
 1306         LOG.debug('Saved the custom certificate for node %(node)s to %(file)s',
 1307                   {'node': node.uuid, 'file': fname})
 1308         return fname