# 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.
#
#
# 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
#
#
# 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 errno
import os
import platform
import re
import select
import shutil
import socket
import tempfile
import time
import urllib2
import six
if PLATFORM != "SunOS":
import tarfile
else:
AI_OPTS = [
'or ssh_key_contents are set, this config setting is used '
'to provide the passphrase if required. If an encrypted '
'key is used, set this to the passphrase.'),
]
]
'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.")
}
'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."),
'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."),
'ipmi_port': _("remote IPMI RMCP port. Optional."),
'ipmi_priv_level':
_("privilege level; default is ADMINISTRATOR. "
'ipmi_bridging': _("bridging_type; default is \"no\". One of \"single\", "
"\"dual\", \"no\". Optional."),
'ipmi_transit_channel': _("transit channel for bridged request. Required "
"only if ipmi_bridging is set to \"dual\"."),
'ipmi_transit_address': _("transit address for bridged request. Required "
"only if ipmi_bridging is set to \"dual\"."),
'ipmi_target_channel': _("destination channel for bridged request. "
"Required only if ipmi_bridging is set to "
"\"single\" or \"dual\"."),
'ipmi_target_address': _("destination address for bridged request. "
"Required only if ipmi_bridging is set "
"to \"single\" or \"dual\"."),
'ipmi_local_address': _("local IPMB address for bridged requests. "
"Used only if ipmi_bridging is set "
"to \"single\" or \"dual\". Optional."),
'ipmi_protocol_version': _('the version of the IPMI protocol; default '
'is "2.0". One of "1.5", "2.0". Optional.'),
'ipmi_force_boot_device': _("Whether Ironic should specify the boot "
"device to the BMC each time the server "
"is turned on, eg. because the BMC is not "
"capable of remembering the selected boot "
"device across power cycles; default value "
"is False. Optional.")
}
IPMI_PROPERTIES = [
('mac', '/System/host_primary_mac_address'),
('serial', '/System/serial_number'),
('cpu_arch', '/System/Processors/architecture'),
('memory_mb', '/System/Memory/installed_memory'),
('cpus', '/System/Processors/installed_cpus'),
('datalinks', '/System/Networking/installed_eth_nics'),
('disks', '/System/Storage/installed_disks'),
('local_gb', '/System/Storage/installed_disk_size')
]
LAST_CMD_TIME = {}
"""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.
"""
returncode = 0
stdout = None
try:
returncode = 1
if raise_exception:
if err_msg:
else:
return stdout, returncode
"""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.
"""
if missing_info:
raise exception.MissingParameterValue(_(
"Missing the following IPMI credentials in node's"
" driver_info: %s.") % missing_info)
if not username:
if not password:
raise exception.InvalidParameterValue(_(
"Invalid IPMI protocol version value %(version)s, the valid "
"value can be one of %(valid_versions)s") %
if port is not None:
if dest_port is not None:
# check if ipmi_bridging has proper value
if bridging_type == 'no':
# if bridging is not selected, then set all bridging params to None
target_address) = (None,) * 5
elif bridging_type in bridging_types:
# check if the particular bridging option is supported on host
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
("ipmi_target_channel", target_channel),
if bridging_type == 'dual':
("ipmi_transit_channel", transit_channel),
('ipmi_transit_address', transit_address)
] if value is None]
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(_(
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,
raise exception.InvalidParameterValue(_(
"Invalid privilege level value:%(priv_level)s, the valid value"
" can be one of %(valid_levels)s") %
return {
'address': address,
'dest_port': dest_port,
'username': username,
'password': password,
'port': port,
'priv_level': priv_level,
'local_address': local_address,
'transit_channel': transit_channel,
'transit_address': transit_address,
'target_channel': target_channel,
'target_address': target_address,
'protocol_version': protocol_version,
'force_boot_device': force_boot_device
}
"""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.
"""
ipmi_version = ('lanplus'
else 'lan')
'-I',
'-H',
driver_info['address'],
]
if driver_info['dest_port']:
if driver_info['username']:
if driver_info[name] is not None:
# specify retry timing more precisely, if supported
while True:
# NOTE(deva): ensure that no communications are sent to a BMC more
# often than once every min_command_interval seconds.
if time_till_next_poll > 0:
# Resetting the list that will be utilized so the password arguments
# from any previous execution are preserved.
# '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(
) as pw_file:
try:
except processutils.ProcessExecutionError as e:
(num_tries == 0) or
not err_list):
'"%(cmd)s" for node %(node)s. '
'Error: %(error)s'),
{
'cmd': e.cmd,
'error': e
})
else:
'"%(cmd)s" for node %(node)s. '
'Error: %(error)s'),
{
'cmd': e.cmd,
'error': e
})
finally:
"""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
"""
if cpu_arch is None:
try:
(err)))
% (cpu_arch))
if 'SPARC' in cpu_arch:
return 'SPARC'
elif 'x86' in cpu_arch:
return 'x86'
else:
% (cpu_arch))
""" 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.
"""
try:
# Get current DB copy of node
except exception.NodeNotFound:
"found and presumed deleted by another process.") %
{'node': node_uuid})
# Thread should have stopped already, but let's make sure.
"transitioning to '%s' state." % \
raise loopingcall.LoopingCallDone()
"not be retrieved: %(err)s") %
# Thread should have stopped already, but lets make sure.
"transitioning to '%s' state." % \
raise loopingcall.LoopingCallDone()
# Node has completed deployment, success or failure
# Thread should have stopped already, but lets make sure.
if deploy_thread.error is not None:
else:
"for more details."
else:
# Raise LoopincCallDone to terminate deployment checking.
raise loopingcall.LoopingCallDone()
# Actual node deployment has initiated
# Node was most likely deleted so end deployment completion checking
raise loopingcall.LoopingCallDone()
# Node deployment has for some reason failed already, exit thread
raise loopingcall.LoopingCallDone()
"""Validate specific exists
:param url: HTTP url
:returns: boolean, True of exists, otherwise False
"""
try:
return True
return False
"""Acquire a lock on reference count image file
:param image_path: Path to image file
:returns: Acquired LockFile lock
"""
while not lock.i_am_locking():
try:
else:
image_size_1 = 0
except LockTimeout:
# Check if image_path size has changed, due to still downloading
else:
image_size_2 = 0
if image_size_1 != image_size_2:
continue
else:
# Assume lock is an old one, force it's removal
return lock
"""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
"""
if count == 0:
# Adjusting by zero makes no sense just return
"on file: %s") % (image_path)
if count < 0:
# Cannot decrement reference on non-existent file
"non-existent file: %s") % (image_path)
# Create reference count file
# Acquire lock on refcount file
if lock is None:
ref_count = 1
# Check if reference count is zero if so remove
# refcount file and image file
try:
raise
try:
raise
else:
if release:
return lock
"""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.
"""
try:
# Check of image already in cache, retrieve if not
try:
# Increment reference, creates refcount file and returns
# the acquired lock.
# Fetch URI from Glance into local file.
# Release acquired lock now that file is retrieved
raise
else:
# Increase reference count for this image
if PLATFORM == "SunOS":
else: # Linux compat
try:
# Increment reference, creates refcount file and
# returns the acquired lock.
# Actually fetch the image
# Release acquired lock now that file is retrieved
raise
else:
# Increase reference count for this image
# Only remove the temporary file if exception occurs
# as noted above Caller is responsible for its removal
else:
try:
raise
raise
return temp_uri
"""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
"""
(mount_dir))
uuid = None
iso = None
if PLATFORM == "SunOS":
else:
try:
except:
# Remove temp_tar_dir and contents
raise
"""Mount a unified archive
:param archive_uri: URI of unified archive to mount
:returns: Path to mounted unified archive
"""
(archive_uri))
# 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
else:
# Can mount archive directly
temp_uar = None
""" Unmount archive and remove mount point directory
:param mount_dir: Path to mounted archive
:param temp_uar: Path to glance local uar to remove
"""
"""Get the UUID of an archive
:param task: a TaskManager instance
:returns: UUID string for an archive otherwise raise exception
"""
uuid = None
if PLATFORM == "SunOS":
try:
except:
raise
else:
try:
except:
raise
if uuid is None:
if PLATFORM != "SunOS":
return uuid
"""Validate archive_uri for reachable, format, etc
:param task: a TaskManager instance.
:raises: InvalidParameterValie if invalid archive_uri
"""
raise exception.InvalidParameterValue(_(
"Unsupported archive scheme (%s) referenced in archive_uri (%s).")
raise exception.InvalidParameterValue(_(
"Missing archive name in archive_uri (%s).") % (archive_uri))
# Glance schema only supported if using keystone authorization
# otherwise ironic being used standalone
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
raise exception.InvalidParameterValue(_(
"Missing Glance image UUID archive_uri (%s).")
% (archive_uri))
# Validate glance image exists by attempting to get download size
try:
if not size:
raise exception.InvalidParameterValue(_(
raise exception.InvalidParameterValue(_(
"Failed to validate Glance image '%s': %s") %
# 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))
raise exception.InvalidParameterValue(_(
"archive_uri does not exist (%s).") % (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
"""
(archive_uri))
if archive_uri:
# Transform uri from glance://<UUID> to
# direct glance URL glance://<GLANCE_REST_API>/<UUID>
else:
auth_token = None
else:
new_uri = None
auth_token = None
return new_uri, auth_token
"""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.
:param task: a TaskManager instance.
:raises: InvalidParameterValue if invalid ai_manifest
"""
"""Validate profiles for format, etc
Configuration profiles are specified as a plus(+) delimited list of paths
:param task: a TaskManager instance.
:param profiles: Plus(+) delimited list of configuration profile
:raises: InvalidParameterValue if invalid configuration profile
"""
# Split profiles into list of paths@environment elements
"""Validate URI for AI Manifest or SC Profile
:param task: a TaskManager instance.
:param uri: URI to AI Manifest or SC profile
"""
raise exception.InvalidParameterValue(_(
"Unsupported uri scheme (%s) referenced"
raise exception.InvalidParameterValue(_(
"Missing URI name (%s).") % (uri))
# 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:
(uri))
raise exception.InvalidParameterValue(_(
"URI does not exist (%s).") % (uri))
else:
# Glance schema only supported if using keystone authorization
# otherwise ironic being used standalone
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
raise exception.InvalidParameterValue(_(
"Missing Glance image UUID for URI (%s).")
% (uri))
# Validate glance uri exists by attempting to get download size
try:
if not size:
raise exception.InvalidParameterValue(_(
else:
(uri))
raise exception.InvalidParameterValue(_(
"Failed to validate Glance URI '%s': %s") %
"""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
"""
# Split fmri into list of possible packages
"""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
"""
raise exception.InvalidParameterValue(_(
"Unsupported IPS scheme (%s) referenced in fmri (%s).")
raise exception.InvalidParameterValue(_(
"Cannot specify publisher name in fmri (%s).") % (fmri))
raise exception.InvalidParameterValue(_(
"Missing IPS package name in fmri (%s).") % (fmri))
elif PLATFORM == "SunOS":
# Validate package name
try:
# PkgFmri object does not like the @latest special case so
# strip it off if it's there.
except IllegalFmri as err:
publishers property is a plus(+) delimited list of IPS publishers
to be installed from, in the format name@origin. e.g.
:param task: a TaskManager instance.
:raises: InvalidParameterValue if invalid publisher
"""
# Split publishers into list of name@origin publishers
try:
# Split into name origin
except ValueError:
raise exception.InvalidParameterValue(_(
"Malformed IPS publisher, must be of format "
"name@origin (%s).") % (pub))
raise exception.InvalidParameterValue(_(
"Malformed IPS publisher, must be of format "
"name@origin (%s).") % (pub))
if PLATFORM == "SunOS":
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))
env=None):
:param task: a TaskManager instance.
:param obj_type: Type of AI object to create "manifest" or "profile"
:param mac: MAC address criteria to use
:param env: Environment to apply profile to
:raises: AICreateProfileFail or AICreateManifestFail
"""
# Fetch URI to local file
try:
# scp temp file to AI Server
else:
raise
try:
if obj_type == "manifest":
# Create AI Profile
elif obj_type == "profile":
# Create AI Profile
else:
raise
# Remove local and remote temporary profiles
else:
"""Thread class to check for deployment completion"""
"""Init method for thread class"""
self.ssh_connection = None
"""Deployment state property"""
"""Deployment error property"""
"""Start the thread """
# Occasionally SSH connection fails, make three attempts
# before returning failure.
while (connection_attempt < 3):
try:
connection_attempt += 1
break
except SSHException as err:
if connection_attempt < 3:
continue
else:
return
else:
return
# Continuously read stdout from console and parse
while True:
try:
console_data = ""
while channel.recv_ready():
# Confirm string to search for on success
else:
# Didn't succeed so default to failure
break
# Read input buffer for prompt
# Send console start command
# Confirm string to search for on success
break
# Confirm string to search for on failure
break
"DeployStateChecker.run(): DEPLOYING")
pass
"""Stop the thread"""
"""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
"""
# Position read at start of file
break
# Return current read point to end of file for subsequent writes
return found_string
"""Generate SSH Dictionary for SSH Connection via paramiko
:returns: dictionary for paramiko connection
"""
ssh_dict = {
}
else:
del ssh_dict['port']
if driver_info['password']:
(ssh_dict))
return ssh_dict
"""AI Deploy Interface """
"""Return Solaris driver properties"""
return COMMON_PROPERTIES
"""Validate the driver-specific Node deployment info.
:param task: a task from TaskManager.
:raises: InvalidParameterValue.
:raises: MissingParameterValue.
"""
# Validate IPMI credentials by getting node architecture
try:
raise exception.InvalidParameterValue(
_("Node %s does not have any port associated with it.") %
# Ensure server configured
raise exception.MissingParameterValue(
_("AI Server not specified in configuration file."))
# Ensure username configured
raise exception.MissingParameterValue(
_("AI Server user not specified in configuration file."))
# One of ssh_key_file / ssh_key_contents / password must be configured
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
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
raise exception.InvalidParameterValue(
# Ensure node archive_uri is valid if specified
# Validate archive_uri for reachable, format, etc
# Ensure custom publisher provided if FMRI provided
raise exception.MissingParameterValue(_(
"Must specify custom publisher with custom fmri."))
# Ensure node publishers are valid if specified
# Validate publishers for format, etc
# Ensure node fmri is valid if specified
# Validate fmri for format, etc
# Ensure node sc_profiles is valid if specified
# Validate sc_profiles for format, etc
# Ensure node install_profiles is valid if specified
# Validate install_profiles for format, etc
# Ensure node manifest is valid of specified
# Validate ai_manifest for format, etc
# Try to get the URL of the Ironic API
try:
except (exception.KeystoneFailure,
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
"""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.
"""
# Ensure persistence is false so net boot only occurs once
if cpu_arch == 'x86':
# Set boot device to PXE network boot
dev_cmd = 'pxe'
elif cpu_arch == 'SPARC':
# Set bootmode script to network DHCP
dev_cmd = 'wanboot'
else:
raise exception.InvalidParameterValue(
_("Invalid node architecture of '%s'.") % (cpu_arch))
return states.DEPLOYWAIT
"""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.
"""
"""Prepare the deployment environment for this node.
1. Ensure Node's AI Service is specified and it exists
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.
"""
# Ensure cache dir exists
# archive_uri, publishers or fmri are ignored if a ai_manifest is
# defined. They should be contained within the custom manifest itself
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
# Extract UUID from archive UAR and instantiate AIService
elif ai_service:
else:
# IPS Install, ensure default architecture service exists
if cpu_arch == "x86":
ai_service = "default-i386"
elif cpu_arch == 'SPARC':
ai_service = "default-sparc"
else:
raise exception.InvalidParameterValue(
_("Invalid node architecture of '%s'.") % (cpu_arch))
# Check if AI Service exists, raise exception of not
if archive_uri:
# Create this service
else:
raise exception.InvalidParameterValue(
# 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
# Ensure node fmri is valid if specified
if fmri:
# Validate fmri, format, etc
# Ensure node sc_profiles is of valid format if specified
if sc_profiles:
# Validate sc_profiles for format, etc
# Ensure node install_profiles is of valid format if specified
if install_profiles:
# Validate install_profiles for format, etc
# Ensure node ai_manifest is valid if specified
if ai_manifest:
# Validate ai_manifest for format, etc
# Check if AI Client exists for this service and if so remove it
# Client exists remove it
# Recreate new ai client for this mac address
# Node. Manifest name will be MAC address stripped of colons
# Check if AI Manifest exists for this service and if so remove it
# Manifest exists remove it
# (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.
else:
# Node, adding a new profile for each SC Profile specified.
# Profile Name will be MAC address prefix and counter suffix.
# e.g. AAEEBBCCFF66-1
# Remove all profiles associated with this MAC address and service
# Profile name starts with MAC address, assuming ironic
# created this profile so remove it.
if profile_prefix in 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:
else:
ins_list = []
if sc_profiles is not None:
else:
sc_list = []
else:
profile_index = 0
profile_index += 1
# Fetch profile locally, copy to AI Server so that
# installadm create-profile CLI works.
# Ensure local copy of archive_uri is removed if not needed
if archive_uri:
elif PLATFORM != "SunOS":
"""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.
:param task: a TaskManager instance.
"""
if archive_uri:
elif ai_service:
else:
if cpu_arch == "x86":
ai_service = "default-i386"
elif cpu_arch == 'SPARC':
ai_service = "default-sparc"
else:
raise exception.InvalidParameterValue(
_("Invalid node architecture of '%s'.") % (cpu_arch))
# Check if AI Service exists, log message if already removed
# There is nothing to clean up as service removed
else:
# 1. Delete AI Client for this MAC Address
# 2. Delete AI Manifest for this MAC Address
# 3. Remove AI Profiles for this MAC Address
# Remove all profiles associated with this MAC address
if profile_prefix in profile_name:
# Ensure local copy of archive_uri is removed if not needed
if archive_uri:
elif PLATFORM != "SunOS":
"""Take over management of this task's node from a dead conductor."""
""" TODO(mattk): Determine if this is required"""
"""Management class for solaris nodes."""
"""Return Solaris driver properties"""
return COMMON_PROPERTIES
try:
'dual_bridge'])
except OSError:
raise exception.DriverLoadError(
reason=_("Unable to locate usable ipmitool command in "
"the system path when checking ipmitool version"))
"""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.
"""
"""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:
else:
# Get architecture of node and return supported boot devices
if cpu_arch == 'x86':
elif cpu_arch == 'SPARC':
else:
raise exception.InvalidParameterValue(
_("Invalid node architecture of '%s'.") % (cpu_arch))
"""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.
"""
# Reset persistent to False, in case of BMC does not support
# persistent or we do not have admin rights.
if cpu_arch == 'x86':
raise exception.InvalidParameterValue(_(
"Invalid boot device %s specified.") % device)
if persistent:
elif cpu_arch == 'SPARC':
# Set bootmode script to network DHCP or disk
script_str = 'boot net:dhcp - install'
if archive_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' % \
if publishers:
if fmri:
# bootmode script property has a size restriction of 255
# characters raise error if this is breached.
raise exception.InvalidParameterValue(_(
"SPARC firmware bootmode script length exceeds 255:"
" %s") % script_str)
else:
raise exception.InvalidParameterValue(_(
"Invalid boot device %s specified.") % (device))
else:
raise exception.InvalidParameterValue(
_("Invalid node architecture of '%s'.") % (cpu_arch))
try:
except (exception.PasswordFileFailedToCreate,
'when executing "ipmitool %(cmd)s". '
'Error: %(error)s'),
"""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.
"""
return {
'persistent': True
}
if cpu_arch == 'x86':
elif cpu_arch == 'SPARC':
else:
raise exception.InvalidParameterValue(
_("Invalid node architecture of '%s'.") % (cpu_arch))
try:
except (exception.PasswordFileFailedToCreate,
'when executing "ipmitool %(cmd)s". '
'Error: %(error)s'),
if cpu_arch == 'x86':
if re_obj:
if 'PXE' in boot_selector:
elif 'Hard-Drive' in boot_selector:
if 'Safe-Mode' in boot_selector:
else:
elif 'BIOS' in boot_selector:
elif 'CD/DVD' in boot_selector:
elif cpu_arch == 'SPARC':
if "net:dhcp" in out:
else:
return response
"""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.
"""
# with '-v' option, we can get the entire sensor data including the
# extended sensor informations
try:
except (exception.PasswordFileFailedToCreate,
"""Inspect class for solaris nodes."""
"""Return Solaris driver properties"""
return COMMON_PROPERTIES
"""Validate driver_info containts IPMI credentials.
Ensure 'driver_info' containers the required IPMI
properties used to access a nodes properties.
:param task: a TaskManager instance
:raises: InvalidParameterValue if required IPMI parameters
are missing.
:raises: MissingParameterValue if a required parameter is missing.
"""
"""Inspect hardware to get the hardware properties.
Inspects hardware to get the defined IPMI_PROPERTIES.
Failure occures if any of the defined IPMI_PROPERTIES are not
determinable from the node.
:param task: a TaskManager instance
:raises: HardwareInspectionFailure if properties could
not be retrieved successfully.
:returns: the resulting state of inspection.
"""
# Installed memory size is returned in GB, Nova assumes this is MB
# so convert if returned in GB
try:
except ValueError:
# Size conversion failed, just ensure value is an int
del propdict['local_gb']
else:
# Local disk size can be returned with size type identifier
# remove identifier as this needs to be an int value
try:
except ValueError:
# Size conversion failed, just ensure value is an int
try:
except ValueError:
return states.MANAGEABLE
"""Retrieve IPMI_PROPERTIES from node
:param task: a TaskManager instance.
:returns: ipmitool retrieved property values
"""
try:
except (exception.PasswordFileFailedToCreate,
'when executing "ipmitool %(cmd)s". '
'Error: %(error)s'),
return out
"""Retrieve IPMI_PROPERTIES from node
:param task: a TaskManager instance.
:param cpus: CPU numbers to use.
:returns: ipmitool retrieved property values
"""
try:
except (exception.PasswordFileFailedToCreate,
'when executing "ipmitool %(cmd)s". '
'Error: %(error)s'),
return out
"""Create ironic port if not existing for this MAC
:param task: Node to creaate port for.
:param mac: MAC address to use for port.
"""
try:
except exception.MACAlreadyExists:
"for node %(node)s."),
class AIService():
"""AI Service"""
"""Initialize AIService object
:param task: a TaskManager instance
:param name: AI Service name
"""
self._image_path = None
self._derived_manifest = None
"""paramiko.SSHClient active connection"""
"""list() of manifest names for this service"""
if not self._manifests:
return self._manifests
"""list() of profile names for this service"""
"""list() of all client names(mac addresses) On AI Server"""
try:
return False
return False
else:
return True
"""image_path for this service"""
if self._image_path is None:
return self._image_path
"""Access default derived manifest URI"""
if not self._derived_manifest:
return self._derived_manifest
"""Create a new AI Service for this object
:param archive_uri: archive_uri to create service from
"""
if PLATFORM == "SunOS":
# 1. Fetch archive
else:
# 1. Fetch archive and Extract ISO file
# 2. scp AI ISO from archive to AI Server
try:
except:
if PLATFORM == "SunOS":
else:
raise
if PLATFORM != "SunOS":
# Remove temp extracted ISO file
# 3. Create a new AI Service
try:
self._manifests = []
if PLATFORM == "SunOS":
else:
raise AICreateServiceFail(
_("Failed to create AI Service %s") % (uuid))
# 4. Remove copy of AI ISO on AI Server
if PLATFORM == "SunOS":
# 5. Unmount UAR
# 6. Decrement reference count for image
if temp_uar is not None:
"""Delete the current AI Service"""
try:
raise AIDeleteServiceFail(
publishers, fmri):
"""Create a client associated with this service
:param mac: MAC Address of client to create
:param cpu_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
"""
# Add specific boot arguments for 'x86' clients only
if cpu_arch == 'x86':
ai_cmd += " -b install=true,console=ttya"
if archive_uri:
if auth_token:
if publishers:
if fmri:
try:
raise AICreateClientFail(_("Failed to create AI Client %s") %
(mac))
# If cpu_arch x86 customize grub reducing grub menu timeout to 0
if cpu_arch == 'x86':
" > %s" % (custom_grub)
try:
raise AICreateClientFail(
_("Failed to create custom grub menu for %s.") % (mac))
try:
raise AICreateClientFail(
_("Failed to customize AI Client %s grub menu.") % (mac))
"""Delete a specific client regardless of service association
:param mac: MAC Address of client to remove
:returns: Nothing exception raised if deletion fails
"""
try:
raise AIDeleteClientFail(_("Failed to delete AI Client %s") %
(mac))
# update list of clients for this service
"""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
"""
"'%s', manifest_path: '%s', mac: '%s'" %
" -c mac=" + mac
try:
raise AICreateManifestFail(_("Failed to create AI Manifest %s.") %
# Update list of manifests for this service
"""Delete a specific manifest
:param manifest_name: name of manifest to remove
:returns: Nothing exception raised if deletion fails
"""
try:
raise AIDeleteManifestFail(_("Failed to delete AI Manifest %s") %
# Update list of manifests for this service
"""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
"""
"'%s', profile_path: '%s', mac: '%s'" %
" -c mac=" + mac
if env is not None:
try:
raise AICreateProfileFail(_("Failed to create AI Profile %s.") %
(profile_name))
# Update list of profiles for this service
"""Delete a specific profile
:param profile_name: name of profile to remove
:returns: Nothing exception raised if deletion fails
"""
(profile_name))
try:
raise AIDeleteProfileFail(_("Failed to delete AI Profile %s") %
(profile_name))
# Update list of profiles for this service
"""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
"""
try:
"""Remove remote file in AI Server
:param path: Path of remote file to remove
:return: Nothing exception raised on failure
"""
(path))
try:
"""Retrieve image_path for this service
:returns: image_path property
"""
return image_path
"""Return service name and client from installadm list -e output
:param list_out: stdout from installadm list -e
:returns: Service Name and MAC Address
"""
service_name = None
client_name = None
(service_name))
return service_name, client_name
"""Given installadm list -n output, parse out service name
:param list_out: stdout from installadm list -n
:returns: Service Name
"""
service_name = None
(service_name))
return service_name
"""Returns an SSH client connected to a node.
:returns: paramiko.SSHClient, an active ssh connection.
"""
"""Generate SSH Dictionary for SSH Connection via paramiko
:returns: dictionary for paramiko connection
"""
raise exception.InvalidParameterValue(_(
"SSH server and username must be set."))
ssh_dict = {
}
raise exception.InvalidParameterValue(_(
"SSH requires one and only one of "
"ssh_key_file or ssh_key_contents to be set."))
if password:
if key_contents:
else:
raise exception.InvalidParameterValue(_(
"SSH key file %s not found.") % key_filename)
return ssh_dict
"""Get a list of manifest names for this service
:returns: list() of manifest names
"""
err_msg=_("Failed to retrieve manifests"
"""Get a list of profile names for this service
:returns: list() of profile names
"""
err_msg=_("Failed to retrieve profiles for "
"""Get a list of client names for this service
"""
err_msg=_("Failed to retrieve clients for "
# Store client names all in lower case
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
"""
(list_out))
names = []
return names
# Custom Exceptions
"""Exception type for AI Service creation failure"""
pass
"""Exception type for AI Service deletion failure"""
pass
"""Exception type for AI Client creation failure"""
pass
"""Exception type for AI Client deletion failure"""
pass
"""Exception type for AI Manifest creation failure"""
pass
"""Exception type for AI Manifest deletion failure"""
pass
"""Exception type for AI Profile creation failure"""
pass
"""Exception type for AI Profile deletion failure"""
pass
"""Generic Solaris IPMI driver exception"""