# Copyright 2015 FUJITSU LIMITED
#
# 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.
"""
Common functionalities shared between different iRMC modules.
"""
import os
from oslo_log import log as logging
from oslo_utils import importutils
from oslo_utils import strutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import utils
from ironic.conf import CONF
import ironic.drivers.modules.irmc.packaging_version as version
from ironic.drivers.modules import snmp
scci = importutils.try_import('scciclient.irmc.scci')
elcm = importutils.try_import('scciclient.irmc.elcm')
scci_mod = importutils.try_import('scciclient')
LOG = logging.getLogger(__name__)
# List of xxx_interface & implementation pair which uses SNMP internally
# and iRMC driver supports
INTERFACE_IMPL_LIST_WITH_SNMP = {
'inspect_interface': {'irmc', },
'power_interface': {'irmc', }}
REQUIRED_PROPERTIES = {
'irmc_address': _("IP address or hostname of the iRMC. Required."),
'irmc_username': _("Username for the iRMC with administrator privileges. "
"Required."),
'irmc_password': _("Password for irmc_username. Required."),
}
OPTIONAL_PROPERTIES = {
'irmc_port': _("Port to be used for iRMC operations; either 80 or 443. "
"The default value is 443. Optional."),
'irmc_auth_method': _("Authentication method for iRMC operations; "
"either 'basic' or 'digest'. The default value is "
"'basic'. Optional."),
'irmc_client_timeout': _("Timeout (in seconds) for iRMC operations. "
"The default value is 60. Optional."),
'irmc_sensor_method': _("Sensor data retrieval method; either "
"'ipmitool' or 'scci'. The default value is "
"'ipmitool'. Optional."),
}
OPTIONAL_DRIVER_INFO_PROPERTIES = {
'irmc_verify_ca': _('Either a Boolean value, a path to a CA_BUNDLE '
'file or directory with certificates of trusted '
'CAs. If set to True the driver will verify the '
'host certificates; if False the driver will '
'ignore verifying the SSL certificate. If it\'s '
'a path the driver will use the specified '
'certificate or one of the certificates in the '
'directory. Defaults to True. Optional'),
}
SNMP_PROPERTIES = {
'irmc_snmp_version': _("SNMP protocol version; either 'v1', 'v2c', or "
"'v3'. The default value is 'v2c'. Optional."),
'irmc_snmp_port': _("SNMP port. The default is 161. Optional."),
'irmc_snmp_community': _("SNMP community required for versions 'v1' and "
"'v2c'. The default value is 'public'. "
"Optional."),
'irmc_snmp_security': _("SNMP security name required for version 'v3'. "
"Optional."),
}
SNMP_V3_REQUIRED_PROPERTIES = {
'irmc_snmp_user': _("SNMPv3 User-based Security Model (USM) username. "
"Required for version 'v3’. "),
'irmc_snmp_auth_password': _("SNMPv3 message authentication key. Must be "
"8+ characters long. Required when message "
"authentication is used. Will be ignored if "
"the version of python-scciclient is before "
"0.10.1."),
'irmc_snmp_priv_password': _("SNMPv3 message privacy key. Must be 8+ "
"characters long. Required when message "
"privacy is used. Will be ignored if the "
"version of python-scciclient is before "
"0.10.1."),
}
SNMP_V3_OPTIONAL_PROPERTIES = {
'irmc_snmp_auth_proto': _("SNMPv3 message authentication protocol ID. "
"Required for version 'v3'. Will be ignored if "
"the version of python-scciclient is before "
"0.10.1. If using iRMC S4/S5, only 'sha' is "
"supported. If using iRMC S6, the valid "
"options are 'sha256', 'sha384', 'sha512'."),
'irmc_snmp_priv_proto': _("SNMPv3 message privacy (encryption) protocol "
"ID. Required for version 'v3'. Will be ignored "
"if the version of python-scciclient is before "
"0.10.1. 'aes' is supported."),
}
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
COMMON_PROPERTIES.update(OPTIONAL_DRIVER_INFO_PROPERTIES)
COMMON_PROPERTIES.update(SNMP_PROPERTIES)
COMMON_PROPERTIES.update(SNMP_V3_REQUIRED_PROPERTIES)
COMMON_PROPERTIES.update(SNMP_V3_OPTIONAL_PROPERTIES)
SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES = [
{'min': '0.8.2', 'max': '0.9.0'},
{'min': '0.9.5', 'max': '0.10.0'},
{'min': '0.10.1', 'max': '0.11.0'},
{'min': '0.11.3', 'max': '0.12.0'},
{'min': '0.12.0', 'max': '0.13.0'}]
SCCI_SNMPv3_AUTHENTICATION_SUPPORT_VERSION_RANGES = [
{'min': '0.10.1', 'max': '0.11.0'},
{'min': '0.11.3', 'max': '0.12.0'},
{'min': '0.12.2', 'max': '0.13.0'}]
def _scci_version_in(version_ranges):
scciclient_version = version.parse(scci_mod.__version__)
for rangev in version_ranges:
if (version.parse(rangev['min']) <= scciclient_version
< version.parse(rangev['max'])):
return True
return False
[docs]def parse_driver_info(node):
"""Gets the specific Node driver info.
This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver.
:param node: An ironic node object.
:returns: A dict containing information from driver_info
and default values.
:raises: InvalidParameterValue if invalid value is contained
in the 'driver_info' property.
:raises: MissingParameterValue if some mandatory key is missing
in the 'driver_info' property.
"""
info = node.driver_info
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
if missing_info:
raise exception.MissingParameterValue(_(
"Missing the following iRMC parameters in node's"
" driver_info: %s.") % missing_info)
req = {key: value for key, value in info.items()
if key in REQUIRED_PROPERTIES}
# corresponding config names don't have 'irmc_' prefix
opt = {param: info.get(param, CONF.irmc.get(param[len('irmc_'):]))
for param in OPTIONAL_PROPERTIES}
opt_driver_info = {param: info.get(param)
for param in OPTIONAL_DRIVER_INFO_PROPERTIES}
d_info = dict(req, **opt, **opt_driver_info)
d_info['irmc_port'] = utils.validate_network_port(
d_info['irmc_port'], 'irmc_port')
error_msgs = []
if (d_info['irmc_auth_method'].lower() not in ('basic', 'digest')):
error_msgs.append(
_("Value '%s' is not supported for 'irmc_auth_method'.") %
d_info['irmc_auth_method'])
if d_info['irmc_port'] not in (80, 443):
error_msgs.append(
_("Value '%s' is not supported for 'irmc_port'.") %
d_info['irmc_port'])
if not isinstance(d_info['irmc_client_timeout'], int):
error_msgs.append(
_("Value '%s' is not an integer for 'irmc_client_timeout'") %
d_info['irmc_client_timeout'])
if d_info['irmc_sensor_method'].lower() not in ('ipmitool', 'scci'):
error_msgs.append(
_("Value '%s' is not supported for 'irmc_sensor_method'.") %
d_info['irmc_sensor_method'])
verify_ca = d_info.get('irmc_verify_ca')
if verify_ca is None:
d_info['irmc_verify_ca'] = verify_ca = CONF.webserver_verify_ca
# Check if verify_ca is a Boolean or a file/directory in the file-system
if isinstance(verify_ca, str):
if ((os.path.isdir(verify_ca) and os.path.isabs(verify_ca))
or (os.path.isfile(verify_ca) and os.path.isabs(verify_ca))):
# If it's fullpath and dir/file, we don't need to do anything
pass
else:
try:
d_info['irmc_verify_ca'] = strutils.bool_from_string(
verify_ca, strict=True)
except ValueError:
error_msgs.append(
_('Invalid value type set in driver_info/'
'irmc_verify_ca on node %(node)s. '
'The value should be a Boolean or the path '
'to a file/directory, not "%(value)s"'
) % {'value': verify_ca, 'node': node.uuid})
elif isinstance(verify_ca, bool):
# If it's a boolean it's grand, we don't need to do anything
pass
else:
error_msgs.append(
_('Invalid value type set in driver_info/irmc_verify_ca '
'on node %(node)s. The value should be a Boolean or the path '
'to a file/directory, not "%(value)s"') % {'value': verify_ca,
'node': node.uuid})
if error_msgs:
msg = (_("The following errors were encountered while parsing "
"driver_info:\n%s") % "\n".join(error_msgs))
raise exception.InvalidParameterValue(msg)
d_info.update(_parse_snmp_driver_info(node, info))
return d_info
def _parse_snmp_driver_info(node, info):
"""Parses the SNMP related driver_info parameters.
:param node: An Ironic node object.
:param info: driver_info dictionary.
:returns: A dictionary containing SNMP information.
:raises: MissingParameterValue if any of the mandatory
parameter values are not provided.
:raises: InvalidParameterValue if there is any invalid
value provided.
"""
snmp_info = {param: info.get(param, CONF.irmc.get(param[len('irmc_'):]))
for param in SNMP_PROPERTIES}
valid_versions = {"v1": snmp.SNMP_V1,
"v2c": snmp.SNMP_V2C,
"v3": snmp.SNMP_V3}
for int_name, impl_list in INTERFACE_IMPL_LIST_WITH_SNMP.items():
if getattr(node, int_name) in impl_list:
break
else:
return snmp_info
if snmp_info['irmc_snmp_version'].lower() not in valid_versions:
raise exception.InvalidParameterValue(_(
"Value '%s' is not supported for 'irmc_snmp_version'.") %
snmp_info['irmc_snmp_version']
)
snmp_info["irmc_snmp_version"] = \
valid_versions[snmp_info["irmc_snmp_version"].lower()]
snmp_info['irmc_snmp_port'] = utils.validate_network_port(
snmp_info['irmc_snmp_port'], 'irmc_snmp_port')
if snmp_info['irmc_snmp_version'] != snmp.SNMP_V3:
if (snmp_info['irmc_snmp_community']
and not isinstance(snmp_info['irmc_snmp_community'], str)):
raise exception.InvalidParameterValue(_(
"Value '%s' is not a string for 'irmc_snmp_community'") %
snmp_info['irmc_snmp_community'])
if utils.is_fips_enabled():
raise exception.InvalidParameterValue(_(
"'v3' has to be set for 'irmc_snmp_version' "
"when FIPS mode is enabled."))
else:
# Parse snmp user info
if 'irmc_snmp_user' in info:
if not isinstance(info['irmc_snmp_user'], str):
raise exception.InvalidParameterValue(_(
"Value %s is not a string for 'irmc_snmp_user'.") %
info['irmc_snmp_user'])
snmp_info['irmc_snmp_user'] = info['irmc_snmp_user']
if snmp_info['irmc_snmp_security']:
LOG.warning(_("'irmc_snmp_security' is ignored in favor of "
"'irmc_snmp_user'. Please remove "
"'irmc_snmp_security' from node %s "
"configuration."), node.uuid)
else:
if not snmp_info['irmc_snmp_security']:
raise exception.MissingParameterValue(_(
"'irmc_snmp_user' should be set when using SNMPv3."))
if not isinstance(snmp_info['irmc_snmp_security'], str):
raise exception.InvalidParameterValue(_(
"Value %s is not a string for 'irmc_snmp_security'.") %
snmp_info['irmc_snmp_security'])
snmp_info['irmc_snmp_user'] = snmp_info['irmc_snmp_security']
if _scci_version_in(SCCI_SNMPv3_AUTHENTICATION_SUPPORT_VERSION_RANGES):
snmp_info.update(_parse_snmp_v3_crypto_info(info))
else:
# For compatible with old version of python-scciclient
snmp_info['irmc_snmp_security'] = snmp_info['irmc_snmp_user']
if 'irmc_snmp_auth_password' in info or \
'irmc_snmp_priv_password' in info:
LOG.warning(_("'irmc_snmp_auth_password' and "
"'irmc_snmp_priv_password' in node %(node)s "
"configuration are ignored. "
"Python-scciclient version %(version)s can only "
"communicate with iRMC with no authentication "
"protocol setted. This means the authentication "
"protocol, private protocol and password of the "
"server's SNMPv3 user should all be blank, "
"otherwise python-scciclient will encounter an "
"authentication error. If you want to set "
"password, please update python-scciclient to "
"a newer version (>=0.10.1, <0.11.0)."),
{'node': node.uuid,
'version': scci_mod.__version__})
return snmp_info
def _parse_snmp_v3_crypto_info(info):
snmp_info = {}
valid_values = {'irmc_snmp_auth_proto': ['sha', 'sha256', 'sha384',
'sha512'],
'irmc_snmp_priv_proto': ['aes']}
valid_protocols = {'irmc_snmp_auth_proto': snmp.snmp_auth_protocols,
'irmc_snmp_priv_proto': snmp.snmp_priv_protocols}
snmp_keys = {'irmc_snmp_auth_password', 'irmc_snmp_priv_password'}
for param in snmp_keys:
try:
snmp_info[param] = info[param]
except KeyError:
raise exception.MissingParameterValue(_(
"%s should be set when using SNMPv3.") % param)
if not isinstance(snmp_info[param], str):
raise exception.InvalidParameterValue(_(
"The value of %s is not a string.") % param)
if len(snmp_info[param]) < 8:
raise exception.InvalidParameterValue(_(
"%s is too short. (8+ chars required)") % param)
for param in SNMP_V3_OPTIONAL_PROPERTIES:
value = None
try:
value = info[param]
if value not in valid_values[param]:
raise exception.InvalidParameterValue(_(
"Invalid value %(value)s given for driver info parameter "
"%(param)s, the valid values are %(valid_values)s.") %
{'param': param,
'value': value,
'valid_values': valid_values[param]})
except KeyError:
value = CONF.irmc.get(param[len('irmc_'):])
snmp_info[param] = valid_protocols[param].get(value)
if not snmp_info[param]:
raise exception.InvalidParameterValue(_(
"Unknown SNMPv3 protocol %(value)s given for "
"driver info parameter %(param)s") % {'param': param,
'value': value})
return snmp_info
[docs]def get_irmc_client(node):
"""Gets an iRMC SCCI client.
Given an ironic node object, this method gives back a iRMC SCCI client
to do operations on the iRMC.
:param node: An ironic node object.
:returns: scci_cmd partial function which takes a SCCI command param.
:raises: InvalidParameterValue on invalid inputs.
:raises: MissingParameterValue if some mandatory information
is missing on the node
:raises: IRMCOperationError if iRMC operation failed
"""
driver_info = parse_driver_info(node)
if _scci_version_in(SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES):
scci_client = scci.get_client(
driver_info['irmc_address'],
driver_info['irmc_username'],
driver_info['irmc_password'],
port=driver_info['irmc_port'],
auth_method=driver_info['irmc_auth_method'],
verify=driver_info.get('irmc_verify_ca'),
client_timeout=driver_info['irmc_client_timeout'])
else:
if driver_info['irmc_port'] == 443:
LOG.warning("Installed version of python-scciclient doesn't "
"support certification on HTTPS connection.")
scci_client = scci.get_client(
driver_info['irmc_address'],
driver_info['irmc_username'],
driver_info['irmc_password'],
port=driver_info['irmc_port'],
auth_method=driver_info['irmc_auth_method'],
client_timeout=driver_info['irmc_client_timeout'])
return scci_client
[docs]def update_ipmi_properties(task):
"""Update ipmi properties to node driver_info.
:param task: A task from TaskManager.
"""
node = task.node
info = node.driver_info
# updating ipmi credentials
info['ipmi_address'] = info.get('irmc_address')
info['ipmi_username'] = info.get('irmc_username')
info['ipmi_password'] = info.get('irmc_password')
# saving ipmi credentials to task object
task.node.driver_info = info
[docs]def get_irmc_report(node):
"""Gets iRMC SCCI report.
Given an ironic node object, this method gives back a iRMC SCCI report.
:param node: An ironic node object.
:returns: A xml.etree.ElementTree object.
:raises: InvalidParameterValue on invalid inputs.
:raises: MissingParameterValue if some mandatory information
is missing on the node.
:raises: scci.SCCIInvalidInputError if required parameters are invalid.
:raises: scci.SCCIClientError if SCCI failed.
"""
driver_info = parse_driver_info(node)
if _scci_version_in(SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES):
report = scci.get_report(
driver_info['irmc_address'],
driver_info['irmc_username'],
driver_info['irmc_password'],
port=driver_info['irmc_port'],
auth_method=driver_info['irmc_auth_method'],
verify=driver_info.get('irmc_verify_ca'),
client_timeout=driver_info['irmc_client_timeout'])
else:
if driver_info['irmc_port'] == 443:
LOG.warning("Installed version of python-scciclient doesn't "
"support certification on HTTPS connection.")
report = scci.get_report(
driver_info['irmc_address'],
driver_info['irmc_username'],
driver_info['irmc_password'],
port=driver_info['irmc_port'],
auth_method=driver_info['irmc_auth_method'],
client_timeout=driver_info['irmc_client_timeout'])
return report
[docs]def get_secure_boot_mode(node):
"""Get the current secure boot mode.
:param node: An ironic node object.
:raises: UnsupportedDriverExtension if secure boot is not present.
:raises: IRMCOperationError if the operation fails.
"""
driver_info = parse_driver_info(node)
try:
return elcm.get_secure_boot_mode(driver_info)
except elcm.SecureBootConfigNotFound:
raise exception.UnsupportedDriverExtension(
driver=node.driver, extension='get_secure_boot_state')
except scci.SCCIError as irmc_exception:
LOG.error("Failed to get secure boot for node %s", node.uuid)
raise exception.IRMCOperationError(
operation=_("getting secure boot mode"),
error=irmc_exception)
[docs]def set_secure_boot_mode(node, enable):
"""Enable or disable UEFI Secure Boot
:param node: An ironic node object.
:param enable: Boolean value. True if the secure boot to be
enabled.
:raises: IRMCOperationError if the operation fails.
"""
driver_info = parse_driver_info(node)
try:
elcm.set_secure_boot_mode(driver_info, enable)
LOG.info("Set secure boot to %(flag)s for node %(node)s",
{'flag': enable, 'node': node.uuid})
except scci.SCCIError as irmc_exception:
LOG.error("Failed to set secure boot to %(flag)s for node %(node)s",
{'flag': enable, 'node': node.uuid})
raise exception.IRMCOperationError(
operation=_("setting secure boot mode"),
error=irmc_exception)