"Fossies" - the Fresh Open Source Software Archive

Member "ironic-16.0.3/ironic/drivers/modules/ipmitool.py" (18 Jan 2021, 66972 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 "ipmitool.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 # Copyright 2012 Hewlett-Packard Development Company, L.P.
    4 # Copyright (c) 2012 NTT DOCOMO, INC.
    5 # Copyright 2014 International Business Machines Corporation
    6 # All Rights Reserved.
    7 #
    8 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
    9 #    not use this file except in compliance with the License. You may obtain
   10 #    a copy of the License at
   11 #
   12 #         http://www.apache.org/licenses/LICENSE-2.0
   13 #
   14 #    Unless required by applicable law or agreed to in writing, software
   15 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   16 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   17 #    License for the specific language governing permissions and limitations
   18 #    under the License.
   19 
   20 """
   21 IPMI power manager driver.
   22 
   23 Uses the 'ipmitool' command (http://ipmitool.sourceforge.net/) to remotely
   24 manage hardware.  This includes setting the boot device, getting a
   25 serial-over-LAN console, and controlling the power state of the machine.
   26 
   27 NOTE THAT CERTAIN DISTROS MAY INSTALL openipmi BY DEFAULT, INSTEAD OF ipmitool,
   28 WHICH PROVIDES DIFFERENT COMMAND-LINE OPTIONS AND *IS NOT SUPPORTED* BY THIS
   29 DRIVER.
   30 """
   31 
   32 import contextlib
   33 import os
   34 import re
   35 import subprocess
   36 import tempfile
   37 import time
   38 
   39 from eventlet.green import subprocess as green_subprocess
   40 from ironic_lib import metrics_utils
   41 from ironic_lib import utils as ironic_utils
   42 from oslo_concurrency import processutils
   43 from oslo_log import log as logging
   44 from oslo_utils import excutils
   45 from oslo_utils import strutils
   46 
   47 from ironic.common import boot_devices
   48 from ironic.common import exception
   49 from ironic.common.i18n import _
   50 from ironic.common import states
   51 from ironic.common import utils
   52 from ironic.conductor import task_manager
   53 from ironic.conductor import utils as cond_utils
   54 from ironic.conf import CONF
   55 from ironic.drivers import base
   56 from ironic.drivers.modules import boot_mode_utils
   57 from ironic.drivers.modules import console_utils
   58 from ironic.drivers import utils as driver_utils
   59 
   60 
   61 LOG = logging.getLogger(__name__)
   62 
   63 METRICS = metrics_utils.get_metrics_logger(__name__)
   64 
   65 VALID_PRIV_LEVELS = ['ADMINISTRATOR', 'CALLBACK', 'OPERATOR', 'USER']
   66 
   67 VALID_PROTO_VERSIONS = ('2.0', '1.5')
   68 
   69 REQUIRED_PROPERTIES = {
   70     'ipmi_address': _("IP address or hostname of the node. Required.")
   71 }
   72 OPTIONAL_PROPERTIES = {
   73     'ipmi_password': _("password. Optional."),
   74     'ipmi_hex_kg_key': _('Kg key for IPMIv2 authentication. '
   75                          'The key is expected in hexadecimal format. '
   76                          'Optional.'),
   77     'ipmi_port': _("remote IPMI RMCP port. Optional."),
   78     'ipmi_priv_level': _("privilege level; default is ADMINISTRATOR. One of "
   79                          "%s. Optional.") % ', '.join(VALID_PRIV_LEVELS),
   80     'ipmi_username': _("username; default is NULL user. Optional."),
   81     'ipmi_bridging': _("bridging_type; default is \"no\". One of \"single\", "
   82                        "\"dual\", \"no\". Optional."),
   83     'ipmi_transit_channel': _("transit channel for bridged request. Required "
   84                               "only if ipmi_bridging is set to \"dual\"."),
   85     'ipmi_transit_address': _("transit address for bridged request. Required "
   86                               "only if ipmi_bridging is set to \"dual\"."),
   87     'ipmi_target_channel': _("destination channel for bridged request. "
   88                              "Required only if ipmi_bridging is set to "
   89                              "\"single\" or \"dual\"."),
   90     'ipmi_target_address': _("destination address for bridged request. "
   91                              "Required only if ipmi_bridging is set "
   92                              "to \"single\" or \"dual\"."),
   93     'ipmi_local_address': _("local IPMB address for bridged requests. "
   94                             "Used only if ipmi_bridging is set "
   95                             "to \"single\" or \"dual\". Optional."),
   96     'ipmi_protocol_version': _('the version of the IPMI protocol; default '
   97                                'is "2.0". One of "1.5", "2.0". Optional.'),
   98     'ipmi_force_boot_device': _("Whether Ironic should specify the boot "
   99                                 "device to the BMC each time the server "
  100                                 "is turned on, eg. because the BMC is not "
  101                                 "capable of remembering the selected boot "
  102                                 "device across power cycles; default value "
  103                                 "is False. Optional."),
  104     'ipmi_disable_boot_timeout': _('By default ironic will send a raw IPMI '
  105                                    'command to disable the 60 second timeout '
  106                                    'for booting. Setting this option to '
  107                                    'False will NOT send that command on '
  108                                    'this node. The '
  109                                    '[ipmi]disable_boot_timeout will be '
  110                                    'used if this option is not set. '
  111                                    'Optional.'),
  112     'ipmi_cipher_suite': _('The number of a cipher suite to use. Only 3 '
  113                            '(AES-128 with SHA1) or 17 (AES-128 with SHA256) '
  114                            'should ever be used; 17 is preferred. The '
  115                            'default value depends on the ipmitool version, '
  116                            'some recent versions have switched from 3 to 17.'),
  117 }
  118 COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
  119 COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
  120 CONSOLE_PROPERTIES = {
  121     'ipmi_terminal_port': _("node's UDP port to connect to. Only required for "
  122                             "console access.")
  123 }
  124 BRIDGING_OPTIONS = [('local_address', '-m'),
  125                     ('transit_channel', '-B'), ('transit_address', '-T'),
  126                     ('target_channel', '-b'), ('target_address', '-t')]
  127 
  128 LAST_CMD_TIME = {}
  129 TIMING_SUPPORT = None
  130 SINGLE_BRIDGE_SUPPORT = None
  131 DUAL_BRIDGE_SUPPORT = None
  132 TMP_DIR_CHECKED = None
  133 
  134 ipmitool_command_options = {
  135     'timing': ['ipmitool', '-N', '0', '-R', '0', '-h'],
  136     'single_bridge': ['ipmitool', '-m', '0', '-b', '0', '-t', '0', '-h'],
  137     'dual_bridge': ['ipmitool', '-m', '0', '-b', '0', '-t', '0',
  138                     '-B', '0', '-T', '0', '-h']}
  139 
  140 # Note(etingof): For more information on IPMI error codes and `ipmitool`
  141 # human interface please refer to:
  142 # https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf
  143 # https://github.com/scottjg/ipmitool/blob/master/lib/ipmi_strings.c#L367
  144 #
  145 # Note(TheJulia): The strings below are hardcoded in ipmitool and get
  146 # substituted in return for the error code received from the IPMI controller.
  147 # As of 1.8.15, no internationalization support appears to be in ipmitool
  148 # which means the strings should always be returned in this form regardless
  149 # of locale.
  150 IPMITOOL_RETRYABLE_FAILURES = ['insufficient resources for session',
  151                                # Generic completion codes considered retryable
  152                                'Node busy',
  153                                'Timeout',
  154                                'Out of space',
  155                                'BMC initialization in progress']
  156 
  157 # NOTE(lucasagomes): A mapping for the boot devices and their hexadecimal
  158 # value. For more information about these values see the "Set System Boot
  159 # Options Command" section 28.13 of the link below (page 392)
  160 # http://www.intel.com/content/www/us/en/servers/ipmi/ipmi-second-gen-interface-spec-v2-rev1-1.html  # noqa
  161 BOOT_DEVICE_HEXA_MAP = {
  162     boot_devices.PXE: '0x04',
  163     boot_devices.DISK: '0x08',
  164     boot_devices.CDROM: '0x14',
  165     boot_devices.BIOS: '0x18',
  166     boot_devices.SAFE: '0x0c'
  167 }
  168 # NOTE(TheJulia): Inline notes for ipmi raw boot device commands.
  169 # Per spec (intel 2nd gen ipmi interface, v2, rev1-1)
  170 #  - bit 7 is CMOS clear
  171 #  - bit 6 is Keyboard lock
  172 #  - bit 5-2 is the boot device selector
  173 #    * where 0000b is to do nothing
  174 #    * where 0001b is to force pxe
  175 #    * where 0010b is boot from hard drive
  176 #    * And anything above 1100b is listed reserved
  177 #      which is 0x30 for ipmitool raw commands!
  178 #  - bit 1 is screen blank
  179 #  - bit 0 is lock out reset button
  180 #
  181 # Effectively this results in
  182 # 00000100b == 0x04 for booting from PXE
  183 # 00001000b == 0x08 for booting from the primary hard disk.
  184 # 00100000b == 0x20 is remotely connected virtual media
  185 # 00100100b == 0x24 which is primary remote boot media as set
  186 #                   via the OEM initiator mailbox information.
  187 #                   Like.. iscsi?
  188 #                   However! Supermicro uses this as the UEFI
  189 #                   hard disk. See: https://www.supermicro.com/support/faqs/faq.cfm?faq=25559  # noqa
  190 # 00010000b == 0x30 Enters the reserved territory, and should not
  191 #                   be expected in use.
  192 
  193 
  194 def _vendor_aware_boot_device_map(task):
  195     """Returns an altered vendor boot device map based upon vendor."""
  196     node = task.node
  197     vendor = node.properties.get('vendor', None)
  198     boot_dev_map = BOOT_DEVICE_HEXA_MAP.copy()
  199     boot_mode = boot_mode_utils.get_boot_mode(node)
  200     if vendor:
  201         vendor = str(vendor).lower()
  202         if boot_mode == 'uefi' and vendor == "supermicro":
  203             # This difference is only known on UEFI mode for supermicro
  204             # hardware.
  205             boot_dev_map[boot_devices.DISK] = '0x24'
  206         # NOTE(TheJulia): Similar differences may exist with Cisco UCS
  207         # hardware when using IPMI, however at present we don't know
  208         # what the setting would be.
  209         # NOTE(TheJulia) I've observed "mc info" manufacter name of a cisco
  210         # c-series machine to return "Unknown (0x168B)"
  211     return boot_dev_map
  212 
  213 
  214 def _check_option_support(options):
  215     """Checks if the specific ipmitool options are supported on host.
  216 
  217     This method updates the module-level variables indicating whether
  218     an option is supported so that it is accessible by any driver
  219     interface class in this module. It is intended to be called from
  220     the __init__ method of such classes only.
  221 
  222     :param options: list of ipmitool options to be checked
  223     :raises: OSError
  224     """
  225     for opt in options:
  226         if _is_option_supported(opt) is None:
  227             try:
  228                 cmd = ipmitool_command_options[opt]
  229                 # NOTE(cinerama): use subprocess.check_call to
  230                 # check options & suppress ipmitool output to
  231                 # avoid alarming people
  232                 with open(os.devnull, 'wb') as nullfile:
  233                     subprocess.check_call(cmd, stdout=nullfile,
  234                                           stderr=nullfile)
  235             except subprocess.CalledProcessError:
  236                 LOG.info("Option %(opt)s is not supported by ipmitool",
  237                          {'opt': opt})
  238                 _is_option_supported(opt, False)
  239             else:
  240                 LOG.info("Option %(opt)s is supported by ipmitool",
  241                          {'opt': opt})
  242                 _is_option_supported(opt, True)
  243 
  244 
  245 def _is_option_supported(option, is_supported=None):
  246     """Indicates whether the particular ipmitool option is supported.
  247 
  248     :param option: specific ipmitool option
  249     :param is_supported: Optional Boolean. when specified, this value
  250                          is assigned to the module-level variable indicating
  251                          whether the option is supported. Used only if a value
  252                          is not already assigned.
  253     :returns: True, indicates the option is supported
  254     :returns: False, indicates the option is not supported
  255     :returns: None, indicates that it is not aware whether the option
  256               is supported
  257     """
  258     global SINGLE_BRIDGE_SUPPORT
  259     global DUAL_BRIDGE_SUPPORT
  260     global TIMING_SUPPORT
  261 
  262     if option == 'single_bridge':
  263         if (SINGLE_BRIDGE_SUPPORT is None) and (is_supported is not None):
  264             SINGLE_BRIDGE_SUPPORT = is_supported
  265         return SINGLE_BRIDGE_SUPPORT
  266     elif option == 'dual_bridge':
  267         if (DUAL_BRIDGE_SUPPORT is None) and (is_supported is not None):
  268             DUAL_BRIDGE_SUPPORT = is_supported
  269         return DUAL_BRIDGE_SUPPORT
  270     elif option == 'timing':
  271         if (TIMING_SUPPORT is None) and (is_supported is not None):
  272             TIMING_SUPPORT = is_supported
  273         return TIMING_SUPPORT
  274 
  275 
  276 def _console_pwfile_path(uuid):
  277     """Return the file path for storing the ipmi password for a console."""
  278     file_name = "%(uuid)s.pw" % {'uuid': uuid}
  279     return os.path.join(CONF.tempdir, file_name)
  280 
  281 
  282 @contextlib.contextmanager
  283 def _make_password_file(password):
  284     """Makes a temporary file that contains the password.
  285 
  286     :param password: the password
  287     :returns: the absolute pathname of the temporary file
  288     :raises: PasswordFileFailedToCreate from creating or writing to the
  289              temporary file
  290     """
  291     f = None
  292     try:
  293         f = tempfile.NamedTemporaryFile(mode='w', dir=CONF.tempdir)
  294         f.write(str(password))
  295         f.flush()
  296     except (IOError, OSError) as exc:
  297         if f is not None:
  298             f.close()
  299         raise exception.PasswordFileFailedToCreate(error=exc)
  300     except Exception:
  301         with excutils.save_and_reraise_exception():
  302             if f is not None:
  303                 f.close()
  304 
  305     try:
  306         # NOTE(jlvillal): This yield can not be in the try/except block above
  307         # because an exception by the caller of this function would then get
  308         # changed to a PasswordFileFailedToCreate exception which would mislead
  309         # about the problem and its cause.
  310         yield f.name
  311     finally:
  312         if f is not None:
  313             f.close()
  314 
  315 
  316 def _parse_driver_info(node):
  317     """Gets the parameters required for ipmitool to access the node.
  318 
  319     :param node: the Node of interest.
  320     :returns: dictionary of parameters.
  321     :raises: InvalidParameterValue when an invalid value is specified
  322     :raises: MissingParameterValue when a required ipmi parameter is missing.
  323 
  324     """
  325     info = node.driver_info or {}
  326     internal_info = node.driver_internal_info or {}
  327     bridging_types = ['single', 'dual']
  328     missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
  329     if missing_info:
  330         raise exception.MissingParameterValue(_(
  331             "Missing the following IPMI credentials in node's"
  332             " driver_info: %s.") % missing_info)
  333 
  334     address = info.get('ipmi_address')
  335     username = info.get('ipmi_username')
  336     password = str(info.get('ipmi_password', ''))
  337     hex_kg_key = info.get('ipmi_hex_kg_key')
  338     dest_port = info.get('ipmi_port')
  339     port = (info.get('ipmi_terminal_port')
  340             or internal_info.get('allocated_ipmi_terminal_port'))
  341     priv_level = info.get('ipmi_priv_level', 'ADMINISTRATOR')
  342     bridging_type = info.get('ipmi_bridging', 'no')
  343     local_address = info.get('ipmi_local_address')
  344     transit_channel = info.get('ipmi_transit_channel')
  345     transit_address = info.get('ipmi_transit_address')
  346     target_channel = info.get('ipmi_target_channel')
  347     target_address = info.get('ipmi_target_address')
  348     protocol_version = str(info.get('ipmi_protocol_version', '2.0'))
  349     force_boot_device = info.get('ipmi_force_boot_device', False)
  350     cipher_suite = info.get('ipmi_cipher_suite')
  351 
  352     if not username:
  353         LOG.warning('ipmi_username is not defined or empty for node %s: '
  354                     'NULL user will be utilized.', node.uuid)
  355     if not password:
  356         LOG.warning('ipmi_password is not defined or empty for node %s: '
  357                     'NULL password will be utilized.', node.uuid)
  358 
  359     if protocol_version not in VALID_PROTO_VERSIONS:
  360         valid_versions = ', '.join(VALID_PROTO_VERSIONS)
  361         raise exception.InvalidParameterValue(_(
  362             "Invalid IPMI protocol version value %(version)s, the valid "
  363             "value can be one of %(valid_versions)s") %
  364             {'version': protocol_version, 'valid_versions': valid_versions})
  365 
  366     if port is not None:
  367         port = utils.validate_network_port(port, 'ipmi_terminal_port')
  368 
  369     if dest_port is not None:
  370         dest_port = utils.validate_network_port(dest_port, 'ipmi_port')
  371 
  372     # check if ipmi_bridging has proper value
  373     if bridging_type == 'no':
  374         # if bridging is not selected, then set all bridging params to None
  375         (local_address, transit_channel, transit_address, target_channel,
  376          target_address) = (None,) * 5
  377     elif bridging_type in bridging_types:
  378         # check if the particular bridging option is supported on host
  379         if not _is_option_supported('%s_bridge' % bridging_type):
  380             raise exception.InvalidParameterValue(_(
  381                 "Value for ipmi_bridging is provided as %s, but IPMI "
  382                 "bridging is not supported by the IPMI utility installed "
  383                 "on host. Ensure ipmitool version is > 1.8.11"
  384             ) % bridging_type)
  385 
  386         # ensure that all the required parameters are provided
  387         params_undefined = [param for param, value in [
  388             ("ipmi_target_channel", target_channel),
  389             ('ipmi_target_address', target_address)] if value is None]
  390         if bridging_type == 'dual':
  391             params_undefined2 = [param for param, value in [
  392                 ("ipmi_transit_channel", transit_channel),
  393                 ('ipmi_transit_address', transit_address)
  394             ] if value is None]
  395             params_undefined.extend(params_undefined2)
  396         else:
  397             # if single bridging was selected, set dual bridge params to None
  398             transit_channel = transit_address = None
  399 
  400         # If the required parameters were not provided,
  401         # raise an exception
  402         if params_undefined:
  403             raise exception.MissingParameterValue(_(
  404                 "%(param)s not provided") % {'param': params_undefined})
  405     else:
  406         raise exception.InvalidParameterValue(_(
  407             "Invalid value for ipmi_bridging: %(bridging_type)s,"
  408             " the valid value can be one of: %(bridging_types)s"
  409         ) % {'bridging_type': bridging_type,
  410              'bridging_types': bridging_types + ['no']})
  411 
  412     if priv_level not in VALID_PRIV_LEVELS:
  413         valid_priv_lvls = ', '.join(VALID_PRIV_LEVELS)
  414         raise exception.InvalidParameterValue(_(
  415             "Invalid privilege level value:%(priv_level)s, the valid value"
  416             " can be one of %(valid_levels)s") %
  417             {'priv_level': priv_level, 'valid_levels': valid_priv_lvls})
  418 
  419     if hex_kg_key and len(hex_kg_key) % 2 != 0:
  420         raise exception.InvalidParameterValue(_(
  421             "Number of ipmi_hex_kg_key characters is not even"))
  422 
  423     if cipher_suite is not None:
  424         try:
  425             int(cipher_suite)
  426         except (TypeError, ValueError):
  427             raise exception.InvalidParameterValue(_(
  428                 "Invalid cipher suite %s, expected a number") % cipher_suite)
  429         if protocol_version == '1.5':
  430             raise exception.InvalidParameterValue(_(
  431                 "Cipher suites cannot be used with IPMI 1.5"))
  432 
  433     return {
  434         'address': address,
  435         'dest_port': dest_port,
  436         'username': username,
  437         'password': password,
  438         'hex_kg_key': hex_kg_key,
  439         'port': port,
  440         'uuid': node.uuid,
  441         'priv_level': priv_level,
  442         'local_address': local_address,
  443         'transit_channel': transit_channel,
  444         'transit_address': transit_address,
  445         'target_channel': target_channel,
  446         'target_address': target_address,
  447         'protocol_version': protocol_version,
  448         'force_boot_device': force_boot_device,
  449         'cipher_suite': cipher_suite,
  450     }
  451 
  452 
  453 def _get_ipmitool_args(driver_info, pw_file=None):
  454     ipmi_version = ('lanplus'
  455                     if driver_info['protocol_version'] == '2.0'
  456                     else 'lan')
  457 
  458     args = ['ipmitool',
  459             '-I', ipmi_version,
  460             '-H', driver_info['address'],
  461             '-L', driver_info['priv_level']
  462             ]
  463 
  464     for field, arg in [('dest_port', '-p'),
  465                        ('username', '-U'),
  466                        ('hex_kg_key', '-y'),
  467                        ('cipher_suite', '-C')]:
  468         if driver_info[field]:
  469             args.append(arg)
  470             args.append(driver_info[field])
  471 
  472     for name, option in BRIDGING_OPTIONS:
  473         if driver_info[name] is not None:
  474             args.append(option)
  475             args.append(driver_info[name])
  476 
  477     if pw_file:
  478         args.append('-f')
  479         args.append(pw_file)
  480 
  481     if CONF.ipmi.debug:
  482         args.append('-v')
  483 
  484     # ensure all arguments are strings
  485     args = [str(arg) for arg in args]
  486 
  487     return args
  488 
  489 
  490 def _exec_ipmitool(driver_info, command, check_exit_code=None,
  491                    kill_on_timeout=False):
  492     """Execute the ipmitool command.
  493 
  494     :param driver_info: the ipmitool parameters for accessing a node.
  495     :param command: the ipmitool command to be executed.
  496     :param check_exit_code: Single bool, int, or list of allowed exit codes.
  497     :param kill_on_timeout: if `True`, kill unresponsive ipmitool on
  498         `min_command_interval` timeout. Default is `False`. Makes no
  499         effect on Windows.
  500     :returns: (stdout, stderr) from executing the command.
  501     :raises: PasswordFileFailedToCreate from creating or writing to the
  502              temporary file.
  503     :raises: processutils.ProcessExecutionError from executing the command.
  504 
  505     """
  506     args = _get_ipmitool_args(driver_info)
  507 
  508     timeout = CONF.ipmi.command_retry_timeout
  509 
  510     # specify retry timing more precisely, if supported
  511     num_tries = max((timeout // CONF.ipmi.min_command_interval), 1)
  512 
  513     if _is_option_supported('timing'):
  514         args.append('-R')
  515         if CONF.ipmi.use_ipmitool_retries:
  516             args.append(str(num_tries))
  517         else:
  518             args.append('1')
  519 
  520         args.append('-N')
  521         args.append(str(CONF.ipmi.min_command_interval))
  522 
  523     extra_args = {}
  524 
  525     if kill_on_timeout:
  526         extra_args['timeout'] = timeout
  527 
  528     if check_exit_code is not None:
  529         extra_args['check_exit_code'] = check_exit_code
  530 
  531     end_time = (time.time() + timeout)
  532 
  533     while True:
  534         num_tries = num_tries - 1
  535         # NOTE(tenbrae): ensure that no communications are sent to a BMC more
  536         #             often than once every min_command_interval seconds.
  537         time_till_next_poll = CONF.ipmi.min_command_interval - (
  538             time.time() - LAST_CMD_TIME.get(driver_info['address'], 0))
  539         if time_till_next_poll > 0:
  540             time.sleep(time_till_next_poll)
  541         # Resetting the list that will be utilized so the password arguments
  542         # from any previous execution are preserved.
  543         cmd_args = args[:]
  544         # 'ipmitool' command will prompt password if there is no '-f'
  545         # option, we set it to '\0' to write a password file to support
  546         # empty password
  547         with _make_password_file(driver_info['password'] or '\0') as pw_file:
  548             cmd_args.append('-f')
  549             cmd_args.append(pw_file)
  550             cmd_args.extend(command.split(" "))
  551             try:
  552                 out, err = utils.execute(*cmd_args, **extra_args)
  553                 return out, err
  554             except processutils.ProcessExecutionError as e:
  555                 with excutils.save_and_reraise_exception() as ctxt:
  556                     err_list = [
  557                         x for x in (
  558                             IPMITOOL_RETRYABLE_FAILURES
  559                             + CONF.ipmi.additional_retryable_ipmi_errors)
  560                         if x in str(e)]
  561                     # If Ironic is doing retries then retry all errors
  562                     retry_failures = (err_list
  563                                       or not CONF.ipmi.use_ipmitool_retries)
  564                     if ((time.time() > end_time)
  565                         or (num_tries == 0)
  566                         or not retry_failures):
  567                         LOG.error('IPMI Error while attempting "%(cmd)s" '
  568                                   'for node %(node)s. Error: %(error)s',
  569                                   {'node': driver_info['uuid'],
  570                                    'cmd': e.cmd, 'error': e})
  571                     else:
  572                         ctxt.reraise = False
  573                         LOG.warning('IPMI Error encountered, retrying '
  574                                     '"%(cmd)s" for node %(node)s. '
  575                                     'Error: %(error)s',
  576                                     {'node': driver_info['uuid'],
  577                                      'cmd': e.cmd, 'error': e})
  578             finally:
  579                 LAST_CMD_TIME[driver_info['address']] = time.time()
  580 
  581 
  582 def _set_and_wait(task, power_action, driver_info, timeout=None):
  583     """Helper function for performing an IPMI power action
  584 
  585     This method assumes the caller knows the current power state and does not
  586     check it prior to changing the power state. Most BMCs should be fine, but
  587     if a driver is concerned, the state should be checked prior to calling this
  588     method.
  589 
  590     :param power_action: the action Ironic will perform when changing the
  591       power state of the node.
  592     :param timeout: timeout (in seconds) positive integer (> 0) for any
  593       power state. ``None`` indicates to use default timeout.
  594     :param driver_info: the ipmitool parameters for accessing a node.
  595     :returns: one of ironic.common.states
  596 
  597     """
  598     if power_action == states.POWER_ON:
  599         cmd_name = "on"
  600         target_state = states.POWER_ON
  601     elif power_action == states.POWER_OFF:
  602         cmd_name = "off"
  603         target_state = states.POWER_OFF
  604     elif power_action == states.SOFT_POWER_OFF:
  605         cmd_name = "soft"
  606         target_state = states.POWER_OFF
  607         timeout = timeout or CONF.conductor.soft_power_off_timeout
  608 
  609     # NOTE(sambetts): Retries for ipmi power action failure will be handled by
  610     # the _exec_ipmitool function, so no need to wrap this call in its own
  611     # retries.
  612     cmd = "power %s" % cmd_name
  613     try:
  614         _exec_ipmitool(driver_info, cmd)
  615     except (exception.PasswordFileFailedToCreate,
  616             processutils.ProcessExecutionError,
  617             subprocess.TimeoutExpired,
  618             # https://github.com/eventlet/eventlet/issues/624
  619             green_subprocess.TimeoutExpired) as e:
  620         LOG.warning("IPMI power action %(cmd)s failed for node %(node_id)s "
  621                     "with error: %(error)s.",
  622                     {'node_id': driver_info['uuid'], 'cmd': cmd, 'error': e})
  623         raise exception.IPMIFailure(cmd=cmd)
  624     return cond_utils.node_wait_for_power_state(task, target_state,
  625                                                 timeout=timeout)
  626 
  627 
  628 def _power_on(task, driver_info, timeout=None):
  629     """Turn the power ON for this node.
  630 
  631     :param driver_info: the ipmitool parameters for accessing a node.
  632     :param timeout: the timeout in seconds (> 0) to wait for the power
  633       action to be completed. ``None`` indicates default timeout".
  634     :returns: one of ironic.common.states POWER_ON or ERROR.
  635     :raises: IPMIFailure on an error from ipmitool (from _power_status call).
  636 
  637     """
  638     return _set_and_wait(task, states.POWER_ON, driver_info, timeout=timeout)
  639 
  640 
  641 def _power_off(task, driver_info, timeout=None):
  642     """Turn the power OFF for this node.
  643 
  644     :param driver_info: the ipmitool parameters for accessing a node.
  645     :param timeout: the timeout in seconds (> 0) to wait for the power
  646       action to be completed. ``None`` indicates default timeout".
  647     :returns: one of ironic.common.states POWER_OFF or ERROR.
  648     :raises: IPMIFailure on an error from ipmitool (from _power_status call).
  649 
  650     """
  651     return _set_and_wait(task, states.POWER_OFF, driver_info, timeout=timeout)
  652 
  653 
  654 def _soft_power_off(task, driver_info, timeout=None):
  655     """Turn the power SOFT OFF for this node.
  656 
  657     :param driver_info: the ipmitool parameters for accessing a node.
  658     :param timeout: the timeout in seconds (> 0) to wait for the power
  659       action to be completed. ``None`` indicates default timeout".
  660     :returns: one of ironic.common.states POWER_OFF or ERROR.
  661     :raises: IPMIFailure on an error from ipmitool (from _power_status call).
  662 
  663     """
  664     return _set_and_wait(task, states.SOFT_POWER_OFF, driver_info,
  665                          timeout=timeout)
  666 
  667 
  668 def _power_status(driver_info):
  669     """Get the power status for a node.
  670 
  671     :param driver_info: the ipmitool access parameters for a node.
  672     :returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR.
  673     :raises: IPMIFailure on an error from ipmitool.
  674 
  675     """
  676     cmd = "power status"
  677     try:
  678         out_err = _exec_ipmitool(
  679             driver_info, cmd, kill_on_timeout=CONF.ipmi.kill_on_timeout)
  680     except (exception.PasswordFileFailedToCreate,
  681             processutils.ProcessExecutionError) as e:
  682         LOG.warning("IPMI power status failed for node %(node_id)s with "
  683                     "error: %(error)s.",
  684                     {'node_id': driver_info['uuid'], 'error': e})
  685         raise exception.IPMIFailure(cmd=cmd)
  686 
  687     if out_err[0] == "Chassis Power is on\n":
  688         return states.POWER_ON
  689     elif out_err[0] == "Chassis Power is off\n":
  690         return states.POWER_OFF
  691     else:
  692         return states.ERROR
  693 
  694 
  695 def _process_sensor(sensor_data):
  696     sensor_data_fields = sensor_data.split('\n')
  697     sensor_data_dict = {}
  698     for field in sensor_data_fields:
  699         if not field:
  700             continue
  701         if field.startswith('<<'):
  702             # This is debug data, and can be safely ignored for this.
  703             continue
  704         kv_value = field.split(':')
  705         if len(kv_value) != 2:
  706             continue
  707         sensor_data_dict[kv_value[0].strip()] = kv_value[1].strip()
  708 
  709     return sensor_data_dict
  710 
  711 
  712 def _get_sensor_type(node, sensor_data_dict):
  713     # Have only three sensor type name IDs: 'Sensor Type (Analog)'
  714     # 'Sensor Type (Discrete)' and 'Sensor Type (Threshold)'
  715 
  716     for key in ('Sensor Type (Analog)', 'Sensor Type (Discrete)',
  717                 'Sensor Type (Threshold)'):
  718         try:
  719             return sensor_data_dict[key].split(' ', 1)[0]
  720         except KeyError:
  721             continue
  722 
  723     raise exception.FailedToParseSensorData(
  724         node=node.uuid,
  725         error=(_("parse ipmi sensor data failed, unknown sensor type"
  726                  " data: %(sensors_data)s"),
  727                {'sensors_data': sensor_data_dict}))
  728 
  729 
  730 def _parse_ipmi_sensors_data(node, sensors_data):
  731     """Parse the IPMI sensors data and format to the dict grouping by type.
  732 
  733     We run 'ipmitool' command with 'sdr -v' options, which can return sensor
  734     details in human-readable format, we need to format them to JSON string
  735     dict-based data for Ceilometer Collector which can be sent it as payload
  736     out via notification bus and consumed by Ceilometer Collector.
  737 
  738     :param sensors_data: the sensor data returned by ipmitool command.
  739     :returns: the sensor data with JSON format, grouped by sensor type.
  740     :raises: FailedToParseSensorData when error encountered during parsing.
  741 
  742     """
  743     sensors_data_dict = {}
  744     if not sensors_data:
  745         return sensors_data_dict
  746 
  747     sensors_data_array = sensors_data.split('\n\n')
  748     for sensor_data in sensors_data_array:
  749         sensor_data_dict = _process_sensor(sensor_data)
  750         if not sensor_data_dict:
  751             continue
  752 
  753         sensor_type = _get_sensor_type(node, sensor_data_dict)
  754 
  755         # ignore the sensors which has no current 'Sensor Reading' data
  756         if 'Sensor Reading' in sensor_data_dict:
  757             sensors_data_dict.setdefault(
  758                 sensor_type,
  759                 {})[sensor_data_dict['Sensor ID']] = sensor_data_dict
  760 
  761     # get nothing, no valid sensor data
  762     if not sensors_data_dict:
  763         raise exception.FailedToParseSensorData(
  764             node=node.uuid,
  765             error=(_("parse ipmi sensor data failed, get nothing with input"
  766                      " data: %(sensors_data)s")
  767                    % {'sensors_data': sensors_data}))
  768     return sensors_data_dict
  769 
  770 
  771 @METRICS.timer('send_raw')
  772 @task_manager.require_exclusive_lock
  773 def send_raw(task, raw_bytes):
  774     """Send raw bytes to the BMC. Bytes should be a string of bytes.
  775 
  776     :param task: a TaskManager instance.
  777     :param raw_bytes: a string of raw bytes to send, e.g. '0x00 0x01'
  778     :returns: a tuple with stdout and stderr.
  779     :raises: IPMIFailure on an error from ipmitool.
  780     :raises: MissingParameterValue if a required parameter is missing.
  781     :raises: InvalidParameterValue when an invalid value is specified.
  782 
  783     """
  784     node_uuid = task.node.uuid
  785     LOG.debug('Sending node %(node)s raw bytes %(bytes)s',
  786               {'bytes': raw_bytes, 'node': node_uuid})
  787     driver_info = _parse_driver_info(task.node)
  788     cmd = 'raw %s' % raw_bytes
  789 
  790     try:
  791         out, err = _exec_ipmitool(driver_info, cmd)
  792         LOG.debug('send raw bytes returned stdout: %(stdout)s, stderr:'
  793                   ' %(stderr)s', {'stdout': out, 'stderr': err})
  794     except (exception.PasswordFileFailedToCreate,
  795             processutils.ProcessExecutionError) as e:
  796         LOG.exception('IPMI "raw bytes" failed for node %(node_id)s '
  797                       'with error: %(error)s.',
  798                       {'node_id': node_uuid, 'error': e})
  799         raise exception.IPMIFailure(cmd=cmd)
  800 
  801     return out, err
  802 
  803 
  804 @METRICS.timer('dump_sdr')
  805 def dump_sdr(task, file_path):
  806     """Dump SDR data to a file.
  807 
  808     :param task: a TaskManager instance.
  809     :param file_path: the path to SDR dump file.
  810     :raises: IPMIFailure on an error from ipmitool.
  811     :raises: MissingParameterValue if a required parameter is missing.
  812     :raises: InvalidParameterValue when an invalid value is specified.
  813 
  814     """
  815     node_uuid = task.node.uuid
  816     LOG.debug('Dump SDR data for node %(node)s to file %(name)s',
  817               {'name': file_path, 'node': node_uuid})
  818     driver_info = _parse_driver_info(task.node)
  819     cmd = 'sdr dump %s' % file_path
  820 
  821     try:
  822         out, err = _exec_ipmitool(driver_info, cmd)
  823         LOG.debug('dump SDR returned stdout: %(stdout)s, stderr:'
  824                   ' %(stderr)s', {'stdout': out, 'stderr': err})
  825     except (exception.PasswordFileFailedToCreate,
  826             processutils.ProcessExecutionError) as e:
  827         LOG.exception('IPMI "sdr dump" failed for node %(node_id)s '
  828                       'with error: %(error)s.',
  829                       {'node_id': node_uuid, 'error': e})
  830         raise exception.IPMIFailure(cmd=cmd)
  831 
  832 
  833 def _check_temp_dir():
  834     """Check for Valid temp directory."""
  835     global TMP_DIR_CHECKED
  836     # because a temporary file is used to pass the password to ipmitool,
  837     # we should check the directory
  838     if TMP_DIR_CHECKED is None:
  839         try:
  840             utils.check_dir()
  841         except (exception.PathNotFound,
  842                 exception.DirectoryNotWritable,
  843                 exception.InsufficientDiskSpace) as e:
  844             with excutils.save_and_reraise_exception():
  845                 TMP_DIR_CHECKED = False
  846                 err_msg = (_("Ipmitool drivers need to be able to create "
  847                              "temporary files to pass password to ipmitool. "
  848                              "Encountered error: %s") % e)
  849                 e.message = err_msg
  850                 LOG.error(err_msg)
  851         else:
  852             TMP_DIR_CHECKED = True
  853 
  854 
  855 def _constructor_checks(driver):
  856     """Common checks to be performed when instantiating an ipmitool class."""
  857     try:
  858         _check_option_support(['timing', 'single_bridge', 'dual_bridge'])
  859     except OSError:
  860         raise exception.DriverLoadError(
  861             driver=driver,
  862             reason=_("Unable to locate usable ipmitool command in "
  863                      "the system path when checking ipmitool version"))
  864     _check_temp_dir()
  865 
  866 
  867 def _allocate_port(task, host=None):
  868     node = task.node
  869     dii = node.driver_internal_info or {}
  870     allocated_port = console_utils.acquire_port(host=host)
  871     dii['allocated_ipmi_terminal_port'] = allocated_port
  872     node.driver_internal_info = dii
  873     node.save()
  874     return allocated_port
  875 
  876 
  877 def _release_allocated_port(task):
  878     node = task.node
  879     dii = node.driver_internal_info or {}
  880     allocated_port = dii.pop('allocated_ipmi_terminal_port', None)
  881     if allocated_port:
  882         node.driver_internal_info = dii
  883         node.save()
  884         console_utils.release_port(allocated_port)
  885 
  886 
  887 class IPMIPower(base.PowerInterface):
  888 
  889     def __init__(self):
  890         _constructor_checks(driver=self.__class__.__name__)
  891 
  892     def get_properties(self):
  893         return COMMON_PROPERTIES
  894 
  895     @METRICS.timer('IPMIPower.validate')
  896     def validate(self, task):
  897         """Validate driver_info for ipmitool driver.
  898 
  899         Check that node['driver_info'] contains IPMI credentials.
  900 
  901         :param task: a TaskManager instance containing the node to act on.
  902         :raises: InvalidParameterValue if required ipmi parameters are missing.
  903         :raises: MissingParameterValue if a required parameter is missing.
  904 
  905         """
  906         _parse_driver_info(task.node)
  907         # NOTE(tenbrae): don't actually touch the BMC in validate because it is
  908         #             called too often, and BMCs are too fragile.
  909         #             This is a temporary measure to mitigate problems while
  910         #             1314954 and 1314961 are resolved.
  911 
  912     @METRICS.timer('IPMIPower.get_power_state')
  913     def get_power_state(self, task):
  914         """Get the current power state of the task's node.
  915 
  916         :param task: a TaskManager instance containing the node to act on.
  917         :returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR.
  918         :raises: InvalidParameterValue if required ipmi parameters are missing.
  919         :raises: MissingParameterValue if a required parameter is missing.
  920         :raises: IPMIFailure on an error from ipmitool (from _power_status
  921             call).
  922 
  923         """
  924         # NOTE(TheJulia): Temporary until we promote detect_vendor as
  925         # a higher level management method and able to automatically
  926         # run detection upon either power state sync or in the enrollment
  927         # to management step.
  928         try:
  929             properties = task.node.properties
  930             if not properties.get('vendor'):
  931                 # We have no vendor stored, so we'll go ahead and
  932                 # call to store it.
  933                 vendor = task.driver.management.detect_vendor(task)
  934                 if vendor:
  935                     task.upgrade_lock()
  936                     props = task.node.properties
  937                     props['vendor'] = vendor
  938                     task.node.properties = props
  939                     task.node.save()
  940         except exception.UnsupportedDriverExtension:
  941             pass
  942 
  943         driver_info = _parse_driver_info(task.node)
  944         return _power_status(driver_info)
  945 
  946     @METRICS.timer('IPMIPower.set_power_state')
  947     @task_manager.require_exclusive_lock
  948     def set_power_state(self, task, power_state, timeout=None):
  949         """Turn the power on, off, soft reboot, or soft power off.
  950 
  951         :param task: a TaskManager instance containing the node to act on.
  952         :param power_state: desired power state.
  953           one of ironic.common.states, POWER_ON, POWER_OFF, SOFT_POWER_OFF,
  954           or SOFT_REBOOT.
  955         :param timeout: timeout (in seconds) positive integer (> 0) for any
  956           power state. The timeout is counted once during power off and once
  957           during power on for reboots. ``None`` indicates that the default
  958           timeout will be used.
  959         :raises: InvalidParameterValue if an invalid power state was specified.
  960         :raises: MissingParameterValue if required ipmi parameters are missing
  961         :raises: PowerStateFailure if the power couldn't be set to pstate.
  962 
  963         """
  964         driver_info = _parse_driver_info(task.node)
  965 
  966         if power_state == states.POWER_ON:
  967             driver_utils.ensure_next_boot_device(task, driver_info)
  968             _power_on(task, driver_info, timeout=timeout)
  969         elif power_state == states.POWER_OFF:
  970             _power_off(task, driver_info, timeout=timeout)
  971         elif power_state == states.SOFT_POWER_OFF:
  972             _soft_power_off(task, driver_info, timeout=timeout)
  973         elif power_state == states.SOFT_REBOOT:
  974             _soft_power_off(task, driver_info, timeout=timeout)
  975             driver_utils.ensure_next_boot_device(task, driver_info)
  976             _power_on(task, driver_info, timeout=timeout)
  977         else:
  978             raise exception.InvalidParameterValue(
  979                 _("set_power_state called "
  980                   "with invalid power state %s.") % power_state)
  981 
  982     @METRICS.timer('IPMIPower.reboot')
  983     @task_manager.require_exclusive_lock
  984     def reboot(self, task, timeout=None):
  985         """Cycles the power to the task's node.
  986 
  987         :param task: a TaskManager instance containing the node to act on.
  988         :param timeout: timeout (in seconds) positive integer (> 0) for any
  989           power state. The timeout is counted once during power off and once
  990           during power on for reboots. ``None`` indicates that the default
  991           timeout will be used.
  992         :raises: MissingParameterValue if required ipmi parameters are missing.
  993         :raises: InvalidParameterValue if an invalid power state was specified.
  994         :raises: PowerStateFailure if the final state of the node is not
  995           POWER_ON or the intermediate state of the node is not POWER_OFF.
  996 
  997         """
  998         driver_info = _parse_driver_info(task.node)
  999         # NOTE(jlvillal): Some BMCs will error if setting power state to off if
 1000         # the node is already turned off.
 1001         current_status = _power_status(driver_info)
 1002         if current_status != states.POWER_OFF:
 1003             _power_off(task, driver_info, timeout=timeout)
 1004         driver_utils.ensure_next_boot_device(task, driver_info)
 1005         _power_on(task, driver_info, timeout=timeout)
 1006 
 1007     def get_supported_power_states(self, task):
 1008         """Get a list of the supported power states.
 1009 
 1010         :param task: A TaskManager instance containing the node to act on.
 1011             currently not used.
 1012         :returns: A list with the supported power states defined
 1013                   in :mod:`ironic.common.states`.
 1014         """
 1015         return [states.POWER_ON, states.POWER_OFF, states.REBOOT,
 1016                 states.SOFT_REBOOT, states.SOFT_POWER_OFF]
 1017 
 1018 
 1019 class IPMIManagement(base.ManagementInterface):
 1020 
 1021     def get_properties(self):
 1022         return COMMON_PROPERTIES
 1023 
 1024     def __init__(self):
 1025         _constructor_checks(driver=self.__class__.__name__)
 1026 
 1027     @METRICS.timer('IPMIManagement.validate')
 1028     def validate(self, task):
 1029         """Check that 'driver_info' contains IPMI credentials.
 1030 
 1031         Validates whether the 'driver_info' property of the supplied
 1032         task's node contains the required credentials information.
 1033 
 1034         :param task: a task from TaskManager.
 1035         :raises: InvalidParameterValue if required IPMI parameters
 1036             are missing.
 1037         :raises: MissingParameterValue if a required parameter is missing.
 1038 
 1039         """
 1040         _parse_driver_info(task.node)
 1041 
 1042     def get_supported_boot_devices(self, task):
 1043         """Get a list of the supported boot devices.
 1044 
 1045         :param task: a task from TaskManager.
 1046         :returns: A list with the supported boot devices defined
 1047                   in :mod:`ironic.common.boot_devices`.
 1048 
 1049         """
 1050         return list(BOOT_DEVICE_HEXA_MAP)
 1051 
 1052     @METRICS.timer('IPMIManagement.set_boot_device')
 1053     @task_manager.require_exclusive_lock
 1054     def set_boot_device(self, task, device, persistent=False):
 1055         """Set the boot device for the task's node.
 1056 
 1057         Set the boot device to use on next reboot of the node.
 1058 
 1059         :param task: a task from TaskManager.
 1060         :param device: the boot device, one of
 1061                        :mod:`ironic.common.boot_devices`.
 1062         :param persistent: Boolean value. True if the boot device will
 1063                            persist to all future boots, False if not.
 1064                            Default: False.
 1065         :raises: InvalidParameterValue if an invalid boot device is specified
 1066         :raises: MissingParameterValue if required ipmi parameters are missing.
 1067         :raises: IPMIFailure on an error from ipmitool.
 1068 
 1069         """
 1070         if device not in self.get_supported_boot_devices(task):
 1071             raise exception.InvalidParameterValue(_(
 1072                 "Invalid boot device %s specified.") % device)
 1073 
 1074         # NOTE(tonyb): Some BMCs do not implement Option 0x03, such as OpenBMC
 1075         # and will error when we try to set this.  Resulting in an abort.  If
 1076         # the BMC doesn't support this timeout there isn't a need to disable
 1077         # it.  Let's use a driver option to signify that.
 1078         # NOTE(kaifeng) [ipmi]disable_boot_timeout provides default value if
 1079         # driver_info/ipmi_disable_boot_timeout is not set.
 1080         idt = task.node.driver_info.get('ipmi_disable_boot_timeout')
 1081         if idt is None:
 1082             idt = CONF.ipmi.disable_boot_timeout
 1083         if strutils.bool_from_string(idt):
 1084             # note(JayF): IPMI spec indicates unless you send these raw bytes
 1085             # the boot device setting times out after 60s. Since it's possible
 1086             # it could be >60s before a node is rebooted, we should always send
 1087             # them.  This mimics pyghmi's current behavior, and the
 1088             # "option=timeout" setting on newer ipmitool binaries.
 1089             timeout_disable = "0x00 0x08 0x03 0x08"
 1090             send_raw(task, timeout_disable)
 1091         else:
 1092             LOG.info('For node %(node_uuid)s, '
 1093                      'driver_info[\'ipmi_disable_boot_timeout\'] is set '
 1094                      'to False, so not sending ipmi boot-timeout-disable',
 1095                      {'node_uuid', task.node.uuid})
 1096 
 1097         ifbd = task.node.driver_info.get('ipmi_force_boot_device', False)
 1098         if strutils.bool_from_string(ifbd):
 1099             driver_utils.force_persistent_boot(task,
 1100                                                device,
 1101                                                persistent)
 1102             # Reset persistent to False, in case of BMC does not support
 1103             # persistent or we do not have admin rights.
 1104             persistent = False
 1105 
 1106         # NOTE(lucasagomes): Older versions of the ipmitool utility
 1107         # are not able to set the options "efiboot" and "persistent"
 1108         # at the same time, combining other options seems to work fine,
 1109         # except efiboot. Newer versions of ipmitool (1.8.17) does fix
 1110         # this problem but (some) distros still packaging an older version.
 1111         # To workaround this problem for now we can make use of sending
 1112         # raw bytes to set the boot device for a node in persistent +
 1113         # uefi mode, this will work with newer and older versions of the
 1114         # ipmitool utility. Also see:
 1115         # https://bugs.launchpad.net/ironic/+bug/1611306
 1116         # NOTE(TheJulia): Previously we added raw device support because
 1117         # most distributions are carrying older downstream patched versions
 1118         # of ipmitool where "efiboot" and "persistent" do not work. However,
 1119         # using the embedded support assumes that every vendor out there uses
 1120         # the same device numbers all the time. Reality is not so kind.
 1121         # It should also be noted that the ipmitool chassis bootparm get
 1122         # command also just interprets back the same fields, so if the vendor
 1123         # has set a different value as default, then ipmitool is not going
 1124         # to understand it and may be listing the wrong boot flag as a result.
 1125         # See: https://storyboard.openstack.org/#!/story/2008241
 1126         boot_mode = boot_mode_utils.get_boot_mode(task.node)
 1127         if boot_mode == 'uefi':
 1128             # Long story short: UEFI was added to IPMI after the final spec
 1129             # release occured. This means BMCs may actually NEED to be
 1130             # explicitly told if the boot is persistant because the
 1131             # BMC may return a value which is explicitly parsed as
 1132             # no change, BUT the BMC may treat that as operational default.
 1133             efi_persistence = '0xe0' if persistent else '0xa0'
 1134             # 0xe0 is persistent UEFI boot, 0xa0 is one-time UEFI boot.
 1135             boot_device_map = _vendor_aware_boot_device_map(task)
 1136             raw_cmd = ('0x00 0x08 0x05 %s %s 0x00 0x00 0x00' %
 1137                        (efi_persistence, boot_device_map[device]))
 1138             send_raw(task, raw_cmd)
 1139             return
 1140 
 1141         options = []
 1142         if persistent:
 1143             options.append('persistent')
 1144         # NOTE(TheJulia): Once upon a time we had efiboot here. It doesn't
 1145         # work in all cases and directs us to unhappy places if the values
 1146         # are incorrect.
 1147         cmd = "chassis bootdev %s" % device
 1148         if options:
 1149             cmd = cmd + " options=%s" % ','.join(options)
 1150         driver_info = _parse_driver_info(task.node)
 1151         try:
 1152             out, err = _exec_ipmitool(driver_info, cmd)
 1153         except (exception.PasswordFileFailedToCreate,
 1154                 processutils.ProcessExecutionError) as e:
 1155             LOG.warning('IPMI set boot device failed for node %(node)s '
 1156                         'when executing "ipmitool %(cmd)s". '
 1157                         'Error: %(error)s',
 1158                         {'node': driver_info['uuid'], 'cmd': cmd, 'error': e})
 1159             raise exception.IPMIFailure(cmd=cmd)
 1160 
 1161     @METRICS.timer('IPMIManagement.get_boot_device')
 1162     def get_boot_device(self, task):
 1163         """Get the current boot device for the task's node.
 1164 
 1165         Returns the current boot device of the node.
 1166 
 1167         :param task: a task from TaskManager.
 1168         :raises: InvalidParameterValue if required IPMI parameters
 1169             are missing.
 1170         :raises: IPMIFailure on an error from ipmitool.
 1171         :raises: MissingParameterValue if a required parameter is missing.
 1172         :returns: a dictionary containing:
 1173 
 1174             :boot_device: the boot device, one of
 1175                 :mod:`ironic.common.boot_devices` or None if it is unknown.
 1176             :persistent: Whether the boot device will persist to all
 1177                 future boots or not, None if it is unknown.
 1178 
 1179         """
 1180         driver_info = task.node.driver_info
 1181         driver_internal_info = task.node.driver_internal_info
 1182         ifbd = driver_info.get('ipmi_force_boot_device', False)
 1183 
 1184         driver_info = _parse_driver_info(task.node)
 1185 
 1186         if (strutils.bool_from_string(ifbd)
 1187                 and driver_internal_info.get('persistent_boot_device')
 1188                 and driver_internal_info.get('is_next_boot_persistent', True)):
 1189             return {
 1190                 'boot_device': driver_internal_info['persistent_boot_device'],
 1191                 'persistent': True
 1192             }
 1193 
 1194         cmd = "chassis bootparam get 5"
 1195         response = {'boot_device': None, 'persistent': None}
 1196 
 1197         try:
 1198             out, err = _exec_ipmitool(driver_info, cmd)
 1199         except (exception.PasswordFileFailedToCreate,
 1200                 processutils.ProcessExecutionError) as e:
 1201             LOG.warning('IPMI get boot device failed for node %(node)s '
 1202                         'when executing "ipmitool %(cmd)s". '
 1203                         'Error: %(error)s',
 1204                         {'node': driver_info['uuid'], 'cmd': cmd, 'error': e})
 1205             raise exception.IPMIFailure(cmd=cmd)
 1206         # FIXME(TheJulia): This whole thing needs to be refactored to be based
 1207         # upon the response generated by the "Boot parameter data".
 1208         # See: https://storyboard.openstack.org/#!/story/2008241
 1209         re_obj = re.search('Boot Device Selector : (.+)?\n', out)
 1210         if re_obj:
 1211             boot_selector = re_obj.groups('')[0]
 1212             if 'PXE' in boot_selector:
 1213                 response['boot_device'] = boot_devices.PXE
 1214             elif 'Hard-Drive' in boot_selector:
 1215                 if 'Safe-Mode' in boot_selector:
 1216                     response['boot_device'] = boot_devices.SAFE
 1217                 else:
 1218                     response['boot_device'] = boot_devices.DISK
 1219             elif 'BIOS' in boot_selector:
 1220                 response['boot_device'] = boot_devices.BIOS
 1221             elif 'CD/DVD' in boot_selector:
 1222                 response['boot_device'] = boot_devices.CDROM
 1223 
 1224         response['persistent'] = 'Options apply to all future boots' in out
 1225         return response
 1226 
 1227     @METRICS.timer('IPMIManagement.detect_vendor')
 1228     def detect_vendor(self, task):
 1229         """Detects, stores, and returns the hardware vendor.
 1230 
 1231         If the Node object ``properties`` field does not already contain
 1232         a ``vendor`` field, then this method is intended to query
 1233         Detects the BMC hardware vendor and stores the returned value
 1234         with-in the Node object ``properties`` field if detected.
 1235 
 1236         :param task: A task from TaskManager.
 1237         :raises: InvalidParameterValue if an invalid component, indicator
 1238             or state is specified.
 1239         :raises: MissingParameterValue if a required parameter is missing
 1240         :returns: String representing the BMC reported Vendor or
 1241                   Manufacturer, otherwise returns None.
 1242         """
 1243         try:
 1244             driver_info = _parse_driver_info(task.node)
 1245             out, err = _exec_ipmitool(driver_info, "mc info")
 1246             re_obj = re.search("Manufacturer Name .*: (.+)", out)
 1247             if re_obj:
 1248                 bmc_vendor = str(re_obj.groups('')[0]).lower().split(':')
 1249                 # Pull unparsed data and save the vendor
 1250                 return bmc_vendor[-1]
 1251         except (exception.PasswordFileFailedToCreate,
 1252                 processutils.ProcessExecutionError) as e:
 1253             LOG.warning('IPMI get boot device failed to detect vendor '
 1254                         'of bmc for %(node)s. Error %(err)s',
 1255                         {'node': task.node.uuid,
 1256                          'err': e})
 1257 
 1258     @METRICS.timer('IPMIManagement.get_sensors_data')
 1259     def get_sensors_data(self, task):
 1260         """Get sensors data.
 1261 
 1262         :param task: a TaskManager instance.
 1263         :raises: FailedToGetSensorData when getting the sensor data fails.
 1264         :raises: FailedToParseSensorData when parsing sensor data fails.
 1265         :raises: InvalidParameterValue if required ipmi parameters are missing
 1266         :raises: MissingParameterValue if a required parameter is missing.
 1267         :returns: returns a dict of sensor data group by sensor type.
 1268 
 1269         """
 1270         driver_info = _parse_driver_info(task.node)
 1271         # with '-v' option, we can get the entire sensor data including the
 1272         # extended sensor informations
 1273         cmd = "sdr -v"
 1274         try:
 1275             out, err = _exec_ipmitool(
 1276                 driver_info, cmd, kill_on_timeout=CONF.ipmi.kill_on_timeout)
 1277         except (exception.PasswordFileFailedToCreate,
 1278                 processutils.ProcessExecutionError) as e:
 1279             raise exception.FailedToGetSensorData(node=task.node.uuid,
 1280                                                   error=e)
 1281 
 1282         return _parse_ipmi_sensors_data(task.node, out)
 1283 
 1284     @METRICS.timer('IPMIManagement.inject_nmi')
 1285     @task_manager.require_exclusive_lock
 1286     def inject_nmi(self, task):
 1287         """Inject NMI, Non Maskable Interrupt.
 1288 
 1289         Inject NMI (Non Maskable Interrupt) for a node immediately.
 1290 
 1291         :param task: A TaskManager instance containing the node to act on.
 1292         :raises: IPMIFailure on an error from ipmitool.
 1293         :returns: None
 1294 
 1295         """
 1296         driver_info = _parse_driver_info(task.node)
 1297         try:
 1298             _exec_ipmitool(driver_info, "power diag")
 1299         except (exception.PasswordFileFailedToCreate,
 1300                 processutils.ProcessExecutionError) as err:
 1301             LOG.error('Inject NMI failed for node %(node)s: %(err)s.',
 1302                       {'node': task.node.uuid, 'err': err})
 1303             raise exception.IPMIFailure(cmd="power diag")
 1304 
 1305 
 1306 class VendorPassthru(base.VendorInterface):
 1307 
 1308     def __init__(self):
 1309         _constructor_checks(driver=self.__class__.__name__)
 1310 
 1311     @METRICS.timer('VendorPassthru.send_raw')
 1312     @base.passthru(['POST'],
 1313                    description=_("Send raw bytes to the BMC. Required "
 1314                                  "argument: 'raw_bytes' - a string of raw "
 1315                                  "bytes (e.g. '0x00 0x01')."))
 1316     @task_manager.require_exclusive_lock
 1317     def send_raw(self, task, http_method, raw_bytes):
 1318         """Send raw bytes to the BMC. Bytes should be a string of bytes.
 1319 
 1320         :param task: a TaskManager instance.
 1321         :param http_method: the HTTP method used on the request.
 1322         :param raw_bytes: a string of raw bytes to send, e.g. '0x00 0x01'
 1323         :raises: IPMIFailure on an error from ipmitool.
 1324         :raises: MissingParameterValue if a required parameter is missing.
 1325         :raises:  InvalidParameterValue when an invalid value is specified.
 1326 
 1327         """
 1328         send_raw(task, raw_bytes)
 1329 
 1330     @METRICS.timer('VendorPassthru.bmc_reset')
 1331     @base.passthru(['POST'],
 1332                    description=_("Reset the BMC. Required argument: 'warm' "
 1333                                  "(Boolean) - for warm (True) or cold (False) "
 1334                                  "reset."))
 1335     @task_manager.require_exclusive_lock
 1336     def bmc_reset(self, task, http_method, warm=True):
 1337         """Reset BMC with IPMI command 'bmc reset (warm|cold)'.
 1338 
 1339         :param task: a TaskManager instance.
 1340         :param http_method: the HTTP method used on the request.
 1341         :param warm: boolean parameter to decide on warm or cold reset.
 1342         :raises: IPMIFailure on an error from ipmitool.
 1343         :raises: MissingParameterValue if a required parameter is missing.
 1344         :raises: InvalidParameterValue when an invalid value is specified
 1345 
 1346         """
 1347         node_uuid = task.node.uuid
 1348 
 1349         warm = strutils.bool_from_string(warm)
 1350         if warm:
 1351             warm_param = 'warm'
 1352         else:
 1353             warm_param = 'cold'
 1354 
 1355         LOG.debug('Doing %(warm)s BMC reset on node %(node)s',
 1356                   {'warm': warm_param, 'node': node_uuid})
 1357         driver_info = _parse_driver_info(task.node)
 1358         cmd = 'bmc reset %s' % warm_param
 1359 
 1360         try:
 1361             out, err = _exec_ipmitool(driver_info, cmd)
 1362             LOG.debug('bmc reset returned stdout: %(stdout)s, stderr:'
 1363                       ' %(stderr)s', {'stdout': out, 'stderr': err})
 1364         except (exception.PasswordFileFailedToCreate,
 1365                 processutils.ProcessExecutionError) as e:
 1366             LOG.exception('IPMI "bmc reset" failed for node %(node_id)s '
 1367                           'with error: %(error)s.',
 1368                           {'node_id': node_uuid, 'error': e})
 1369             raise exception.IPMIFailure(cmd=cmd)
 1370 
 1371     def get_properties(self):
 1372         return COMMON_PROPERTIES
 1373 
 1374     @METRICS.timer('VendorPassthru.validate')
 1375     def validate(self, task, method, **kwargs):
 1376         """Validate vendor-specific actions.
 1377 
 1378         If invalid, raises an exception; otherwise returns None.
 1379 
 1380         Valid methods:
 1381           * send_raw
 1382           * bmc_reset
 1383 
 1384         :param task: a task from TaskManager.
 1385         :param method: method to be validated
 1386         :param kwargs: info for action.
 1387         :raises: InvalidParameterValue when an invalid parameter value is
 1388                  specified.
 1389         :raises: MissingParameterValue if a required parameter is missing.
 1390 
 1391         """
 1392         if method == 'send_raw':
 1393             if not kwargs.get('raw_bytes'):
 1394                 raise exception.MissingParameterValue(_(
 1395                     'Parameter raw_bytes (string of bytes) was not '
 1396                     'specified.'))
 1397 
 1398         _parse_driver_info(task.node)
 1399 
 1400 
 1401 class IPMIConsole(base.ConsoleInterface):
 1402     """A base ConsoleInterface that uses ipmitool."""
 1403 
 1404     def __init__(self):
 1405         _constructor_checks(driver=self.__class__.__name__)
 1406 
 1407     def get_properties(self):
 1408         d = COMMON_PROPERTIES.copy()
 1409         d.update(CONSOLE_PROPERTIES)
 1410         return d
 1411 
 1412     @METRICS.timer('IPMIConsole.validate')
 1413     def validate(self, task):
 1414         """Validate the Node console info.
 1415 
 1416         :param task: a task from TaskManager.
 1417         :raises: InvalidParameterValue
 1418         :raises: MissingParameterValue when a required parameter is missing
 1419 
 1420         """
 1421         driver_info = _parse_driver_info(task.node)
 1422         if not driver_info['port'] and CONF.console.port_range is None:
 1423             raise exception.MissingParameterValue(_(
 1424                 "Either missing 'ipmi_terminal_port' parameter in node's "
 1425                 "driver_info or [console]port_range is not configured"))
 1426 
 1427         if driver_info['protocol_version'] != '2.0':
 1428             raise exception.InvalidParameterValue(_(
 1429                 "Serial over lan only works with IPMI protocol version 2.0. "
 1430                 "Check the 'ipmi_protocol_version' parameter in "
 1431                 "node's driver_info"))
 1432 
 1433     def _get_ipmi_cmd(self, driver_info, pw_file):
 1434         """Get ipmi command for ipmitool usage.
 1435 
 1436         :param driver_info: driver info with the ipmitool parameters
 1437         :param pw_file: password file to be used in ipmitool command
 1438         :returns: returns a command string for ipmitool
 1439         """
 1440         return ' '.join(_get_ipmitool_args(driver_info, pw_file=pw_file))
 1441 
 1442     def _start_console(self, driver_info, start_method):
 1443         """Start a remote console for the node.
 1444 
 1445         :param driver_info: the parameters for accessing a node
 1446         :param start_method: console_utils method to start console
 1447         :raises: InvalidParameterValue if required ipmi parameters are missing
 1448         :raises: PasswordFileFailedToCreate if unable to create a file
 1449                  containing the password
 1450         :raises: ConsoleError if the directory for the PID file cannot be
 1451                  created
 1452         :raises: ConsoleSubprocessFailed when invoking the subprocess failed
 1453         """
 1454         path = _console_pwfile_path(driver_info['uuid'])
 1455         pw_file = console_utils.make_persistent_password_file(
 1456             path, driver_info['password'] or '\0')
 1457         ipmi_cmd = self._get_ipmi_cmd(driver_info, pw_file)
 1458         ipmi_cmd += ' sol activate'
 1459 
 1460         try:
 1461             start_method(driver_info['uuid'], driver_info['port'], ipmi_cmd)
 1462         except (exception.ConsoleError, exception.ConsoleSubprocessFailed):
 1463             with excutils.save_and_reraise_exception():
 1464                 ironic_utils.unlink_without_raise(path)
 1465 
 1466 
 1467 class IPMIShellinaboxConsole(IPMIConsole):
 1468     """A ConsoleInterface that uses ipmitool and shellinabox."""
 1469 
 1470     def _get_ipmi_cmd(self, driver_info, pw_file):
 1471         """Get ipmi command for ipmitool usage.
 1472 
 1473         :param driver_info: driver info with the ipmitool parameters
 1474         :param pw_file: password file to be used in ipmitool command
 1475         :returns: returns a command string for ipmitool
 1476         """
 1477         command = super(IPMIShellinaboxConsole, self)._get_ipmi_cmd(
 1478             driver_info, pw_file)
 1479         return ("/:%(uid)s:%(gid)s:HOME:%(basic_command)s"
 1480                 % {'uid': os.getuid(),
 1481                    'gid': os.getgid(),
 1482                    'basic_command': command})
 1483 
 1484     @METRICS.timer('IPMIShellinaboxConsole.start_console')
 1485     def start_console(self, task):
 1486         """Start a remote console for the node.
 1487 
 1488         :param task: a task from TaskManager
 1489         :raises: InvalidParameterValue if required ipmi parameters are missing
 1490         :raises: PasswordFileFailedToCreate if unable to create a file
 1491                  containing the password
 1492         :raises: ConsoleError if the directory for the PID file cannot be
 1493                  created
 1494         :raises: ConsoleSubprocessFailed when invoking the subprocess failed
 1495         """
 1496         driver_info = _parse_driver_info(task.node)
 1497         if not driver_info['port']:
 1498             driver_info['port'] = _allocate_port(task)
 1499 
 1500         self._start_console(driver_info,
 1501                             console_utils.start_shellinabox_console)
 1502 
 1503     @METRICS.timer('IPMIShellinaboxConsole.stop_console')
 1504     def stop_console(self, task):
 1505         """Stop the remote console session for the node.
 1506 
 1507         :param task: a task from TaskManager
 1508         :raises: ConsoleError if unable to stop the console
 1509         """
 1510         try:
 1511             console_utils.stop_shellinabox_console(task.node.uuid)
 1512         finally:
 1513             ironic_utils.unlink_without_raise(
 1514                 _console_pwfile_path(task.node.uuid))
 1515         _release_allocated_port(task)
 1516 
 1517     @METRICS.timer('IPMIShellinaboxConsole.get_console')
 1518     def get_console(self, task):
 1519         """Get the type and connection information about the console."""
 1520         driver_info = _parse_driver_info(task.node)
 1521         url = console_utils.get_shellinabox_console_url(driver_info['port'])
 1522         return {'type': 'shellinabox', 'url': url}
 1523 
 1524 
 1525 class IPMISocatConsole(IPMIConsole):
 1526     """A ConsoleInterface that uses ipmitool and socat."""
 1527 
 1528     @METRICS.timer('IPMISocatConsole.start_console')
 1529     def start_console(self, task):
 1530         """Start a remote console for the node.
 1531 
 1532         :param task: a task from TaskManager
 1533         :raises: InvalidParameterValue if required ipmi parameters are missing
 1534         :raises: PasswordFileFailedToCreate if unable to create a file
 1535                  containing the password
 1536         :raises: ConsoleError if the directory for the PID file cannot be
 1537                  created
 1538         :raises: ConsoleSubprocessFailed when invoking the subprocess failed
 1539         """
 1540         driver_info = _parse_driver_info(task.node)
 1541         if not driver_info['port']:
 1542             driver_info['port'] = _allocate_port(
 1543                 task, host=CONF.console.socat_address)
 1544 
 1545         try:
 1546             self._exec_stop_console(driver_info)
 1547         except OSError:
 1548             # We need to drop any existing sol sessions with sol deactivate.
 1549             # OSError is raised when sol session is already deactivated,
 1550             # so we can ignore it.
 1551             pass
 1552         self._start_console(driver_info, console_utils.start_socat_console)
 1553 
 1554     @METRICS.timer('IPMISocatConsole.stop_console')
 1555     def stop_console(self, task):
 1556         """Stop the remote console session for the node.
 1557 
 1558         :param task: a task from TaskManager
 1559         :raises: ConsoleError if unable to stop the console
 1560         """
 1561         driver_info = _parse_driver_info(task.node)
 1562         try:
 1563             console_utils.stop_socat_console(task.node.uuid)
 1564         finally:
 1565             ironic_utils.unlink_without_raise(
 1566                 _console_pwfile_path(task.node.uuid))
 1567         self._exec_stop_console(driver_info)
 1568         _release_allocated_port(task)
 1569 
 1570     def _exec_stop_console(self, driver_info):
 1571         cmd = "sol deactivate"
 1572         _exec_ipmitool(driver_info, cmd, check_exit_code=[0, 1])
 1573 
 1574     @METRICS.timer('IPMISocatConsole.get_console')
 1575     def get_console(self, task):
 1576         """Get the type and connection information about the console.
 1577 
 1578         :param task: a task from TaskManager
 1579         """
 1580         driver_info = _parse_driver_info(task.node)
 1581         url = console_utils.get_socat_console_url(driver_info['port'])
 1582         return {'type': 'socat', 'url': url}