--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/components/openstack/ironic/files/drivers/modules/solaris_ipmitool.py Wed Jun 10 10:12:11 2015 +0100
@@ -0,0 +1,2585 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2012 NTT DOCOMO, INC.
+# Copyright 2014 International Business Machines Corporation
+# All Rights Reserved.
+#
+# Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Solaris Driver and supporting meta-classes.
+"""
+
+import os
+import platform
+import re
+import select
+import shutil
+import socket
+from subprocess import Popen, PIPE
+import tempfile
+from threading import Thread
+import time
+import urllib2
+from urlparse import urlparse
+
+from lockfile import LockFile, LockTimeout
+from oslo.config import cfg
+from pkg.fmri import is_valid_pkg_name
+from pkg.misc import valid_pub_prefix, valid_pub_url
+from scp import SCPClient
+
+from ironic.common import boot_devices, exception, images, keystone, states, \
+ utils
+from ironic.common.i18n import _, _LW
+from ironic.conductor import task_manager
+from ironic.conductor import utils as manager_utils
+from ironic.db import api as dbapi
+from ironic.drivers import base
+from ironic.drivers.modules import ipmitool
+from ironic.drivers import utils as driver_utils
+from ironic.openstack.common import log as logging
+from ironic.openstack.common import loopingcall, processutils
+
+PLATFORM = platform.system()
+if PLATFORM != "SunOS":
+ import tarfile
+
+
+AI_OPTS = [
+ cfg.StrOpt('server',
+ default='None',
+ help='Host name for AI Server.'),
+ cfg.StrOpt('username',
+ default='None',
+ help='Username to ssh to AI Server.'),
+ cfg.StrOpt('password',
+ default='None',
+ help='Password for user to ssh to AI Server.'),
+ cfg.StrOpt('port',
+ default='22',
+ help='SSH port to use.'),
+ cfg.StrOpt('timeout',
+ default='10',
+ help='SSH socket timeout value in seconds.'),
+ cfg.StrOpt('deploy_interval',
+ default='10',
+ help='Interval in seconds to check AI deployment status.'),
+ cfg.StrOpt('derived_manifest',
+ default='file:///usr/lib/ironic/ironic-manifest.ksh',
+ help='Derived Manifest used for deployment.'),
+ cfg.StrOpt('ssh_key_file',
+ default='None',
+ help='SSH Filename to use.'),
+ cfg.StrOpt('ssh_key_contents',
+ default='None',
+ help='Actual SSH Key contents to use.')
+ ]
+
+AUTH_OPTS = [
+ cfg.StrOpt('auth_strategy',
+ default='keystone',
+ help='Method to use for authentication: noauth or keystone.')
+ ]
+
+SOLARIS_IPMI_OPTS = [
+ cfg.StrOpt('imagecache_dirname',
+ default='/var/lib/ironic/images',
+ help='Default path to image cache.'),
+ cfg.StrOpt('imagecache_lock_timeout',
+ default='60',
+ help='Timeout to wait when attempting to lock refcount file.')
+]
+
+LOG = logging.getLogger(__name__)
+
+CONF = cfg.CONF
+OPT_GROUP = cfg.OptGroup(name='ai',
+ title='Options for the Automated Install driver')
+CONF.register_group(OPT_GROUP)
+CONF.register_opts(AI_OPTS, OPT_GROUP)
+CONF.register_opts(AUTH_OPTS)
+SOLARIS_IPMI_GROUP = cfg.OptGroup(
+ name="solaris_ipmi",
+ title="Options defined in ironic.drivers.modules.solaris_ipmi")
+CONF.register_group(SOLARIS_IPMI_GROUP)
+CONF.register_opts(SOLARIS_IPMI_OPTS, SOLARIS_IPMI_GROUP)
+
+VALID_ARCH = ['x86', 'SPARC']
+VALID_ARCHIVE_SCHEMES = ["file", "http", "https", "glance"]
+VALID_URI_SCHEMES = VALID_ARCHIVE_SCHEMES
+DEFAULT_ARCHIVE_IMAGE_PATH = 'auto_install/manifest/default_archive.xml'
+AI_STRING = "Automated Installation"
+AI_SUCCESS_STRING = AI_STRING + " succeeded"
+AI_FAILURE_STRING = AI_STRING + " failed"
+AI_DEPLOY_STRING = AI_STRING + " started"
+
+REQUIRED_PROPERTIES = {
+ 'ipmi_address': _("IP address or hostname of the node. Required."),
+ 'ipmi_username': _("username to use for IPMI connection. Required."),
+ 'ipmi_password': _("password to use for IPMI connection. Required.")
+}
+
+OPTIONAL_PROPERTIES = {
+ 'ai_manifest': _("Automated install manifest to be used for provisioning. "
+ "Optional."),
+ 'ai_service': _("Automated Install service name to use. Optional."),
+ 'archive_uri': _("URI of archive to deploy. Optional."),
+ 'fmri': _("List of IPS package FMRIs to be installed. "
+ "Required if publishers property is set."),
+ 'install_profiles': _("List of configuration profiles to be applied "
+ "to the installation environment during an install. "
+ "Optional."),
+ 'ipmi_bridging': _("bridging_type; default is \"no\". One of \"single\", "
+ "\"dual\", \"no\". Optional."),
+ 'ipmi_local_address': _("local IPMB address for bridged requests. "
+ "Used only if ipmi_bridging is set "
+ "to \"single\" or \"dual\". Optional."),
+ 'ipmi_priv_level':
+ _("privilege level; default is ADMINISTRATOR. "
+ "One of %s. Optional.") % '. '.join(ipmitool.VALID_PRIV_LEVELS),
+ 'ipmi_target_address': _("destination address for bridged request. "
+ "Required only if ipmi_bridging is set "
+ "to \"single\" or \"dual\"."),
+ 'ipmi_target_channel': _("destination channel for bridged request. "
+ "Required only if ipmi_bridging is set to "
+ "\"single\" or \"dual\"."),
+ 'ipmi_transit_address': _("transit address for bridged request. Required "
+ "only if ipmi_bridging is set to \"dual\"."),
+ 'ipmi_transit_channel': _("transit channel for bridged request. Required "
+ "only if ipmi_bridging is set to \"dual\"."),
+ 'publishers': _("List of IPS publishers to install from, in the format "
+ "name@origin. Required if fmri property is set."),
+ 'sc_profiles': _("List of system configuration profiles to be applied "
+ "to an installed system. Optional.")
+}
+
+COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
+COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
+
+LAST_CMD_TIME = {}
+TIMING_SUPPORT = None
+SINGLE_BRIDGE_SUPPORT = None
+DUAL_BRIDGE_SUPPORT = None
+
+
+def _ssh_execute(ssh_obj, ssh_cmd, raise_exception=True, err_msg=None):
+ """Execute a command via SSH.
+
+ :param ssh_obj: paramiko.SSHClient, an active ssh connection
+ :param ssh_cmd: Command to execute over SSH.
+ :param raise_exception: Wheter to raise exception or not
+ :param err_msg: Custom error message to use
+ :returns: tuple [stdout from command, returncode]
+ :raises: SSHCommandFailed on an error from ssh, if specified to raise.
+ """
+ LOG.debug("_ssh_execute():ssh_cmd: %s" % (ssh_cmd))
+
+ returncode = 0
+ try:
+ stdout = processutils.ssh_execute(ssh_obj, ssh_cmd)[0]
+ except Exception as err:
+ LOG.debug(_("Cannot execute SSH cmd %(cmd)s. Reason: %(err)s.") %
+ {'cmd': ssh_cmd, 'err': err})
+ returncode = 1
+ if raise_exception:
+ if err_msg:
+ raise SolarisIPMIError(msg=err_msg)
+ else:
+ raise exception.SSHCommandFailed(cmd=ssh_cmd)
+
+ return stdout, returncode
+
+
+def _parse_driver_info(node):
+ """Gets the parameters required for ipmitool to access the node.
+
+ Copied from ironic/drivers/modules/ipmitool.py. No differences.
+ Copied locally as REQUIRED_PROPERTIES differs from standard ipmitool.
+
+ :param node: the Node of interest.
+ :returns: dictionary of parameters.
+ :raises: InvalidParameterValue when an invalid value is specified
+ :raises: MissingParameterValue when a required ipmi parameter is missing.
+
+ """
+ LOG.debug("_parse_driver_info()")
+ info = node.driver_info or {}
+ bridging_types = ['single', 'dual']
+ missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
+ if missing_info:
+ raise exception.MissingParameterValue(
+ _("The following IPMI credentials are not supplied"
+ " to IPMI driver: %s.") % missing_info)
+
+ address = info.get('ipmi_address')
+ username = info.get('ipmi_username')
+ password = info.get('ipmi_password')
+ port = info.get('ipmi_terminal_port')
+ priv_level = info.get('ipmi_priv_level', 'ADMINISTRATOR')
+ bridging_type = info.get('ipmi_bridging', 'no')
+ local_address = info.get('ipmi_local_address')
+ transit_channel = info.get('ipmi_transit_channel')
+ transit_address = info.get('ipmi_transit_address')
+ target_channel = info.get('ipmi_target_channel')
+ target_address = info.get('ipmi_target_address')
+
+ if port:
+ try:
+ port = int(port)
+ except ValueError:
+ raise exception.InvalidParameterValue(_(
+ "IPMI terminal port is not an integer."))
+
+ # check if ipmi_bridging has proper value
+ if bridging_type == 'no':
+ # if bridging is not selected, then set all bridging params to None
+ local_address = transit_channel = transit_address = \
+ target_channel = target_address = None
+ elif bridging_type in bridging_types:
+ # check if the particular bridging option is supported on host
+ if not ipmitool._is_option_supported('%s_bridge' % bridging_type):
+ raise exception.InvalidParameterValue(_(
+ "Value for ipmi_bridging is provided as %s, but IPMI "
+ "bridging is not supported by the IPMI utility installed "
+ "on host. Ensure ipmitool version is > 1.8.11"
+ ) % bridging_type)
+
+ # ensure that all the required parameters are provided
+ params_undefined = [param for param, value in [
+ ("ipmi_target_channel", target_channel),
+ ('ipmi_target_address', target_address)] if value is None]
+ if bridging_type == 'dual':
+ params_undefined2 = [param for param, value in [
+ ("ipmi_transit_channel", transit_channel),
+ ('ipmi_transit_address', transit_address)
+ ] if value is None]
+ params_undefined.extend(params_undefined2)
+ else:
+ # if single bridging was selected, set dual bridge params to None
+ transit_channel = transit_address = None
+
+ # If the required parameters were not provided,
+ # raise an exception
+ if params_undefined:
+ raise exception.MissingParameterValue(_(
+ "%(param)s not provided") % {'param': params_undefined})
+ else:
+ raise exception.InvalidParameterValue(_(
+ "Invalid value for ipmi_bridging: %(bridging_type)s,"
+ " the valid value can be one of: %(bridging_types)s"
+ ) % {'bridging_type': bridging_type,
+ 'bridging_types': bridging_types + ['no']})
+
+ if priv_level not in ipmitool.VALID_PRIV_LEVELS:
+ valid_priv_lvls = ', '.join(ipmitool.VALID_PRIV_LEVELS)
+ raise exception.InvalidParameterValue(_(
+ "Invalid privilege level value:%(priv_level)s, the valid value"
+ " can be one of %(valid_levels)s") %
+ {'priv_level': priv_level, 'valid_levels': valid_priv_lvls})
+
+ return {
+ 'address': address,
+ 'username': username,
+ 'password': password,
+ 'port': port,
+ 'uuid': node.uuid,
+ 'priv_level': priv_level,
+ 'local_address': local_address,
+ 'transit_channel': transit_channel,
+ 'transit_address': transit_address,
+ 'target_channel': target_channel,
+ 'target_address': target_address
+ }
+
+
+def _exec_ipmitool(driver_info, command):
+ """Execute the ipmitool command.
+
+ This uses the lanplus interface to communicate with the BMC device driver.
+
+ Copied from ironic/drivers/modules/ipmitool.py. Only one difference.
+ ipmitool.py version expects a string of space separated commands, and
+ it splits this into an list using 'space' as delimiter.
+ This causes setting of bootmode script for SPARC network boot to fail.
+ Solaris versions takes a list() as command paramater, and therefore
+ we don't need to split.
+
+ :param driver_info: the ipmitool parameters for accessing a node.
+ :param command: list() : the ipmitool command to be executed.
+ :returns: (stdout, stderr) from executing the command.
+ :raises: PasswordFileFailedToCreate from creating or writing to the
+ temporary file.
+ :raises: processutils.ProcessExecutionError from executing the command.
+
+ """
+ LOG.debug("SolarisDeploy._exec_ipmitool:driver_info: '%s', "
+ "command: '%s'" % (driver_info, command))
+ args = ['/usr/sbin/ipmitool',
+ '-I',
+ 'lanplus',
+ '-H',
+ driver_info['address'],
+ '-L', driver_info.get('priv_level')
+ ]
+
+ if driver_info['username']:
+ args.append('-U')
+ args.append(driver_info['username'])
+
+ for name, option in ipmitool.BRIDGING_OPTIONS:
+ if driver_info[name] is not None:
+ args.append(option)
+ args.append(driver_info[name])
+
+ # specify retry timing more precisely, if supported
+ if ipmitool._is_option_supported('timing'):
+ num_tries = max(
+ (CONF.ipmi.retry_timeout // CONF.ipmi.min_command_interval), 1)
+ args.append('-R')
+ args.append(str(num_tries))
+
+ args.append('-N')
+ args.append(str(CONF.ipmi.min_command_interval))
+
+ # 'ipmitool' command will prompt password if there is no '-f' option,
+ # we set it to '\0' to write a password file to support empty password
+ with ipmitool._make_password_file(driver_info['password'] or '\0') \
+ as pw_file:
+ args.append('-f')
+ args.append(pw_file)
+ args = args + list(command) # Append as a list don't split(" ")
+
+ # NOTE(deva): ensure that no communications are sent to a BMC more
+ # often than once every min_command_interval seconds.
+ time_till_next_poll = CONF.ipmi.min_command_interval - (
+ time.time() - LAST_CMD_TIME.get(driver_info['address'], 0))
+ if time_till_next_poll > 0:
+ time.sleep(time_till_next_poll)
+ try:
+ out, err = utils.execute(*args)
+ finally:
+ LAST_CMD_TIME[driver_info['address']] = time.time()
+ return out, err
+
+
+def _get_node_architecture(node):
+ """Queries the node for architecture type
+
+ :param node: the Node of interest.
+ :returns: SPARC or X86 depending on architecture discovered
+ :raises: IPMIFailure if ipmitool command fails
+ """
+ LOG.debug("SolarisDeploy._get_node_architecture")
+ ipmi_cmd_args = ['sunoem', 'getval', '/System/Processors/architecture']
+ driver_info = _parse_driver_info(node)
+ try:
+ out, _err = _exec_ipmitool(driver_info, ipmi_cmd_args)
+ except Exception:
+ raise exception.IPMIFailure(cmd=ipmi_cmd_args)
+
+ LOG.debug("SolarisDeploy._get_node_architecture: arch: '%s'" % (out))
+
+ if 'SPARC' in out:
+ return 'SPARC'
+ elif 'x86' in out:
+ return 'x86'
+ else:
+ raise SolarisIPMIError(msg="Unknown node architecture: %s" % (out))
+
+
+def _check_deploy_state(task, node_uuid, deploy_thread):
+ """ Check deployment state of a running install
+
+ Check the deployment status for this node ideally this will be
+ achieved via communicating with the AI Server and querying the
+ telemetry data returned by the AI Client install to the AI Server.
+
+ However until that is integrated we need to maintain a connection
+ with the Serial Console of the node being installed and parse the
+ output to the console made during an install.
+
+ :param task: a TaskManager instance.
+ :param deploy_thread: Threaded class monitor deployment status
+ :returns: Nothing, raises loopingcall.LoopingCallDone() once
+ node deployment status is determined as done or failed.
+ """
+ LOG.debug("_check_deploy_state()")
+ LOG.debug("_check_deploy_state() deploy_thread_state: %s" %
+ (deploy_thread.state))
+
+ # Get DB instance
+ mydbapi = dbapi.get_instance()
+ try:
+ # Get current DB copy of node
+ cur_node = mydbapi.get_node_by_uuid(node_uuid)
+ except exception.NodeNotFound:
+ LOG.info(_("During check_deploy_state, node %(node)s was not "
+ "found and presumed deleted by another process.") %
+ {'node': node_uuid})
+ # Thread should have stopped already, but let's make sure.
+ deploy_thread.stop()
+ if deploy_thread.state in [states.DEPLOYING, states.DEPLOYWAIT]:
+ # Update node with done/fail state
+ if task.node:
+ task.node.provision_state = states.DEPLOYFAIL
+ task.node.last_error = "Failed to find node."
+ task.node.target_provision_state = states.NOSTATE
+ task.node.save()
+ raise loopingcall.LoopingCallDone()
+ except Exception as err:
+ LOG.info(_("During check_deploy_state, node %(node)s could "
+ "not be retrieved: %(err)") %
+ {'node': node_uuid, 'err': err})
+ # Thread should have stopped already, but lets make sure.
+ deploy_thread.stop()
+ if deploy_thread.state in [states.DEPLOYING, states.DEPLOYWAIT]:
+ # Update node with done/fail state
+ if task.node:
+ task.node.last_error = "Failed to find node."
+ task.node.provision_state = states.DEPLOYFAIL
+ task.node.target_provision_state = states.NOSTATE
+ task.node.save()
+ raise loopingcall.LoopingCallDone()
+
+ LOG.debug("_check_deploy_state().cur_node.target_provision_state: %s" %
+ (cur_node.target_provision_state))
+
+ if deploy_thread.state not in [states.DEPLOYING, states.DEPLOYWAIT]:
+ LOG.debug("_check_deploy_state().done: %s" % (deploy_thread.state))
+ # Node has completed deployment, success or failure
+
+ # Thread should have stopped already, but lets make sure.
+ deploy_thread.stop()
+
+ # Update node with done/fail state
+ if deploy_thread.state == states.DEPLOYDONE:
+ cur_node.provision_state = states.ACTIVE
+ elif deploy_thread.state == states.DEPLOYFAIL:
+ cur_node.last_error = "Install failed; check install.log for " + \
+ "more details."
+ cur_node.provision_state = deploy_thread.state
+ else:
+ cur_node.provision_state = deploy_thread.state
+ cur_node.target_provision_state = states.NOSTATE
+ cur_node.save()
+
+ # Raise LoopincCallDone to terminate deployment checking.
+ raise loopingcall.LoopingCallDone()
+
+ elif deploy_thread.state == states.DEPLOYING and \
+ cur_node.provision_state != states.DEPLOYING:
+ # Actual node deployment has initiated
+ LOG.debug("_check_deploy_state().deploying: %s" %
+ (deploy_thread.state))
+ cur_node.provision_state = states.DEPLOYING
+ cur_node.save()
+
+ elif cur_node.target_provision_state == states.NOSTATE:
+ # Node was most likely deleted so end deployment completion checking
+ LOG.debug("_check_deploy_state().deleted: %s" %
+ (cur_node.target_provision_state))
+ deploy_thread.stop()
+ raise loopingcall.LoopingCallDone()
+
+
+def _url_exists(url):
+ """Validate specific exists
+
+ :param url: HTTP url
+ :returns: boolean, True of exists, otherwise False
+ """
+ LOG.debug("_url_exists: url: %s" % (url.strip()))
+ try:
+ _open_url = urllib2.urlopen(urllib2.Request(url))
+ return True
+ except Exception as err:
+ LOG.debug(_("URL %s not reachable: %s") % (url, err))
+ return False
+
+
+def _image_refcount_acquire_lock(image_path):
+ """Acquire a lock on reference count image file
+
+ :param image_path: Path to image file
+ :returns: Acquired LockFile lock
+ """
+ LOG.debug("_image_refcount_acquire_lock: image_path: %s" % (image_path))
+ ref_filename = image_path + ".ref"
+ lock = LockFile(ref_filename)
+ while not lock.i_am_locking():
+ try:
+ if os.path.exists(image_path):
+ image_size_1 = os.path.getsize(image_path)
+ else:
+ image_size_1 = 0
+ lock.acquire(
+ timeout=int(CONF.solaris_ipmi.imagecache_lock_timeout))
+ except LockTimeout:
+ # Check if image_path size has changed, due to still downloading
+ if os.path.exists(image_path):
+ image_size_2 = os.path.getsize(image_path)
+ else:
+ image_size_2 = 0
+
+ if image_size_1 != image_size_2:
+ LOG.debug("_image_refcount_acquire_lock: Image downloading...")
+ continue
+ else:
+ # Assume lock is an old one, force it's removal
+ LOG.debug("_image_refcount_acquire_lock: Breaking stale lock.")
+ lock.break_lock()
+ lock.acquire()
+
+ return lock
+
+
+def _image_refcount_adjust(image_path, count, release=True):
+ """Adjust cached image file reference counter
+
+ :param image_path: Path to image file
+ :param count: Integer count value to adjust reference by
+ :param release: Release the acquired lock or return it.
+ :returns: Acquired lock
+ """
+ LOG.debug("_image_refcount_adjust: image_path: %s, "
+ "count: %s" % (image_path, str(count)))
+
+ if count == 0:
+ # Adjusting by zero makes no sense just return
+ err_msg = _("Zero reference count adjustment attempted "
+ "on file: %s") % (image_path)
+ LOG.error(err_msg)
+ raise SolarisIPMIError(msg=err_msg)
+
+ ref_filename = image_path + ".ref"
+
+ if not os.path.exists(ref_filename):
+ if count < 0:
+ # Cannot decrement reference on non-existent file
+ err_msg = _("Negative reference count adjustment attempted on "
+ "non-existent file: %s") % (image_path)
+ LOG.error(err_msg)
+ raise SolarisIPMIError(msg=err_msg)
+
+ # Create reference count file
+ with open(ref_filename, "w") as fp:
+ fp.write("0")
+
+ # Acquire lock on refcount file
+ lock = _image_refcount_acquire_lock(image_path)
+ if lock is None:
+ err_msg = _("Failed to acquire lock on image: %s") % (image_path)
+ LOG.error(err_msg)
+ raise SolarisIPMIError(msg=err_msg)
+
+ with open(ref_filename, "r+") as fp:
+ ref_count = fp.readline()
+ if len(ref_count) == 0:
+ ref_count = 1
+ ref_count = str(int(ref_count) + count)
+
+ # Check if reference count is zero if so remove
+ # refcount file and image file
+ if int(ref_count) <= 0:
+ lock.release()
+ os.remove(ref_filename)
+ os.remove(image_path)
+ else:
+ fp.seek(0)
+ fp.write(ref_count)
+ if release:
+ lock.release()
+ return lock
+
+
+def _fetch_uri(task, uri):
+ """Retrieve the specified URI to local temporary file
+
+ Removal of locally fetched file is the responsibility of the
+ caller.
+
+ :param task: a TaskManager instance
+ :param uri: URI of file to fetch.
+ """
+ LOG.debug("SolarisDeploy._fetch_uri:uri: '%s'" % (uri))
+ url = urlparse(uri)
+
+ try:
+ if url.scheme == "glance":
+ temp_uri = os.path.join(CONF.solaris_ipmi.imagecache_dirname,
+ url.netloc)
+
+ # Check of image already in cache, retrieve if not
+ if not os.path.isfile(temp_uri):
+ try:
+ # Increment reference, creates refcount file and returns
+ # the acquired lock.
+ lock = _image_refcount_adjust(temp_uri, 1, release=False)
+
+ # Fetch URI from Glance into local file.
+ images.fetch(task.context, url.netloc, temp_uri)
+
+ # Release acquired lock now that file is retrieved
+ lock.release()
+
+ except Exception as err:
+ LOG.error(_("Unable to fetch Glance image: id %s: %s")
+ % (url.netloc, err))
+ raise
+ else:
+ # Increase reference count for this image
+ _image_refcount_adjust(temp_uri, 1)
+
+ else: # http/file scheme handled directly by curl
+ if PLATFORM == "SunOS":
+ _fd, temp_uri = tempfile.mkstemp(
+ dir=CONF.solaris_ipmi.imagecache_dirname)
+ cmd = ["/usr/bin/curl", "-sS", "-o", temp_uri, uri]
+ pc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ _stdout, err = pc.communicate()
+ if pc.returncode != 0:
+ err_msg = _("Failed to retrieve image: %s") % err
+ raise SolarisIPMIError(msg=err_msg)
+ else: # Linux compat
+ temp_uri = os.path.join(CONF.solaris_ipmi.imagecache_dirname,
+ url.path.replace("/", ""))
+ if not os.path.isfile(temp_uri):
+ try:
+ # Increment reference, creates refcount file and
+ # returns the acquired lock.
+ lock = _image_refcount_adjust(temp_uri, 1,
+ release=False)
+
+ # Actually fetch the image
+ cmd = ["/usr/bin/curl", "-sS", "-o", temp_uri, uri]
+ pc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ _stdout, err = pc.communicate()
+ if pc.returncode != 0:
+ err_msg = _("Failed to retrieve image: %s") % err
+ raise SolarisIPMIError(msg=err_msg)
+
+ # Release acquired lock now that file is retrieved
+ lock.release()
+
+ except Exception as err:
+ LOG.error(_("Unable to fetch image: id %s: %s")
+ % (url.netloc, err))
+ raise
+ else:
+ # Increase reference count for this image
+ _image_refcount_adjust(temp_uri, 1)
+ except Exception as err:
+ # Only remove the temporary file if exception occurs
+ # as noted above Caller is responsible for its removal
+ LOG.error(_("Unable to fetch image: uri %s: %s") % (uri, err))
+ if url.scheme == "glance":
+ _image_refcount_adjust(temp_uri, -1)
+ else:
+ os.remove(temp_uri)
+ raise
+
+ return temp_uri
+
+
+def _get_archive_iso_and_uuid(mount_dir, extract_iso=False):
+ """Get ISO name and UUID
+
+ Retrieved from mounted archive if on Solaris
+
+ On non-Solaris systems we cannot mount a UAR so we need to parse the
+ contents of the unified archive and extract ISO and UUID from
+ cached UAR. In this scenario the caller is responsible for removing
+ the extracted file.
+
+ :param mount_dir: Location of locally mounted UAR or locally cached UAR
+ :param extract_iso: Whether to extract ISO file to temp file
+ :returns: Extracted ISO location and UUID
+ """
+ LOG.debug("SolarisDeploy._get_archive_iso_and_uuid:mount_dir: '%s'" %
+ (mount_dir))
+ uuid = None
+ iso = None
+
+ if PLATFORM == "SunOS":
+ ovf_dir = os.path.join(mount_dir, "OVF")
+
+ for uar_file in os.listdir(ovf_dir):
+ if uar_file.endswith('.ovf'):
+ uuid = uar_file.split('.ovf')[0]
+ elif uar_file.endswith('.iso'):
+ iso = os.path.join(ovf_dir, uar_file)
+ else:
+ tf = tarfile.open(name=mount_dir)
+
+ for ti in tf.getmembers():
+ if ti.path.endswith('.ovf'):
+ uuid = ti.path.split('.ovf')[0]
+ elif ti.path.endswith('.iso') and extract_iso:
+ try:
+ temp_tar_dir = tempfile.mkdtemp(
+ dir=CONF.solaris_ipmi.imagecache_dirname)
+ tf.extractall(path=temp_tar_dir, members=[ti])
+ iso = os.path.join(temp_tar_dir, ti.path)
+ except:
+ # Remove temp_tar_dir and contents
+ shutil.rmtree(temp_tar_dir)
+ raise
+
+ return iso, uuid
+
+
+def _mount_archive(task, archive_uri):
+ """Mount a unified archive
+
+ :param archive_uri: URI of unified archive to mount
+ :returns: Path to mounted unified archive
+ """
+ LOG.debug("SolarisDeploy._mount_archive:archive_uri: '%s'" %
+ (archive_uri))
+
+ if urlparse(archive_uri).scheme == "glance":
+ # TODO(mattk):
+ # Ideally mounting the http ISO directly is preferred.
+ # However mount(1M), does not support auth_token
+ # thus we must fetch the image locally and then mount the
+ # local image.
+ # Tried putting a proxy in place to intercept the mount(1M)
+ # http request and adding an auth_token as it proceeds.
+ # However mount(1M) launches a new SMF instance for each HTTP
+ # mount request, and each SMF instance has a minimal environment
+ # set, which does not include http_proxy, thus the custom local
+ # proxy never gets invoked.
+ # Would love to have a new mount(1M) option to accept either
+ # a proxy e.g. -o proxy=<proxy> or to accept setting of http headers
+ # e.g. -o http_header="X-Auth-Token: askdalksjdlakjsd"
+
+ # Retrieve UAR to local temp file for mounting
+ temp_uar = _fetch_uri(task, archive_uri)
+ archive_mount = temp_uar
+ else:
+ # Can mount archive directly
+ temp_uar = None
+ archive_mount = archive_uri
+
+ mount_dir = tempfile.mkdtemp(dir=CONF.solaris_ipmi.imagecache_dirname)
+
+ cmd = ["/usr/sbin/mount", "-F", "uvfs", "-o",
+ "archive=%s" % (archive_mount), "/usr/lib/fs/uafs/uafs", mount_dir]
+ LOG.debug("SolarisDeploy._mount_archive:cmd: '%s'" % (cmd))
+ pc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ _stdout, err = pc.communicate()
+ if pc.returncode != 0:
+ err_msg = _("Failed to mount UAR %s: %s") % (archive_uri, err)
+ shutil.rmtree(mount_dir)
+ raise SolarisIPMIError(msg=err_msg)
+
+ return mount_dir, temp_uar
+
+
+def _umount_archive(mount_dir, temp_uar):
+ """ Unmount archive and remove mount point directory
+
+ :param mount_dir: Path to mounted archive
+ :param temp_uar: Path to glance local uar to remove
+ """
+ LOG.debug("SolarisDeploy._umount_archive:mount_dir: '%s', temp_uar: %s" %
+ (mount_dir, temp_uar))
+
+ cmd = ["/usr/sbin/umount", mount_dir]
+ pc = Popen(cmd, stdout=PIPE, stderr=PIPE)
+ _stdout, err = pc.communicate()
+ if pc.returncode != 0:
+ err_msg = _("Failed to unmount UAR %s: %s") % (mount_dir, err)
+ raise SolarisIPMIError(msg=err_msg)
+
+ shutil.rmtree(mount_dir)
+
+
+def _get_archive_uuid(task):
+ """Get the UUID of an archive
+
+ :param task: a TaskManager instance
+ :returns: UUID string for an archive otherwise raise exception
+ """
+ LOG.debug("SolarisDeploy._get_archive_uuid")
+ uuid = None
+ archive_uri = task.node.driver_info['archive_uri']
+
+ if PLATFORM == "SunOS":
+ mount_dir, temp_uar = _mount_archive(task, archive_uri)
+ try:
+ _iso, uuid = _get_archive_iso_and_uuid(mount_dir)
+ except:
+ _umount_archive(mount_dir, temp_uar)
+ raise
+ _umount_archive(mount_dir, temp_uar)
+ else:
+ temp_uar = _fetch_uri(task, archive_uri)
+ try:
+ _iso, uuid = _get_archive_iso_and_uuid(temp_uar)
+ except:
+ _image_refcount_adjust(temp_uar, -1)
+ raise
+
+ if uuid is None:
+ err_msg = _("Failed to extract UUID from UAR: %s") % archive_uri
+ if PLATFORM != "SunOS":
+ _image_refcount_adjust(temp_uar, -1)
+ raise SolarisIPMIError(msg=err_msg)
+
+ LOG.debug("SolarisDeploy._get_archive_uuid: uuid: %s" % (uuid))
+ return uuid
+
+
+def _validate_archive_uri(task):
+ """Validate archive_uri for reachable, format, etc
+
+ :param task: a TaskManager instance.
+ :raises: InvalidParameterValie if invalid archive_uri
+ """
+ LOG.debug("SolarisDeploy._validate_archive_uri")
+ archive_uri = task.node.driver_info['archive_uri']
+
+ url = urlparse(archive_uri)
+
+ if url.scheme not in VALID_ARCHIVE_SCHEMES:
+ raise exception.InvalidParameterValue(_(
+ "Unsupported archive scheme (%s) referenced in archive_uri (%s).")
+ % (url.scheme, archive_uri))
+
+ if not url.netloc and not url.path:
+ raise exception.InvalidParameterValue(_(
+ "Missing archive name in archive_uri (%s).") % (archive_uri))
+
+ if url.scheme == "glance":
+ # Glance schema only supported if using keystone authorization
+ # otherwise ironic being used standalone
+ if CONF.auth_strategy != "keystone":
+ raise exception.InvalidParameterValue(_(
+ "Glance scheme only supported when using Keystone (%s).")
+ % (archive_uri))
+
+ # Format : glance://<glance UUID>
+ # When parsed by urlparse, Glance image uuid appears as netloc param
+ if not url.netloc:
+ raise exception.InvalidParameterValue(_(
+ "Missing Glance image UUID archive_uri (%s).")
+ % (archive_uri))
+
+ # Validate glance image exists by attempting to get download size
+ try:
+ size = images.download_size(task.context, url.netloc)
+ LOG.debug("Image %s size: %s" % (url.netloc, str(size)))
+ if not size:
+ raise exception.InvalidParameterValue(_(
+ "Glance image not found: %s") % (url.netloc))
+
+ except Exception as err:
+ raise exception.InvalidParameterValue(_(
+ "Failed to validate Glance image '%s': %s") %
+ (url.netloc, err))
+
+ elif url.scheme in ["http", "https"]:
+ # Presuming client authentication using HTTPS is not being used.
+ # Just a secure connection.
+ # TODO(mattk): Do I need to support client side HTTPS authentication
+ if not _url_exists(archive_uri):
+ raise exception.InvalidParameterValue(_(
+ "archive_uri does not exist (%s).") % (archive_uri))
+ elif url.scheme == "file":
+ file_path = os.path.join(os.sep,
+ url.netloc.strip(os.sep),
+ url.path.strip(os.sep))
+ if not os.path.isfile(file_path):
+ raise exception.InvalidParameterValue(_(
+ "archive_uri does not exist (%s).") % (archive_uri))
+
+
+def _format_archive_uri(task, archive_uri):
+ """Format archive URL to be passed as boot argument to AI client
+
+ Transformation of archive_uri is only required if URI scheme is glance.
+
+ :param task: a TaskManager instance.
+ :param archive_uri: URI path to unified archive
+ :returns: Formatted archive URI, and auth_token if needed
+ """
+ LOG.debug("SolarisDeploy._format_archive_uri: archive_uri: %s" %
+ (archive_uri))
+ if archive_uri:
+ url = urlparse(archive_uri)
+
+ if url.scheme == "glance":
+ # Transform uri from glance://<UUID> to
+ # direct glance URL glance://<GLANCE_REST_API>/<UUID>
+ new_uri = "http://%s:%s/v2/images/%s/file" % \
+ (CONF.glance.glance_host, CONF.glance.glance_port,
+ url.netloc)
+ auth_token = task.context.auth_token
+ else:
+ new_uri = archive_uri
+ auth_token = None
+ else:
+ new_uri = None
+ auth_token = None
+
+ return new_uri, auth_token
+
+
+def _validate_ai_manifest(task):
+ """Validate ai_manifest for format, etc
+
+ driver_info/ai_manifest is used to specify a path to a single
+ AI manifest to be used instead of the default derived script.
+ e.g. http://path-to-manifest
+
+ :param task: a TaskManager instance.
+ :raises: InvalidParameterValue if invalid ai_manifest
+ """
+ LOG.debug("SolarisDeploy._validate_ai_manifest")
+ ai_manifest = task.node.driver_info['ai_manifest']
+ _validate_uri(task, ai_manifest)
+
+
+def _validate_profiles(task, profiles):
+ """Validate profiles for format, etc
+
+ Configuration profiles are specified as a plus(+) delimited list of paths
+ e.g. http://path-to-profile+http://path-to-another-profile
+
+ :param task: a TaskManager instance.
+ :param profiles: Plus(+) delimited list of configuration profile
+ :raises: InvalidParameterValue if invalid configuration profile
+ """
+ LOG.debug("SolarisDeploy._validate_profiles: %s" % (profiles))
+
+ # Split profiles into list of paths@environment elements
+ prof_list = [prof.strip() for prof in profiles.split('+') if prof.strip()]
+
+ for profile in prof_list:
+ _validate_uri(task, profile)
+
+
+def _validate_uri(task, uri):
+ """Validate URI for AI Manifest or SC Profile
+
+ :param task: a TaskManager instance.
+ :param uri: URI to AI Manifest or SC profile
+ :raises: InvalidParameterValue if invalid manifest/profile URI
+ """
+ LOG.debug("SolarisDeploy._validate_uri: URI: %s" % (uri))
+ url = urlparse(uri)
+
+ if url.scheme not in VALID_URI_SCHEMES:
+ raise exception.InvalidParameterValue(_(
+ "Unsupported uri scheme (%s) referenced"
+ " in URI (%s).") % (url.scheme, uri))
+
+ if not url.netloc and not url.path:
+ raise exception.InvalidParameterValue(_(
+ "Missing URI name (%s).") % (uri))
+
+ if url.scheme in ["http", "https"]:
+ # Presuming client authentication using HTTPS is not being used.
+ # Just a secure connection.
+ # TODO(mattk): Do I need to support client side HTTPS authentication
+ if not _url_exists(uri):
+ raise exception.InvalidParameterValue(_(
+ "URI does not exist (%s).") % (uri))
+ else:
+ LOG.debug("SolarisDeploy._validate_uri: %s exists." %
+ (uri))
+ elif url.scheme == "file":
+ file_path = os.path.join(os.sep,
+ url.netloc.strip(os.sep),
+ url.path.strip(os.sep))
+ if not os.path.isfile(file_path):
+ raise exception.InvalidParameterValue(_(
+ "URI does not exist (%s).") % (uri))
+ else:
+ LOG.debug("SolarisDeploy._validate_uri: %s exists." %
+ (url.scheme))
+ elif url.scheme == "glance":
+ # Glance schema only supported if using keystone authorization
+ # otherwise ironic being used standalone
+ if CONF.auth_strategy != "keystone":
+ raise exception.InvalidParameterValue(_(
+ "Glance scheme only supported when using Keystone (%s).")
+ % (uri))
+
+ # Format : glance://<glance UUID>
+ # When parsed by urlparse, Glance image uuid appears as netloc param
+ if not url.netloc:
+ raise exception.InvalidParameterValue(_(
+ "Missing Glance image UUID for URI (%s).")
+ % (uri))
+
+ # Validate glance uri exists by attempting to get download size
+ try:
+ size = images.download_size(task.context, url.netloc)
+ LOG.debug("Image %s size: %s" % (url.netloc, str(size)))
+ if not size:
+ raise exception.InvalidParameterValue(_(
+ "Glance image not found: %s") % (url.netloc))
+ else:
+ LOG.debug("SolarisDeploy._validate_uri: %s exists." %
+ (uri))
+
+ except Exception as err:
+ raise exception.InvalidParameterValue(_(
+ "Failed to validate Glance URI '%s': %s") %
+ (url.netloc, err))
+
+
+def _validate_fmri(task):
+ """Validate fmri for format, etc
+
+ driver_info/fmri is a plus(+) delimited list of IPS package
+ FMRIs to be installed. e.g. pkg:/pkg1+pkg:/pkg2
+
+ :param task: a TaskManager instance.
+ :raises: InvalidParameterValue if invalid fmri
+ """
+ LOG.debug("SolarisDeploy._validate_fmri")
+ fmri = task.node.driver_info['fmri']
+
+ # Split fmri into list of possible packages
+ pkg_list = [pkg.strip() for pkg in fmri.split('+') if pkg.strip()]
+ for fmri in pkg_list:
+ _validate_fmri_format(fmri)
+
+
+def _validate_fmri_format(fmri):
+ """Validate FMRI for format
+ FMRI must not contain the publisher and must be of the format:
+
+ pkg:/<package path>
+
+ Note the fmri only contains a single backslash.
+
+ :param fmri: IPS FMRI
+ :raises: InvalidParameterValue if invalid FMRI
+ """
+ LOG.debug("SolarisDeploy._validate_fmri_format: fmri: %s" % (fmri))
+ url = urlparse(fmri)
+
+ if url.scheme != "pkg":
+ raise exception.InvalidParameterValue(_(
+ "Unsupported IPS scheme (%s) referenced in fmri (%s).")
+ % (url.scheme, fmri))
+
+ if url.netloc:
+ raise exception.InvalidParameterValue(_(
+ "Cannot specify publisher name in fmri (%s).") % (fmri))
+
+ if not url.path:
+ raise exception.InvalidParameterValue(_(
+ "Missing IPS package name in fmri (%s).") % (fmri))
+ else:
+ # Validate package name
+ if not is_valid_pkg_name(url.path.strip("/")):
+ raise exception.InvalidParameterValue(_(
+ "Malformed IPS package name in fmri (%s).") % (fmri))
+
+
+def _validate_publishers(task):
+ """Validate custom publisher name/origins for format
+
+ publishers property is a plus(+) delimited list of IPS publishers
+ to be installed from, in the format name@origin. e.g.
+ solaris@http://pkg.oracle.com/solaris+extra@http://int.co.com/extras
+
+ :param task: a TaskManager instance.
+ :raises: InvalidParameterValue if invalid publisher
+ """
+ LOG.debug("SolarisDeploy._validate_publishers")
+ pubs = task.node.driver_info['publishers']
+
+ # Split publishers into list of name@origin publishers
+ pub_list = [pub.strip() for pub in pubs.split('+') if pub.strip()]
+ for pub in pub_list:
+ # Split into name origin
+ name, origin = pub.split('@', 1)
+ if not name or not origin:
+ raise exception.InvalidParameterValue(_(
+ "Malformed IPS publisher must be of format "
+ "name@origin (%s).") % (pub))
+
+ if not valid_pub_prefix(name):
+ raise exception.InvalidParameterValue(_(
+ "Malformed IPS publisher name (%s).") % (name))
+
+ if not valid_pub_url(origin):
+ raise exception.InvalidParameterValue(_(
+ "Malformed IPS publisher origin (%s).") % (origin))
+
+
+def _fetch_and_create(task, obj_type, obj_name, obj_uri, aiservice, mac,
+ env=None):
+ """Fetch manifest/profile and create on AI Server
+
+ :param task: a TaskManager instance.
+ :param obj_type: Type of AI object to create "manifest" or "profile"
+ :param obj_name: manifest/profile name
+ :param obj_uri: URI to manifest/profile to use
+ :param aiservice: AI Service to create manifest/profile for
+ :param mac: MAC address criteria to use
+ :param env: Environment to apply profile to
+ :raises: AICreateProfileFail or AICreateManifestFail
+ """
+ # Fetch URI to local file
+ url = urlparse(obj_uri)
+ temp_file = _fetch_uri(task, obj_uri)
+
+ try:
+ # scp temp file to AI Server
+ remote_file = os.path.join("/tmp", obj_name) + ".xml"
+ aiservice.copy_remote_file(temp_file, remote_file)
+ except Exception as err:
+ LOG.error(_("Fetch and create failed for %s: name: %s: %s") %
+ (obj_type, obj_uri, err))
+ if url.scheme == "glance":
+ _image_refcount_adjust(temp_file, -1)
+ else:
+ os.remove(temp_file)
+ raise
+
+ try:
+ if obj_type == "manifest":
+ # Create AI Profile
+ aiservice.create_manifest(obj_name, remote_file, mac)
+ elif obj_type == "profile":
+ # Create AI Profile
+ aiservice.create_profile(obj_name, remote_file, mac, env)
+
+ except (AICreateManifestFail, AICreateProfileFail) as _err:
+ aiservice.delete_remote_file(remote_file)
+ if url.scheme == "glance":
+ _image_refcount_adjust(temp_file, -1)
+ else:
+ os.remove(temp_file)
+ raise
+
+ # Remove local and remote temporary profiles
+ aiservice.delete_remote_file(remote_file)
+ if url.scheme == "glance":
+ _image_refcount_adjust(temp_file, -1)
+ else:
+ os.remove(temp_file)
+
+
+class DeployStateChecker(Thread):
+ """Thread class to check for deployment completion"""
+
+ def __init__(self, task):
+ """Init method for thread class"""
+ LOG.debug("DeployStateChecker.__init__()")
+ Thread.__init__(self)
+
+ self.task = task
+ self.node = task.node
+ self._state = states.DEPLOYWAIT
+ self.ssh_connection = None
+ self.running = True
+
+ @property
+ def state(self):
+ """Deployment state property"""
+ return self._state
+
+ def run(self):
+ """Start the thread """
+ LOG.debug("DeployStateChecker.run(): Connecting...")
+ client = utils.ssh_connect(self._get_ssh_dict())
+ channel = client.invoke_shell()
+ channel.settimeout(0.0)
+ channel.set_combine_stderr(True)
+
+ # Continuously read stdout from console and parse
+ # specifically for success/failure output
+ while self.running:
+ with tempfile.TemporaryFile(dir='/var/lib/ironic') as tf:
+ while True:
+ rchans, _wchans, _echans = select.select([channel], [], [])
+ if channel in rchans:
+ try:
+ console_data = ""
+ while channel.recv_ready():
+ console_data += channel.recv(1024)
+
+ if len(console_data) == 0:
+ tf.write("\n*** EOF\n")
+ # Confirm string to search for on success
+ if self._string_in_file(tf, AI_SUCCESS_STRING):
+ self._state = states.DEPLOYDONE
+ else:
+ # Didn't succeed so default to failure
+ self._state = states.DEPLOYFAIL
+ self.stop()
+ break
+ tf.write(console_data)
+ tf.flush()
+
+ # Read input buffer for prompt
+ if re.search("->", console_data):
+ # Send console start command
+ channel.send("start -script SP/Console\n")
+
+ # Cater for Yes/No prompts always sending Yes
+ elif re.search("y/n", console_data):
+ channel.send("y\n")
+
+ # Confirm string to search for on success
+ elif self._string_in_file(tf, AI_SUCCESS_STRING):
+ LOG.debug("DeployStateChecker.run(): Done")
+ self._state = states.DEPLOYDONE
+ self.stop()
+ break
+
+ # Confirm string to search for on failure
+ elif self._string_in_file(tf, AI_FAILURE_STRING):
+ LOG.debug("DeployStateChecker.run(): FAIL")
+ self._state = states.DEPLOYFAIL
+ self.stop()
+ break
+
+ elif self._string_in_file(tf, AI_DEPLOY_STRING):
+ LOG.debug(
+ "DeployStateChecker.run(): DEPLOYING")
+ self._state = states.DEPLOYING
+ except socket.timeout:
+ pass
+
+ def stop(self):
+ """Stop the thread"""
+ LOG.debug("DeployStateChecker.stop()")
+ self.running = False
+
+ def _string_in_file(self, fp, string):
+ """Read all data from file checking for string presence
+
+ :param fp: Open file pointer to read
+ :param string: Specific string to check for
+ :returns: boolean True of string present in file, False if not
+ """
+ found_string = False
+
+ # Position read at start of file
+ fp.seek(0)
+ for line in fp:
+ if re.search(string, line):
+ found_string = True
+ break
+
+ # Return current read point to end of file for subsequent writes
+ fp.seek(0, 2)
+ return found_string
+
+ def _get_ssh_dict(self):
+ """Generate SSH Dictionary for SSH Connection via paramiko
+
+ :returns: dictionary for paramiko connection
+ """
+ LOG.debug("DeployStateChecker._get_ssh_dict()")
+
+ driver_info = _parse_driver_info(self.node)
+
+ ssh_dict = {
+ 'host': driver_info.get('address'),
+ 'username': driver_info.get('username'),
+ 'port': driver_info.get('port', 22)
+ }
+
+ if ssh_dict.get('port') is not None:
+ ssh_dict['port'] = int(ssh_dict.get('port'))
+ else:
+ del ssh_dict['port']
+
+ if driver_info['password']:
+ ssh_dict['password'] = driver_info['password']
+
+ LOG.debug("DeployStateChecker._get_ssh_dict():ssh_dict: %s" %
+ (ssh_dict))
+ return ssh_dict
+
+
+class SolarisDeploy(base.DeployInterface):
+ """AI Deploy Interface """
+
+ def get_properties(self):
+ """Return Solaris driver properties"""
+ return COMMON_PROPERTIES
+
+ def validate(self, task):
+ """Validate the driver-specific Node deployment info.
+
+ :param task: a task from TaskManager.
+ :raises: InvalidParameterValue.
+ :raises: MissingParameterValue.
+ """
+ LOG.debug("SolarisDeploy.validate()")
+ LOG.debug(task.context.auth_token)
+
+ # Validate IPMI credentials by getting node architecture
+ try:
+ _arch = _get_node_architecture(task.node)
+ except Exception as err:
+ raise exception.InvalidParameterValue(_(err))
+
+ if not driver_utils.get_node_mac_addresses(task):
+ raise exception.InvalidParameterValue(
+ _("Node %s does not have any port associated with it.") %
+ (task.node.uuid))
+
+ # Ensure server configured
+ if not CONF.ai.server or CONF.ai.server == "None":
+ raise exception.MissingParameterValue(
+ _("AI Server not specified in configuration file."))
+
+ # Ensure username configured
+ if not CONF.ai.username or CONF.ai.username == "None":
+ raise exception.MissingParameterValue(
+ _("AI Server user not specified in configuration file."))
+
+ # One of ssh_key_file / ssh_key_contents / password must be configured
+ if ((not CONF.ai.password or CONF.ai.password == "None") and
+ (not CONF.ai.ssh_key_file or CONF.ai.ssh_key_file == "None") and
+ (not CONF.ai.ssh_key_contents or
+ CONF.ai.ssh_key_contents == "None")):
+ raise exception.MissingParameterValue(
+ _("AI Server authentication not specified. One of password, "
+ "ssh_key_file and ssh_key_contents must be present in "
+ "configuration file."))
+
+ # archive_uri, publishers or fmri are ignored if a ai_manifest is
+ # defined. They should be contained within the custom manifest itself
+ if (task.node.driver_info.get('ai_manifest') and
+ (task.node.driver_info.get('archive_uri') or
+ task.node.driver_info.get('publishers') or
+ task.node.driver_info.get('fmri'))):
+ raise exception.InvalidParameterValue(
+ _("Custom Archive, Publishers or FMRI cannot be specified "
+ "when specifying a custom AI Manifest. They should be "
+ "contained within this custom AI Manifest."))
+
+ # Ensure ai_service is valid if specified in driver
+ if task.node.driver_info.get('ai_service'):
+ aiservice = AIService(task,
+ task.node.driver_info.get('ai_service'))
+ if not aiservice.exists:
+ raise exception.InvalidParameterValue(
+ _("AI Service %s does not exist.") % (aiservice.name))
+
+ # Ensure node archive_uri is valid if specified
+ if task.node.driver_info.get('archive_uri'):
+ # Validate archive_uri for reachable, format, etc
+ _validate_archive_uri(task)
+
+ # Ensure custom publisher provided if FMRI provided
+ if task.node.driver_info.get('fmri') and \
+ not task.node.driver_info.get('publishers'):
+ raise exception.MissingParameterValue(_(
+ "Must specify custom publisher with custom fmri."))
+
+ # Ensure node publishers are valid if specified
+ if task.node.driver_info.get('publishers'):
+ # Validate publishers for format, etc
+ _validate_publishers(task)
+
+ # Ensure node fmri is valid if specified
+ if task.node.driver_info.get('fmri'):
+ # Validate fmri for format, etc
+ _validate_fmri(task)
+
+ # Ensure node sc_profiles is valid if specified
+ if task.node.driver_info.get('sc_profiles'):
+ # Validate sc_profiles for format, etc
+ _validate_profiles(task, task.node.driver_info.get('sc_profiles'))
+
+ # Ensure node install_profiles is valid if specified
+ if task.node.driver_info.get('install_profiles'):
+ # Validate install_profiles for format, etc
+ _validate_profiles(task,
+ task.node.driver_info.get('install_profiles'))
+
+ # Ensure node manifest is valid of specified
+ if task.node.driver_info.get('ai_manifest'):
+ # Validate ai_manifest for format, etc
+ _validate_ai_manifest(task)
+
+ # Try to get the URL of the Ironic API
+ try:
+ CONF.conductor.api_url or keystone.get_service_url()
+ except (exception.CatalogFailure,
+ exception.CatalogNotFound,
+ exception.CatalogUnauthorized):
+ raise exception.InvalidParameterValue(_(
+ "Couldn't get the URL of the Ironic API service from the "
+ "configuration file or Keystone catalog."))
+
+ # Validate driver_info by parsing contents
+ _parse_driver_info(task.node)
+
+ @task_manager.require_exclusive_lock
+ def deploy(self, task):
+ """Perform start deployment a node.
+
+ For AI Deployment of x86 machines, we simply need to set the chassis
+ boot device to pxe and reboot the physical node.
+
+ For AI Deployment of SPARC Machines we need to supply a boot script
+ indicating to perform a network DHCP boot.
+
+ AI Server settings for this node, e.g. client, manifest, boot args
+ etc, will have been configured via prepare() method which is called
+ before deploy().
+
+ :param task: a TaskManager instance.
+ :returns: deploy state DEPLOYWAIT.
+ """
+ LOG.debug("SolarisDeploy.deploy()")
+
+ arch = _get_node_architecture(task.node)
+
+ # Ensure persistence is false so net boot only occurs once
+ if arch == 'x86':
+ # Set boot device to PXE network boot
+ dev_cmd = 'pxe'
+ elif arch == 'SPARC':
+ # Set bootmode script to network DHCP
+ dev_cmd = 'wanboot'
+ else:
+ raise exception.InvalidParameterValue(
+ _("Invalid node architecture of '%s'.") % (arch))
+
+ manager_utils.node_set_boot_device(task, dev_cmd,
+ persistent=False)
+ manager_utils.node_power_action(task, states.REBOOT)
+
+ deploy_thread = DeployStateChecker(task)
+ deploy_thread.start()
+ timer = loopingcall.FixedIntervalLoopingCall(_check_deploy_state,
+ task, task.node.uuid,
+ deploy_thread)
+ timer.start(interval=int(CONF.ai.deploy_interval))
+
+ return states.DEPLOYWAIT
+
+ @task_manager.require_exclusive_lock
+ def tear_down(self, task):
+ """Tear down a previous deployment.
+
+ Reset boot device or bootmode script and power off the node.
+ All actual clean-up is done in the clean_up()
+ method which should be called separately.
+
+ :param task: a TaskManager instance.
+ :returns: deploy state DELETED.
+ """
+ LOG.debug("SolarisDeploy.tear_down()")
+ manager_utils.node_set_boot_device(task, 'disk',
+ persistent=False)
+ manager_utils.node_power_action(task, states.POWER_OFF)
+
+ return states.DELETED
+
+ def prepare(self, task):
+ """Prepare the deployment environment for this node.
+
+ 1. Ensure Node's AI Service is specified and it exists
+ 2. (Re)Create AI Clients for each port/Mac specified for this Node
+ 3. (Re)Create AI Manifest for each port/Mac specified for this Node
+ with specific criteria of MAC address
+
+ AI Service to use for installation is determined from
+ driver_info properties archive_uri or ai_service. archive_uri
+ takes precedence over ai_service.
+
+ 1. archive_uri specified.
+ Extract AI ISO from UAR and create a new AI service if service
+ for this ID does not exist.
+ 2. ai_service specified
+ AI Service must exist.
+ 3. archive_uri & ai_service not specified
+ Use default architecture specific service to perform IPS
+ install.
+
+ :param task: a TaskManager instance.
+ """
+ LOG.debug("SolarisDeploy.prepare()")
+
+ ai_manifest = task.node.driver_info.get('ai_manifest', None)
+ ai_service = task.node.driver_info.get('ai_service', None)
+ arch = _get_node_architecture(task.node)
+ archive_uri = task.node.driver_info.get('archive_uri', None)
+ fmri = task.node.driver_info.get('fmri', None)
+ install_profiles = task.node.driver_info.get('install_profiles', None)
+ publishers = task.node.driver_info.get('publishers', None)
+ sc_profiles = task.node.driver_info.get('sc_profiles', None)
+
+ # Ensure cache dir exists
+ if not os.path.exists(CONF.solaris_ipmi.imagecache_dirname):
+ os.makedirs(CONF.solaris_ipmi.imagecache_dirname)
+
+ # archive_uri, publishers or fmri are ignored if a ai_manifest is
+ # defined. They should be contained within the custom manifest itself
+ if ((ai_manifest) and (archive_uri or publishers or fmri)):
+ raise exception.InvalidParameterValue(
+ _("Custom Archive, Publishers or FMRI cannot be specified "
+ "when specifying a custom AI Manifest. They should be "
+ "contained within this custom AI Manifest."))
+
+ # 1. Ensure Node's AI Service exists, if archive_uri then
+ # create a new service of UUID of archive does not already exist
+ if archive_uri:
+ # Validate archive_uri, format, reachable, etc
+ _validate_archive_uri(task)
+
+ # Extract UUID from archive UAR and instantiate AIService
+ ai_service = _get_archive_uuid(task)
+ aiservice = AIService(task, ai_service)
+
+ elif ai_service:
+ # Instantiate AIService object for this node/service
+ aiservice = AIService(task, ai_service)
+ else:
+ # IPS Install, ensure default architecture service exists
+ if arch == "x86":
+ ai_service = "default-i386"
+ elif arch == 'SPARC':
+ ai_service = "default-sparc"
+ else:
+ raise exception.InvalidParameterValue(
+ _("Invalid node architecture of '%s'.") % (arch))
+
+ # Instantiate AIService object for this node/service
+ aiservice = AIService(task, ai_service)
+
+ # Check if AI Service exists, raise exception of not
+ if not aiservice.exists:
+ if archive_uri:
+ # Create this service
+ aiservice.create_service(archive_uri)
+ else:
+ raise exception.InvalidParameterValue(
+ _("AI Service %s does not exist.") % (aiservice.name))
+
+ # Ensure custom publisher provided if FMRI provided
+ if fmri and not publishers:
+ raise exception.InvalidParameterValue(_(
+ "Must specify custom publisher with custom fmri."))
+
+ # Ensure node publishers are valid if specified
+ if publishers:
+ # Validate publishers for format, etc
+ _validate_publishers(task)
+
+ # Ensure node fmri is valid if specified
+ if fmri:
+ # Validate fmri, format, etc
+ _validate_fmri(task)
+
+ # Ensure node sc_profiles is of valid format if specified
+ if sc_profiles:
+ # Validate sc_profiles for format, etc
+ _validate_profiles(task, sc_profiles)
+
+ # Ensure node install_profiles is of valid format if specified
+ if install_profiles:
+ # Validate install_profiles for format, etc
+ _validate_profiles(task, install_profiles)
+
+ # Ensure node ai_manifest is valid if specified
+ if ai_manifest:
+ # Validate ai_manifest for format, etc
+ _validate_ai_manifest(task)
+
+ for mac in driver_utils.get_node_mac_addresses(task):
+ # 2. Recreate AI Clients for each port/Mac specified for this Node
+ # Check if AI Client exists for this service and if so remove it
+ if mac.lower() in aiservice.clients:
+ # Client exists remove it
+ aiservice.delete_client(mac)
+
+ # Recreate new ai client for this mac address
+ new_uri, auth_token = _format_archive_uri(task, archive_uri)
+ aiservice.create_client(mac, arch, new_uri, auth_token,
+ publishers, fmri)
+
+ # 3. (Re)Create AI Manifest for each port/Mac specified for this
+ # Node. Manifest name will be MAC address stripped of colons
+ manifest_name = mac.replace(':', '')
+
+ # Check if AI Manifest exists for this service and if so remove it
+ if manifest_name in aiservice.manifests:
+ # Manifest exists remove it
+ aiservice.delete_manifest(manifest_name)
+
+ # (Re)Create new ai Manifest for this mac address
+ # If ai_manifest is specified use it as the manifest otherwise
+ # use derived manifest script specified by aiservice.
+ if ai_manifest is not None:
+ # Fetch manifest locally, copy to AI Server so that
+ # installadm create-manifest CLI works.
+ _fetch_and_create(task, "manifest", manifest_name, ai_manifest,
+ aiservice, mac)
+ else:
+ _fetch_and_create(task, "manifest", manifest_name,
+ aiservice.derived_manifest, aiservice, mac)
+
+ # 4. (Re)Create AI Profiles for each port/MAC specified for this
+ # Node, adding a new profile for each SC Profile specified.
+ # Profile Name will be MAC address prefix and counter suffix.
+ # e.g. AAEEBBCCFF66-1
+ profile_prefix = mac.replace(':', '') + "-"
+
+ # Remove all profiles associated with this MAC address and service
+ for profile_name in aiservice.profiles:
+ # Profile name starts with MAC address, assuming ironic
+ # created this profile so remove it.
+ if profile_prefix in profile_name:
+ aiservice.delete_profile(profile_name)
+
+ # Process both sc_profiles and install_profiles filtering into
+ # unique list of profiles and environments to be applied to.
+ if install_profiles is not None:
+ ins_list = [prof.strip() for prof in
+ install_profiles.split('+') if prof.strip()]
+ else:
+ ins_list = []
+
+ prof_dict = dict(((uri, "install") for uri in ins_list))
+
+ if sc_profiles is not None:
+ sc_list = [prof.strip() for prof in sc_profiles.split('+')
+ if prof.strip()]
+ else:
+ sc_list = []
+
+ for profile in sc_list:
+ if profile in prof_dict:
+ prof_dict[profile] = "all"
+ else:
+ prof_dict[profile] = "system"
+
+ profile_index = 0
+ for profile_uri, profile_env in prof_dict.iteritems():
+ profile_index += 1
+ profile_name = profile_prefix + str(profile_index)
+
+ # Fetch profile locally, copy to AI Server so that
+ # installadm create-profile CLI works.
+ _fetch_and_create(task, "profile", profile_name, profile_uri,
+ aiservice, mac, env=profile_env)
+
+ # Ensure local copy of archive_uri is removed if not needed
+ if archive_uri:
+ url = urlparse(archive_uri)
+ if url.scheme == "glance":
+ temp_uar = os.path.join(CONF.solaris_ipmi.imagecache_dirname,
+ url.netloc)
+ _image_refcount_adjust(temp_uar, -1)
+ elif PLATFORM != "SunOS":
+ temp_uar = os.path.join(CONF.solaris_ipmi.imagecache_dirname,
+ url.path.replace("/", ""))
+ _image_refcount_adjust(temp_uar, -1)
+
+ def clean_up(self, task):
+ """Clean up the deployment environment for this node.
+
+ As node is being torn down we need to clean up specific
+ AI Clients and Manifests associated with MAC addresses
+ associated with this node.
+
+ 1. Delete AI Clients for each port/Mac specified for this Node
+ 2. Delete AI Manifest for each port/Mac specified for this Node
+
+ :param task: a TaskManager instance.
+ """
+ LOG.debug("SolarisDeploy.clean_up()")
+
+ ai_service = task.node.driver_info.get('ai_service', None)
+ arch = _get_node_architecture(task.node)
+ archive_uri = task.node.driver_info.get('archive_uri', None)
+
+ # Instantiate AIService object for this node/service
+ if archive_uri:
+ aiservice = AIService(task, _get_archive_uuid(task))
+ elif ai_service:
+ aiservice = AIService(task, ai_service)
+ else:
+ if arch == "x86":
+ ai_service = "default-i386"
+ elif arch == 'SPARC':
+ ai_service = "default-sparc"
+ else:
+ raise exception.InvalidParameterValue(
+ _("Invalid node architecture of '%s'.") % (arch))
+ aiservice = AIService(task, ai_service)
+
+ # Check if AI Service exists, log message if already removed
+ if not aiservice.exists:
+ # There is nothing to clean up as service removed
+ LOG.info(_("AI Service %s already removed.") % (aiservice.name))
+ else:
+ for mac in driver_utils.get_node_mac_addresses(task):
+ # 1. Delete AI Client for this MAC Address
+ if mac.lower() in aiservice.clients:
+ aiservice.delete_client(mac)
+
+ # 2. Delete AI Manifest for this MAC Address
+ manifest_name = mac.replace(':', '')
+ if manifest_name in aiservice.manifests:
+ aiservice.delete_manifest(manifest_name)
+
+ # 3. Remove AI Profiles for this MAC Address
+ profile_prefix = mac.replace(':', '') + "-"
+
+ # Remove all profiles associated with this MAC address
+ for profile_name in aiservice.profiles:
+ if profile_prefix in profile_name:
+ aiservice.delete_profile(profile_name)
+
+ # Ensure local copy of archive_uri is removed if not needed
+ if archive_uri:
+ url = urlparse(archive_uri)
+ if url.scheme == "glance":
+ temp_uar = os.path.join(CONF.solaris_ipmi.imagecache_dirname,
+ url.netloc)
+ _image_refcount_adjust(temp_uar, -1)
+ elif PLATFORM != "SunOS":
+ temp_uar = os.path.join(CONF.solaris_ipmi.imagecache_dirname,
+ url.path.replace("/", ""))
+ _image_refcount_adjust(temp_uar, -1)
+
+ def take_over(self, _task):
+ """Take over management of this task's node from a dead conductor."""
+ """ TODO(mattk): Determine if this is required"""
+ LOG.debug("SolarisDeploy.take_over()")
+
+
+class SolarisManagement(base.ManagementInterface):
+ """Management class for solaris nodes."""
+
+ def get_properties(self):
+ """Return Solaris driver properties"""
+ return COMMON_PROPERTIES
+
+ def __init__(self):
+ try:
+ ipmitool._check_option_support(['timing', 'single_bridge',
+ 'dual_bridge'])
+ except OSError:
+ raise exception.DriverLoadError(
+ driver=self.__class__.__name__,
+ reason=_("Unable to locate usable ipmitool command in "
+ "the system path when checking ipmitool version"))
+
+ def validate(self, task):
+ """Check that 'driver_info' contains IPMI credentials.
+
+ Validates whether the 'driver_info' property of the supplied
+ task's node contains the required credentials information.
+
+ :param task: a task from TaskManager.
+ :raises: InvalidParameterValue if required IPMI parameters
+ are missing.
+ :raises: MissingParameterValue if a required parameter is missing.
+
+ """
+ _parse_driver_info(task.node)
+
+ def get_supported_boot_devices(self, task=None):
+ """Get a list of the supported boot devices.
+
+ :param task: a task from TaskManager.
+ :returns: A list with the supported boot devices defined
+ in :mod:`ironic.common.boot_devices`.
+
+ """
+ if task is None:
+ return [boot_devices.PXE, boot_devices.DISK, boot_devices.CDROM,
+ boot_devices.BIOS, boot_devices.SAFE]
+ else:
+ # Get architecture of node and return supported boot devices
+ arch = _get_node_architecture(task.node)
+ if arch == 'x86':
+ return [boot_devices.PXE, boot_devices.DISK,
+ boot_devices.CDROM, boot_devices.BIOS,
+ boot_devices.SAFE]
+ elif arch == 'SPARC':
+ return [boot_devices.DISK, 'wanboot']
+ else:
+ raise exception.InvalidParameterValue(
+ _("Invalid node architecture of '%s'.") % (arch))
+
+ @task_manager.require_exclusive_lock
+ def set_boot_device(self, task, device, persistent=False):
+ """Set the boot device for the task's node.
+
+ Set the boot device to use on next reboot of the node.
+
+ :param task: a task from TaskManager.
+ :param device: the boot device, one of
+ :mod:`ironic.common.boot_devices`.
+ :param persistent: Boolean value. True if the boot device will
+ persist to all future boots, False if not.
+ Default: False.
+ :raises: InvalidParameterValue if an invalid boot device is specified
+ :raises: MissingParameterValue if required ipmi parameters are missing.
+ :raises: IPMIFailure on an error from ipmitool.
+
+ """
+ LOG.debug("SolarisManagement.set_boot_device: %s" % device)
+
+ arch = _get_node_architecture(task.node)
+ archive_uri = task.node.driver_info.get('archive_uri')
+ publishers = task.node.driver_info.get('publishers')
+ fmri = task.node.driver_info.get('fmri')
+
+ if arch == 'x86':
+ if device not in self.get_supported_boot_devices(task=task):
+ raise exception.InvalidParameterValue(_(
+ "Invalid boot device %s specified.") % device)
+ cmd = ["chassis", "bootdev", device]
+ if persistent:
+ cmd = cmd + " options=persistent"
+ elif arch == 'SPARC':
+ # Set bootmode script to network DHCP or disk
+ if device == 'wanboot':
+ boot_cmd = 'set /HOST/bootmode script="'
+ script_str = 'boot net:dhcp - install'
+ if archive_uri:
+ new_uri, auth_token = _format_archive_uri(task,
+ archive_uri)
+ script_str += ' archive_uri=%s' % (new_uri)
+
+ if auth_token is not None:
+ # Add auth_token to boot arg, AI archive transfer will
+ # use this by setting X-Auth-Token header when using
+ # curl to retrieve archive from glance.
+ script_str += ' auth_token=%s' % \
+ (task.context.auth_token)
+
+ if publishers:
+ pub_list = [pub.strip() for pub in publishers.split('+')
+ if pub.strip()]
+ script_str += ' publishers=%s' % ('+'.join(pub_list))
+
+ if fmri:
+ pkg_list = [pkg.strip() for pkg in fmri.split('+')
+ if pkg.strip()]
+ script_str += ' fmri=%s' % ('+'.join(pkg_list))
+
+ # bootmode script property has a size restriction of 255
+ # characters raise error if this is breached.
+ if len(script_str) > 255:
+ raise exception.InvalidParameterValue(_(
+ "SPARC firmware bootmode script length exceeds 255:"
+ " %s") % script_str)
+ boot_cmd += script_str + '"'
+ cmd = ['sunoem', 'cli', boot_cmd]
+ elif device == 'disk':
+ cmd = ['sunoem', 'cli',
+ 'set /HOST/bootmode script=""']
+ else:
+ raise exception.InvalidParameterValue(_(
+ "Invalid boot device %s specified.") % (device))
+ else:
+ raise exception.InvalidParameterValue(
+ _("Invalid node architecture of '%s'.") % (arch))
+
+ driver_info = _parse_driver_info(task.node)
+ try:
+ _out, _err = _exec_ipmitool(driver_info, cmd)
+ except (exception.PasswordFileFailedToCreate,
+ processutils.ProcessExecutionError) as err:
+ LOG.warning(_LW('IPMI set boot device failed for node %(node)s '
+ 'when executing "ipmitool %(cmd)s". '
+ 'Error: %(error)s'),
+ {'node': driver_info['uuid'],
+ 'cmd': cmd, 'error': err})
+ raise exception.IPMIFailure(cmd=cmd)
+
+ def get_boot_device(self, task):
+ """Get the current boot device for the task's node.
+
+ Returns the current boot device of the node.
+
+ :param task: a task from TaskManager.
+ :raises: InvalidParameterValue if required IPMI parameters
+ are missing.
+ :raises: IPMIFailure on an error from ipmitool.
+ :raises: MissingParameterValue if a required parameter is missing.
+ :returns: a dictionary containing:
+
+ :boot_device: the boot device, one of
+ :mod:`ironic.common.boot_devices` or None if it is unknown.
+ :persistent: Whether the boot device will persist to all
+ future boots or not, None if it is unknown.
+
+ """
+ LOG.debug("SolarisManagement.get_boot_device")
+ arch = _get_node_architecture(task.node)
+ driver_info = _parse_driver_info(task.node)
+ response = {'boot_device': None, 'persistent': None}
+
+ if arch == 'x86':
+ cmd = ["chassis", "bootparam", "get", "5"]
+ elif arch == 'SPARC':
+ cmd = ['sunoem', 'getval', '/HOST/bootmode/script']
+ else:
+ raise exception.InvalidParameterValue(
+ _("Invalid node architecture of '%s'.") % (arch))
+
+ try:
+ out, _err = _exec_ipmitool(driver_info, cmd)
+ except (exception.PasswordFileFailedToCreate,
+ processutils.ProcessExecutionError) as err:
+ LOG.warning(_LW('IPMI get boot device failed for node %(node)s '
+ 'when executing "ipmitool %(cmd)s". '
+ 'Error: %(error)s'),
+ {'node': driver_info['uuid'],
+ 'cmd': cmd, 'error': err})
+ raise exception.IPMIFailure(cmd=cmd)
+
+ if arch == 'x86':
+ re_obj = re.search('Boot Device Selector : (.+)?\n', out)
+ if re_obj:
+ boot_selector = re_obj.groups('')[0]
+ if 'PXE' in boot_selector:
+ response['boot_device'] = boot_devices.PXE
+ elif 'Hard-Drive' in boot_selector:
+ if 'Safe-Mode' in boot_selector:
+ response['boot_device'] = boot_devices.SAFE
+ else:
+ response['boot_device'] = boot_devices.DISK
+ elif 'BIOS' in boot_selector:
+ response['boot_device'] = boot_devices.BIOS
+ elif 'CD/DVD' in boot_selector:
+ response['boot_device'] = boot_devices.CDROM
+
+ response['persistent'] = 'Options apply to all future boots' in out
+ elif arch == 'SPARC':
+ if "net:dhcp" in out:
+ response['boot_device'] = 'wanboot'
+ else:
+ response['boot_device'] = 'disk'
+ LOG.debug(response)
+ return response
+
+ def get_sensors_data(self, task):
+ """Get sensors data.
+
+ :param task: a TaskManager instance.
+ :raises: FailedToGetSensorData when getting the sensor data fails.
+ :raises: FailedToParseSensorData when parsing sensor data fails.
+ :raises: InvalidParameterValue if required ipmi parameters are missing
+ :raises: MissingParameterValue if a required parameter is missing.
+ :returns: returns a dict of sensor data group by sensor type.
+
+ """
+ driver_info = _parse_driver_info(task.node)
+ # with '-v' option, we can get the entire sensor data including the
+ # extended sensor informations
+ cmd = "-v sdr"
+ try:
+ out, _err = _exec_ipmitool(driver_info, cmd)
+ except (exception.PasswordFileFailedToCreate,
+ processutils.ProcessExecutionError) as err:
+ raise exception.FailedToGetSensorData(node=task.node.uuid,
+ error=err)
+
+ return ipmitool._parse_ipmi_sensors_data(task.node, out)
+
+
+class AIService():
+ """AI Service"""
+
+ def __init__(self, task, name):
+ """Initialize AIService object
+
+ :param task: a TaskManager instance
+ :param name: AI Service name
+ """
+ LOG.debug("AIService.__init__()")
+ self.task = task
+ self.name = name
+ self._clients = list()
+ self._image_path = None
+ self._manifests = list()
+ self._profiles = list()
+ self._ssh_obj = None
+ self._derived_manifest = None
+
+ @property
+ def ssh_obj(self):
+ """paramiko.SSHClient active connection"""
+ LOG.debug("AIService.ssh_obj")
+ if self._ssh_obj is None:
+ self._ssh_obj = self._get_ssh_connection()
+ return self._ssh_obj
+
+ @property
+ def manifests(self):
+ """list() of manifest names for this service"""
+ LOG.debug("AIService.manifests")
+ if not self._manifests:
+ self._manifests = self._get_manifest_names()
+ return self._manifests
+
+ @property
+ def profiles(self):
+ """list() of profile names for this service"""
+ LOG.debug("AIService.profiles")
+ if not self._profiles:
+ self._profiles = self._get_profile_names()
+ return self._profiles
+
+ @property
+ def clients(self):
+ """list() of all client names(mac addresses) On AI Server"""
+ LOG.debug("AIService.clients")
+ if not self._clients:
+ self._clients = self._get_all_client_names()
+ return self._clients
+
+ @property
+ def exists(self):
+ """True/False indicator of this service exists of not"""
+ LOG.debug("AIService.exists")
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm list -n " + self.name
+ try:
+ stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ return False
+
+ if self.name != self._parse_service_name(stdout):
+ return False
+ else:
+ return True
+
+ @property
+ def image_path(self):
+ """image_path for this service"""
+ LOG.debug("AIService.image_path")
+ if self._image_path is None:
+ self._image_path = self._get_image_path()
+ return self._image_path
+
+ @property
+ def derived_manifest(self):
+ """Access default derived manifest URI"""
+ LOG.debug("AIService.derived_manifest")
+ if not self._derived_manifest:
+ self._derived_manifest = CONF.ai.derived_manifest
+ return self._derived_manifest
+
+ def create_service(self, archive_uri):
+ """Create a new AI Service for this object
+
+ :param archive_uri: archive_uri to create service from
+ """
+
+ LOG.debug("AIService.create_service(): %s" % (self.name))
+
+ if PLATFORM == "SunOS":
+ # 1. Fetch archive
+ mount_dir, temp_uar = _mount_archive(self.task, archive_uri)
+ iso, uuid = _get_archive_iso_and_uuid(mount_dir)
+ else:
+ # 1. Fetch archive and Extract ISO file
+ temp_uar = _fetch_uri(self.task, archive_uri)
+ iso, uuid = _get_archive_iso_and_uuid(temp_uar, extract_iso=True)
+
+ # 2. scp AI ISO from archive to AI Server
+ remote_iso = os.path.join("/tmp", uuid) + ".iso"
+ try:
+ self.copy_remote_file(iso, remote_iso)
+ except:
+ if PLATFORM == "SunOS":
+ _umount_archive(mount_dir, temp_uar)
+ if urlparse(archive_uri).scheme == "glance":
+ _image_refcount_adjust(temp_uar, -1)
+ else:
+ shutil.rmtree(os.path.dirname(iso))
+ _image_refcount_adjust(temp_uar, -1)
+ raise
+
+ if PLATFORM != "SunOS":
+ # Remove temp extracted ISO file
+ shutil.rmtree(os.path.dirname(iso))
+
+ # 3. Create a new AI Service
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm create-service " + \
+ " -y -n " + uuid + " -s " + remote_iso
+
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ self.name = uuid
+ self._clients = []
+ self._manifests = []
+ self._profiles = []
+
+ except Exception as _err:
+ self.delete_remote_file(remote_iso)
+ if PLATFORM == "SunOS":
+ _umount_archive(mount_dir, temp_uar)
+ else:
+ _image_refcount_adjust(temp_uar, -1)
+ raise AICreateServiceFail(
+ _("Failed to create AI Service %s") % (uuid))
+
+ # 4. Remove copy of AI ISO on AI Server
+ self.delete_remote_file(remote_iso)
+
+ if PLATFORM == "SunOS":
+ # 5. Unmount UAR
+ _umount_archive(mount_dir, temp_uar)
+
+ # 6. Decrement reference count for image
+ if temp_uar is not None:
+ _image_refcount_adjust(temp_uar, -1)
+
+ def delete_service(self):
+ """Delete the current AI Service"""
+ LOG.debug("AIService.delete_service():name: %s" % (self.name))
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm delete-service" + \
+ " -r -y -n " + self.name
+
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AIDeleteServiceFail(
+ _("Failed to delete AI Service %s") % (self.name))
+
+ def create_client(self, mac, arch, archive_uri, auth_token,
+ publishers, fmri):
+ """Create a client associated with this service
+
+ :param mac: MAC Address of client to create
+ :param arch: Machine architecture for this node
+ :param archive_uri: URI of archive to install node from
+ :param auth_token: Authorization token for glance UAR retrieval
+ :param publishers: IPS publishers list in name@origin format
+ :param fmri: IPS package FMRIs to install
+ :returns: Nothing exception raised if deletion fails
+ """
+ LOG.debug("AIService.create_client():mac: %s" % (mac))
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm create-client -e " + \
+ mac + " -n " + self.name
+
+ # Add specific boot arguments for 'x86' clients only
+ if arch == 'x86':
+ ai_cmd += " -b install=true,console=ttya"
+
+ if archive_uri:
+ ai_cmd += ",archive_uri=%s" % (archive_uri)
+
+ if auth_token:
+ ai_cmd += ",auth_token=%s" % (auth_token)
+
+ if publishers:
+ pub_list = [pub.strip() for pub in publishers.split('+')
+ if pub.strip()]
+ ai_cmd += ",publishers='%s'" % ('+'.join(pub_list))
+
+ if fmri:
+ pkg_list = [pkg.strip() for pkg in fmri.split('+')
+ if pkg.strip()]
+ ai_cmd += ",fmri='%s'" % ('+'.join(pkg_list))
+
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AICreateClientFail(_("Failed to create AI Client %s") %
+ (mac))
+
+ # If arch x86 customize grub reducing grub menu timeout to 0
+ if arch == 'x86':
+ custom_grub = "/tmp/%s.grub" % (mac)
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm export -e " + \
+ mac + " -G | /usr/bin/sed -e 's/timeout=30/timeout=0/'" + \
+ " > %s" % (custom_grub)
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AICreateClientFail(
+ _("Failed to create custom grub menu for %s.") % (mac))
+
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm set-client -e " + \
+ mac + " -G %s" % (custom_grub)
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AICreateClientFail(
+ _("Failed to customize AI Client %s grub menu.") % (mac))
+
+ self.delete_remote_file(custom_grub)
+
+ self._clients = self._get_all_client_names()
+
+ def delete_client(self, mac):
+ """Delete a specific client regardless of service association
+
+ :param mac: MAC Address of client to remove
+ :returns: Nothing exception raised if deletion fails
+ """
+ LOG.debug("AIService.delete_client():mac: %s" % (mac))
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm delete-client -e " + mac
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AIDeleteClientFail(_("Failed to delete AI Client %s") %
+ (mac))
+
+ # update list of clients for this service
+ self._clients = self._get_all_client_names()
+
+ def create_manifest(self, manifest_name, manifest_path, mac):
+ """Create a manifest associated with this service
+
+ :param manifest_name: manifest_name to create
+ :param manifest_path: path to manifest file to use
+ :param mac: MAC address to add as criteria
+ :returns: Nothing exception raised if creation fails
+ """
+ LOG.debug("AIService.create_manifest():manifest_name: "
+ "'%s', manifest_path: '%s', mac: '%s'" %
+ (manifest_name, manifest_path, mac))
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm create-manifest -n " + \
+ self.name + " -m " + manifest_name + " -f " + manifest_path + \
+ " -c mac=" + mac
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AICreateManifestFail(_("Failed to create AI Manifest %s.") %
+ (manifest_name))
+
+ # Update list of manifests for this service
+ self._manifests = self._get_manifest_names()
+
+ def delete_manifest(self, manifest_name):
+ """Delete a specific manifest
+
+ :param manifest_name: name of manifest to remove
+ :returns: Nothing exception raised if deletion fails
+ """
+ LOG.debug("AIService.delete_manifest():manifest_name: %s" %
+ (manifest_name))
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm delete-manifest -m " + \
+ manifest_name + " -n " + self.name
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AIDeleteManifestFail(_("Failed to delete AI Manifest %s") %
+ (manifest_name))
+
+ # Update list of manifests for this service
+ self._manifests = self._get_manifest_names()
+
+ def create_profile(self, profile_name, profile_path, mac, env):
+ """Create a profile associated with this service
+
+ :param profile)_name: profile name to create
+ :param profile_path: path to profile file to use
+ :param mac: MAC address to add as criteria
+ :param env: Environment to apply profile to
+ :returns: Nothing exception raised if creation fails
+ """
+ LOG.debug("AIService.create_profile():profile_name: "
+ "'%s', profile_path: '%s', mac: '%s'" %
+ (profile_name, profile_path, mac))
+
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm create-profile -n " + \
+ self.name + " -p " + profile_name + " -f " + profile_path + \
+ " -c mac=" + mac
+
+ if env is not None:
+ ai_cmd = ai_cmd + " -e " + env
+
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AICreateProfileFail(_("Failed to create AI Profile %s.") %
+ (profile_name))
+
+ # Update list of profiles for this service
+ self._profiles = self._get_profile_names()
+
+ def delete_profile(self, profile_name):
+ """Delete a specific profile
+
+ :param profile_name: name of profile to remove
+ :returns: Nothing exception raised if deletion fails
+ """
+ LOG.debug("AIService.delete_profile():profile_name: %s" %
+ (profile_name))
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm delete-profile -p " + \
+ profile_name + " -n " + self.name
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as _err:
+ raise AIDeleteProfileFail(_("Failed to delete AI Profile %s") %
+ (profile_name))
+
+ # Update list of profiles for this service
+ self._profiles = self._get_profile_names()
+
+ def copy_remote_file(self, local, remote):
+ """Using scp copy local file to remote location
+
+ :param local: Local file path to copy
+ :param remote: Remote file path to copy to
+ :returns: Nothing, exception raised on failure
+ """
+ LOG.debug("AIService.copy_remote_file():local: %s, remote: %s" %
+ (local, remote))
+ try:
+ scp = SCPClient(self.ssh_obj.get_transport())
+ scp.put(local, remote)
+ except Exception as err:
+ err_msg = _("Failed to copy file to remote server: %s") % err
+ raise SolarisIPMIError(msg=err_msg)
+
+ def delete_remote_file(self, path):
+ """Remove remote file in AI Server
+
+ :param path: Path of remote file to remove
+ :return: Nothing exception raised on failure
+ """
+ LOG.debug("AIService.delete_remote_file():path: %s" %
+ (path))
+
+ ai_cmd = "/usr/bin/rm -f " + path
+ try:
+ _stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+ except Exception as err:
+ err_msg = _("Failed to delete remote file: %s") % err
+ raise SolarisIPMIError(msg=err_msg)
+
+ def _get_image_path(self):
+ """Retrieve image_path for this service
+
+ :returns: image_path property
+ """
+ LOG.debug("AIService._get_image_path()")
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm list -vn " + self.name
+ stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd)
+
+ for line in stdout.splitlines():
+ words = line.split()
+ if len(words) > 2 and words[0] == "Image" and words[1] == "Path":
+ image_path = words[-1]
+ LOG.debug("AIService._get_image_path():image_path: %s" % (image_path))
+ return image_path
+
+ def _parse_client(self, list_out):
+ """Return service name and client from installadm list -e output
+
+ :param list_out: stdout from installadm list -e
+ :returns: Service Name and MAC Address
+ """
+ LOG.debug("AIService._parse_client():list_out: %s" % (list_out))
+ lines = list_out.splitlines()
+ service_name = None
+ client_name = None
+
+ if len(lines[2].split()[0]) > 0:
+ service_name = lines[2].split()[0]
+
+ if len(lines[2].split()[1]) > 0:
+ client_name = lines[2].split()[1]
+
+ LOG.debug("AIService._parse_client():service_name: %s" %
+ (service_name))
+ LOG.debug("AIService._parse_client():client_name: %s" % (client_name))
+ return service_name, client_name
+
+ def _parse_service_name(self, list_out):
+ """Given installadm list -n output, parse out service name
+
+ :param list_out: stdout from installadm list -n
+ :returns: Service Name
+ """
+ LOG.debug("AIService._parse_service_name():list_out: %s" % (list_out))
+ service_name = None
+
+ lines = list_out.splitlines()
+ if len(lines[2].split()[0]) > 0:
+ service_name = lines[2].split()[0]
+
+ LOG.debug("AIService._parse_service_name():service_name: %s" %
+ (service_name))
+ return service_name
+
+ def _get_ssh_connection(self):
+ """Returns an SSH client connected to a node.
+
+ :returns: paramiko.SSHClient, an active ssh connection.
+ """
+ LOG.debug("AIService._get_ssh_connection()")
+ return utils.ssh_connect(self._get_ssh_dict())
+
+ def _get_ssh_dict(self):
+ """Generate SSH Dictionary for SSH Connection via paramiko
+
+ :returns: dictionary for paramiko connection
+ """
+ LOG.debug("AIService._get_ssh_dict()")
+ if not CONF.ai.server or not CONF.ai.username:
+ raise exception.InvalidParameterValue(_(
+ "SSH server and username must be set."))
+
+ ssh_dict = {
+ 'host': CONF.ai.server,
+ 'username': CONF.ai.username,
+ 'port': int(CONF.ai.port),
+ 'timeout': int(CONF.ai.timeout)
+ }
+
+ key_contents = key_filename = password = None
+ if CONF.ai.ssh_key_contents and CONF.ai.ssh_key_contents != "None":
+ key_contents = CONF.ai.ssh_key_contents
+ if CONF.ai.ssh_key_file and CONF.ai.ssh_key_file != "None":
+ key_filename = CONF.ai.ssh_key_file
+ if CONF.ai.password and CONF.ai.password != "None":
+ password = CONF.ai.password
+
+ if len(filter(None, (key_filename, key_contents))) != 1:
+ raise exception.InvalidParameterValue(_(
+ "SSH requires one and only one of "
+ "ssh_key_file or ssh_key_contents to be set."))
+ if password:
+ ssh_dict['password'] = password
+
+ if key_contents:
+ ssh_dict['key_contents'] = key_contents
+ else:
+ if not os.path.isfile(key_filename):
+ raise exception.InvalidParameterValue(_(
+ "SSH key file %s not found.") % key_filename)
+ ssh_dict['key_filename'] = key_filename
+ LOG.debug("AIService._get_ssh_dict():ssh_dict: %s" % (ssh_dict))
+ return ssh_dict
+
+ def _get_manifest_names(self):
+ """Get a list of manifest names for this service
+
+ :returns: list() of manifest names
+ """
+ LOG.debug("AIService._get_manifest_names()")
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm list -mn " + self.name
+ stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd,
+ err_msg=_("Failed to retrieve manifests"
+ " for service %s") % (self.name))
+ return self._parse_names(stdout)
+
+ def _get_profile_names(self):
+ """Get a list of profile names for this service
+
+ :returns: list() of profile names
+ """
+ LOG.debug("AIService._get_profile_names()")
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm list -pn " + self.name
+ stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd,
+ err_msg=_("Failed to retrieve profiles for "
+ "service %s") % (self.name))
+ return self._parse_names(stdout)
+
+ def _get_all_client_names(self):
+ """Get a list of client names for this service
+
+ :returns: list() of client/mac names
+ """
+ LOG.debug("AIService._get_all_client_names()")
+ ai_cmd = "/usr/bin/pfexec /usr/sbin/installadm list -c"
+ stdout, _rc = _ssh_execute(self.ssh_obj, ai_cmd,
+ err_msg=_("Failed to retrieve clients for "
+ "service %s") % (self.name))
+ # Store client names all in lower case
+ return [client.lower() for client in self._parse_names(stdout)]
+
+ def _parse_names(self, list_out):
+ """Parse client/manifest/profile names from installadm list output
+
+ Note: when we convert to using RAD, parsing installadm CLI output
+ will not be required, as API will return a list of names.
+
+ :param list_out: stdout from installadm list -c or -mn or -pn
+ :returns: a list of client/manifest/profile names
+ """
+ LOG.debug("AIService._parse_names():list_out: %s" %
+ (list_out))
+ names = []
+ lines = list_out.splitlines()
+
+ # Get index into string for client/manifest/profile names
+ # client/manifest/profile names are all in 2nd column of output
+ if len(lines) > 1:
+ col_start = lines[1].index(" --")
+
+ for line in range(2, len(lines)):
+ names.append(lines[line][col_start:].split()[0])
+
+ LOG.debug("AIService._parse_names():names: %s" % (names))
+ return names
+
+
+# Custom Exceptions
+class AICreateServiceFail(exception.IronicException):
+ """Exception type for AI Service creation failure"""
+ pass
+
+
+class AIDeleteServiceFail(exception.IronicException):
+ """Exception type for AI Service deletion failure"""
+ pass
+
+
+class AICreateClientFail(exception.IronicException):
+ """Exception type for AI Client creation failure"""
+ pass
+
+
+class AIDeleteClientFail(exception.IronicException):
+ """Exception type for AI Client deletion failure"""
+ pass
+
+
+class AICreateManifestFail(exception.IronicException):
+ """Exception type for AI Manifest creation failure"""
+ pass
+
+
+class AIDeleteManifestFail(exception.IronicException):
+ """Exception type for AI Manifest deletion failure"""
+ pass
+
+
+class AICreateProfileFail(exception.IronicException):
+ """Exception type for AI Profile creation failure"""
+ pass
+
+
+class AIDeleteProfileFail(exception.IronicException):
+ """Exception type for AI Profile deletion failure"""
+ pass
+
+
+class SolarisIPMIError(exception.IronicException):
+ """Generic Solaris IPMI driver exception"""
+ message = _("%(msg)s")