OneView Driver for Ironic

This driver that will promote integration between Ironic and the HP OneView
Management System. The driver will allow Ironic to borrow non-dedicated servers
from OneViews's inventory to provision baremetal instances with minimal common
pre-configuration, set through OneView's *Server Profile Templates*.

Co-Authored-By: Alberto Barros <albertoffb@lsd.ufcg.edu.br>
Co-Authored-By: André Aranha <afaranha@lsd.ufcg.edu.br>
Co-Authored-By: Caio Oliveira <caiobo@lsd.ufcg.edu.br>
Co-Authored-By: Diego Pereira <diegolp@lsd.ufcg.edu.br>
Co-Authored-By: Gabriel Bezerra <gabrielb@lsd.ufcg.edu.br>
Co-Authored-By: Lilia Sampaio <liliars@lsd.ufcg.edu.br>
Co-Authored-By: Sinval Vieira <sinval@lsd.ufcg.edu.br>

Change-Id: Ic2fb7860e6b4f5183b6525ff7709c7616350a96a
Implements: blueprint new-ironic-driver-for-oneview
Depends-on: I914596e592477e148e642f93cfbe114464c3fe38
This commit is contained in:
Thiago Paiva 2015-06-15 11:09:32 -03:00
parent cee8b22a11
commit 460b9f1f5f
20 changed files with 1771 additions and 0 deletions

View File

@ -8,6 +8,7 @@ proliantutils>=2.1.5
pyghmi>=0.8.0
pysnmp
python-ironic-inspector-client
python-oneviewclient>=2.0.0
python-scciclient>=0.2.0
python-seamicroclient>=0.4.0
UcsSdk==0.8.2.2

View File

@ -1413,6 +1413,33 @@
#cleaning_network_uuid=<None>
[oneview]
#
# Options defined in ironic.drivers.modules.oneview.common
#
# URL where OneView is available (string value)
#manager_url=<None>
# OneView username to be used (string value)
#username=<None>
# OneView password to be used (string value)
#password=<None>
# Option to allow insecure connection with OneView (boolean
# value)
#allow_insecure_connections=false
# Path to CA certificate (string value)
#tls_cacert_file=<None>
# Max connection retries to check changes on OneView (integer
# value)
#max_polling_attempts=20
[oslo_concurrency]
#

View File

@ -605,3 +605,7 @@ class ImageUploadFailed(IronicException):
class CIMCException(IronicException):
_msg_fmt = _("Cisco IMC exception occurred for node %(node)s: %(error)s")
class OneViewError(IronicException):
_msg_fmt = _("OneView exception occurred. Error: %(error)s")

View File

@ -0,0 +1,259 @@
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common import states
from ironic.drivers import utils
LOG = logging.getLogger(__name__)
client = importutils.try_import('oneview_client.client')
oneview_states = importutils.try_import('oneview_client.states')
oneview_exceptions = importutils.try_import('oneview_client.exceptions')
opts = [
cfg.StrOpt('manager_url',
help=_('URL where OneView is available')),
cfg.StrOpt('username',
help=_('OneView username to be used')),
cfg.StrOpt('password',
secret=True,
help=_('OneView password to be used')),
cfg.BoolOpt('allow_insecure_connections',
default=False,
help=_('Option to allow insecure connection with OneView')),
cfg.StrOpt('tls_cacert_file',
default=None,
help=_('Path to CA certificate')),
cfg.IntOpt('max_polling_attempts',
default=12,
help=_('Max connection retries to check changes on OneView')),
]
CONF = cfg.CONF
CONF.register_opts(opts, group='oneview')
REQUIRED_ON_DRIVER_INFO = {
'server_hardware_uri': _("Server Hardware URI. Required."),
'server_profile_template_uri': _("Server Profile Template URI to clone "
"from. Required."),
}
REQUIRED_ON_PROPERTIES = {
'server_hardware_type_uri': _("Server Hardware Type URI. Required."),
}
OPTIONAL_ON_PROPERTIES = {
'enclosure_group_uri': _("Enclosure Group URI.")
}
COMMON_PROPERTIES = {}
COMMON_PROPERTIES.update(REQUIRED_ON_DRIVER_INFO)
COMMON_PROPERTIES.update(REQUIRED_ON_PROPERTIES)
COMMON_PROPERTIES.update(OPTIONAL_ON_PROPERTIES)
def get_oneview_client():
"""Generates an instance of the OneView client.
Generates an instance of the OneView client using the imported
oneview_client library.
:returns: an instance of the OneView client
"""
oneview_client = client.Client(
manager_url=CONF.oneview.manager_url,
username=CONF.oneview.username,
password=CONF.oneview.password,
allow_insecure_connections=CONF.oneview.allow_insecure_connections,
tls_cacert_file=CONF.oneview.tls_cacert_file,
max_polling_attempts=CONF.oneview.max_polling_attempts
)
return oneview_client
def verify_node_info(node):
"""Verifies if fields and namespaces of a node are valid.
Verifies if the 'driver_info' field and the 'properties/capabilities'
namespace exist and are not empty.
:param: node: node object to be verified
:raises: InvalidParameterValue if required node capabilities and/or
driver_info are malformed or missing
:raises: MissingParameterValue if required node capabilities and/or
driver_info are missing
"""
capabilities_dict = utils.capabilities_to_dict(
node.properties.get('capabilities', '')
)
driver_info = node.driver_info
_verify_node_info('properties/capabilities', capabilities_dict,
REQUIRED_ON_PROPERTIES)
_verify_node_info('driver_info', driver_info,
REQUIRED_ON_DRIVER_INFO)
def get_oneview_info(node):
"""Gets OneView information from the node.
:param: node: node object to get information from
:returns: a dictionary containing:
:server_hardware_uri: the uri of the server hardware in OneView
:server_hardware_type_uri: the uri of the server hardware type in
OneView
:enclosure_group_uri: the uri of the enclosure group in OneView
:server_profile_template_uri: the uri of the server profile template in
OneView
:raises InvalidParameterValue if node capabilities are malformed
"""
capabilities_dict = utils.capabilities_to_dict(
node.properties.get('capabilities', '')
)
driver_info = node.driver_info
oneview_info = {
'server_hardware_uri':
driver_info.get('server_hardware_uri'),
'server_hardware_type_uri':
capabilities_dict.get('server_hardware_type_uri'),
'enclosure_group_uri':
capabilities_dict.get('enclosure_group_uri'),
'server_profile_template_uri':
driver_info.get('server_profile_template_uri'),
}
return oneview_info
def validate_oneview_resources_compatibility(task):
"""Validates if the node configuration is consistent with OneView.
This method calls python-oneviewclient functions to validate if the node
configuration is consistent with the OneView resources it represents,
including server_hardware_uri, server_hardware_type_uri,
server_profile_template_uri, enclosure_group_uri and node ports. Also
verifies if a Server Profile is applied to the Server Hardware the node
represents. If any validation fails, python-oneviewclient will raise
an appropriate OneViewException.
:param: task: a TaskManager instance containing the node to act on.
"""
node = task.node
node_ports = task.ports
try:
oneview_client = get_oneview_client()
oneview_info = get_oneview_info(node)
oneview_client.validate_node_server_hardware(
oneview_info, node.properties.get('memory_mb'),
node.properties.get('cpus')
)
oneview_client.validate_node_server_hardware_type(oneview_info)
oneview_client.check_server_profile_is_applied(oneview_info)
oneview_client.is_node_port_mac_compatible_with_server_profile(
oneview_info, node_ports
)
oneview_client.validate_node_enclosure_group(oneview_info)
oneview_client.validate_node_server_profile_template(oneview_info)
except oneview_exceptions.OneViewException as oneview_exc:
msg = (_("Error validating node resources with OneView: %s")
% oneview_exc)
LOG.error(msg)
raise exception.OneViewError(error=msg)
def translate_oneview_power_state(power_state):
"""Translates OneView's power states strings to Ironic's format.
:param: power_state: power state string to be translated
:returns: the power state translated
"""
power_states_map = {
oneview_states.ONEVIEW_POWER_ON: states.POWER_ON,
oneview_states.ONEVIEW_POWERING_OFF: states.POWER_ON,
oneview_states.ONEVIEW_POWER_OFF: states.POWER_OFF,
oneview_states.ONEVIEW_POWERING_ON: states.POWER_OFF,
oneview_states.ONEVIEW_RESETTING: states.REBOOT
}
return power_states_map.get(power_state, states.ERROR)
def _verify_node_info(node_namespace, node_info_dict, info_required):
"""Verify if info_required is present in node_namespace of the node info.
"""
missing_keys = set(info_required) - set(node_info_dict)
if missing_keys:
raise exception.MissingParameterValue(
_("Missing the keys for the following OneView data in node's "
"%(namespace)s: %(missing_keys)s.") %
{'namespace': node_namespace,
'missing_keys': ', '.join(missing_keys)
}
)
# False and 0 can still be considered as valid values
missing_values_keys = [k for k in info_required
if node_info_dict[k] in ('', None)]
if missing_values_keys:
missing_keys = ["%s:%s" % (node_namespace, k)
for k in missing_values_keys]
raise exception.MissingParameterValue(
_("Missing parameter value for: '%s'") % "', '".join(missing_keys)
)
def node_has_server_profile(func):
"""Checks if the node's Server Hardware as a Server Profile associated.
"""
def inner(*args, **kwargs):
task = args[1]
oneview_info = get_oneview_info(task.node)
oneview_client = get_oneview_client()
try:
node_has_server_profile = (
oneview_client.get_server_profile_from_hardware(oneview_info)
)
except oneview_exceptions.OneViewException as oneview_exc:
LOG.error(
_LE("Failed to get server profile from OneView appliance for"
"node %(node)s. Error: %(message)s"),
{"node": task.node.uuid, "message": oneview_exc}
)
raise exception.OneViewError(error=oneview_exc)
if not node_has_server_profile:
raise exception.OperationNotPermitted(
_("A Server Profile is not associated with node %s.") %
task.node.uuid
)
return func(*args, **kwargs)
return inner

View File

@ -0,0 +1,172 @@
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
from oslo_log import log as logging
from oslo_utils import importutils
from ironic.common import boot_devices
from ironic.common import exception
from ironic.common.i18n import _
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules.oneview import common
LOG = logging.getLogger(__name__)
BOOT_DEVICE_MAPPING_TO_OV = {
boot_devices.DISK: 'HardDisk',
boot_devices.PXE: 'PXE',
boot_devices.CDROM: 'CD',
}
BOOT_DEVICE_OV_TO_GENERIC = {
v: k
for k, v in BOOT_DEVICE_MAPPING_TO_OV.items()
}
oneview_exceptions = importutils.try_import('oneview_client.exceptions')
class OneViewManagement(base.ManagementInterface):
def get_properties(self):
return common.COMMON_PROPERTIES
def validate(self, task):
"""Checks required info on 'driver_info' and validates node with OneView
Validates whether the 'driver_info' property of the supplied
task's node contains the required info such as server_hardware_uri,
server_hardware_type, server_profile_template_uri and
enclosure_group_uri. Also, checks if the server profile of the node is
applied, if NICs are valid for the server profile of the node, and if
the server hardware attributes (ram, memory, vcpus count) are
consistent with OneView.
:param task: a task from TaskManager.
:raises: InvalidParameterValue if parameters set are inconsistent with
resources in OneView
"""
common.verify_node_info(task.node)
try:
common.validate_oneview_resources_compatibility(task)
except exception.OneViewError as oneview_exc:
raise exception.InvalidParameterValue(oneview_exc)
def get_supported_boot_devices(self, task):
"""Gets 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`.
"""
return sorted(BOOT_DEVICE_MAPPING_TO_OV.keys())
@task_manager.require_exclusive_lock
@common.node_has_server_profile
def set_boot_device(self, task, device, persistent=False):
"""Sets the boot device for a node.
Sets the boot device to use on next reboot of the node.
:param task: a task from TaskManager.
:param device: the boot device, one of the supported devices
listed in :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: OperationNotPermitted if the server has no server profile or
if the server is already powered on.
:raises: OneViewError if the communication with OneView fails
"""
oneview_info = common.get_oneview_info(task.node)
if device not in self.get_supported_boot_devices(task):
raise exception.InvalidParameterValue(
_("Invalid boot device %s specified.") % device)
LOG.debug("Setting boot device to %(device)s for node %(node)s",
{"device": device, "node": task.node.uuid})
try:
oneview_client = common.get_oneview_client()
device_to_oneview = BOOT_DEVICE_MAPPING_TO_OV.get(device)
oneview_client.set_boot_device(oneview_info, device_to_oneview)
except oneview_exceptions.OneViewException as oneview_exc:
msg = (_(
"Error setting boot device on OneView. Error: %s")
% oneview_exc
)
LOG.error(msg)
raise exception.OneViewError(error=msg)
@common.node_has_server_profile
def get_boot_device(self, task):
"""Get the current boot device for the task's node.
Provides the current boot device of the node.
:param task: a task from TaskManager.
:returns: a dictionary containing:
:boot_device: the boot device, one of
:mod:`ironic.common.boot_devices` [PXE, DISK, CDROM]
:persistent: Whether the boot device will persist to all
future boots or not, None if it is unknown.
:raises: OperationNotPermitted if no Server Profile is associated with
the node
:raises: InvalidParameterValue if the boot device is unknown
:raises: OneViewError if the communication with OneView fails
"""
oneview_info = common.get_oneview_info(task.node)
try:
oneview_client = common.get_oneview_client()
boot_order = oneview_client.get_boot_order(oneview_info)
except oneview_exceptions.OneViewException as oneview_exc:
msg = (_(
"Error getting boot device from OneView. Error: %s")
% oneview_exc
)
LOG.error(msg)
raise exception.OneViewError(msg)
primary_device = boot_order[0]
if primary_device not in BOOT_DEVICE_OV_TO_GENERIC:
raise exception.InvalidParameterValue(
_("Unsupported boot Device %(device)s for Node: %(node)s")
% {"device": primary_device, "node": task.node.uuid}
)
boot_device = {
'boot_device': BOOT_DEVICE_OV_TO_GENERIC.get(primary_device),
'persistent': True,
}
return boot_device
def get_sensors_data(self, task):
"""Get sensors data.
Not implemented by this driver.
:param task: a TaskManager instance.
"""
raise NotImplementedError()

View File

@ -0,0 +1,133 @@
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
from oslo_log import log as logging
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules.oneview import common
LOG = logging.getLogger(__name__)
oneview_exceptions = importutils.try_import('oneview_client.exceptions')
class OneViewPower(base.PowerInterface):
def get_properties(self):
return common.COMMON_PROPERTIES
def validate(self, task):
"""Checks required info on 'driver_info' and validates node with OneView
Validates whether the 'oneview_info' property of the supplied
task's node contains the required info such as server_hardware_uri,
server_hardware_type, server_profile_template_uri and
enclosure_group_uri. Also, checks if the server profile of the node is
applied, if NICs are valid for the server profile of the node, and if
the server hardware attributes (ram, memory, vcpus count) are
consistent with OneView.
:param task: a task from TaskManager.
:raises: MissingParameterValue if a required parameter is missing.
:raises: InvalidParameterValue if parameters set are inconsistent with
resources in OneView
"""
common.verify_node_info(task.node)
try:
common.validate_oneview_resources_compatibility(task)
except exception.OneViewError as oneview_exc:
raise exception.InvalidParameterValue(oneview_exc)
def get_power_state(self, task):
"""Gets the current power state.
:param task: a TaskManager instance.
:param node: The Node.
:returns: one of :mod:`ironic.common.states` POWER_OFF,
POWER_ON or ERROR.
:raises: OneViewError if fails to retrieve power state of OneView
resource
"""
oneview_info = common.get_oneview_info(task.node)
oneview_client = common.get_oneview_client()
try:
power_state = oneview_client.get_node_power_state(oneview_info)
except oneview_exceptions.OneViewException as oneview_exc:
LOG.error(
_LE("Error getting power state for node %(node)s. Error:"
"%(error)s"),
{'node': task.node.uuid, 'error': oneview_exc}
)
raise exception.OneViewError(error=oneview_exc)
return common.translate_oneview_power_state(power_state)
@task_manager.require_exclusive_lock
def set_power_state(self, task, power_state):
"""Turn the current power state on or off.
:param task: a TaskManager instance.
:param node: The Node.
:param power_state: The desired power state POWER_ON, POWER_OFF or
REBOOT from :mod:`ironic.common.states`.
:raises: InvalidParameterValue if an invalid power state was specified.
:raises: PowerStateFailure if the power couldn't be set to power_state.
:raises: OneViewError if OneView fails setting the power state.
"""
oneview_info = common.get_oneview_info(task.node)
oneview_client = common.get_oneview_client()
LOG.debug('Setting power state of node %(node_uuid)s to '
'%(power_state)s',
{'node_uuid': task.node.uuid, 'power_state': power_state})
try:
if power_state == states.POWER_ON:
oneview_client.power_on(oneview_info)
elif power_state == states.POWER_OFF:
oneview_client.power_off(oneview_info)
elif power_state == states.REBOOT:
oneview_client.power_off(oneview_info)
oneview_client.power_on(oneview_info)
else:
raise exception.InvalidParameterValue(
_("set_power_state called with invalid power state %s.")
% power_state)
except oneview_exceptions.OneViewException as exc:
raise exception.OneViewError(
_("Error setting power state: %s") % exc
)
@task_manager.require_exclusive_lock
def reboot(self, task):
"""Reboot the node
:param task: a TaskManager instance.
:param node: The Node.
:raises: PowerStateFailure if the final state of the node is not
POWER_ON.
"""
self.set_power_state(task, states.REBOOT)

View File

@ -0,0 +1,113 @@
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
from oslo_log import log
import retrying
from ironic.common.i18n import _
from ironic.common.i18n import _LI
from ironic.common.i18n import _LW
from ironic.common import states
from ironic.conductor import utils as manager_utils
from ironic.drivers.modules import agent
from ironic.drivers.modules import deploy_utils
LOG = log.getLogger(__name__)
CONF = agent.CONF
# NOTE (thiagop): We overwrite this interface because we cannot change the boot
# device of OneView managed blades while they are still powered on. We moved
# the call of node_set_boot_device from reboot_to_instance to
# reboot_and_finish_deploy and changed the behavior to shutdown the node before
# doing it.
# TODO(thiagop): remove this interface once bug/1503855 is fixed
class AgentVendorInterface(agent.AgentVendorInterface):
def reboot_to_instance(self, task, **kwargs):
task.process_event('resume')
node = task.node
error = self.check_deploy_success(node)
if error is not None:
# TODO(jimrollenhagen) power off if using neutron dhcp to
# align with pxe driver?
msg = (_('node %(node)s command status errored: %(error)s') %
{'node': node.uuid, 'error': error})
LOG.error(msg)
deploy_utils.set_failed_state(task, msg)
return
LOG.info(_LI('Image successfully written to node %s'), node.uuid)
LOG.debug('Rebooting node %s to instance', node.uuid)
self.reboot_and_finish_deploy(task)
# NOTE(TheJulia): If we deployed a whole disk image, we
# should expect a whole disk image and clean-up the tftp files
# on-disk incase the node is disregarding the boot preference.
# TODO(rameshg87): Not all in-tree drivers using reboot_to_instance
# have a boot interface. So include a check for now. Remove this
# check once all in-tree drivers have a boot interface.
if task.driver.boot:
task.driver.boot.clean_up_ramdisk(task)
def reboot_and_finish_deploy(self, task):
"""Helper method to trigger reboot on the node and finish deploy.
This method initiates a reboot on the node. On success, it
marks the deploy as complete. On failure, it logs the error
and marks deploy as failure.
:param task: a TaskManager object containing the node
:raises: InstanceDeployFailure, if node reboot failed.
"""
wait = CONF.agent.post_deploy_get_power_state_retry_interval * 1000
attempts = CONF.agent.post_deploy_get_power_state_retries + 1
@retrying.retry(
stop_max_attempt_number=attempts,
retry_on_result=lambda state: state != states.POWER_OFF,
wait_fixed=wait
)
def _wait_until_powered_off(task):
return task.driver.power.get_power_state(task)
node = task.node
try:
try:
self._client.power_off(node)
_wait_until_powered_off(task)
except Exception as e:
LOG.warning(
_LW('Failed to soft power off node %(node_uuid)s '
'in at least %(timeout)d seconds. Error: %(error)s'),
{'node_uuid': node.uuid,
'timeout': (wait * (attempts - 1)) / 1000,
'error': e})
manager_utils.node_power_action(task, states.POWER_OFF)
manager_utils.node_set_boot_device(task, 'disk',
persistent=True)
manager_utils.node_power_action(task, states.POWER_ON)
except Exception as e:
msg = (_('Error rebooting node %(node)s after deploy. '
'Error: %(error)s') %
{'node': node.uuid, 'error': e})
self._log_and_raise_deployment_error(task, msg)
task.process_event('done')
LOG.info(_LI('Deployment to node %s done'), task.node.uuid)

110
ironic/drivers/oneview.py Normal file
View File

@ -0,0 +1,110 @@
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
"""
OneView Driver and supporting meta-classes.
"""
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules import fake
from ironic.drivers.modules import iscsi_deploy
from ironic.drivers.modules.oneview import common
from ironic.drivers.modules.oneview import management
from ironic.drivers.modules.oneview import power
from ironic.drivers.modules.oneview import vendor
from ironic.drivers.modules import pxe
class AgentPXEOneViewDriver(base.BaseDriver):
"""Agent + OneView driver.
This driver implements the `core` functionality, combining
:class:`ironic.drivers.ov.OVPower` for power on/off and reboot of virtual
machines, with :class:`ironic.driver.pxe.PXEBoot` for booting deploy kernel
and ramdisk and :class:`ironic.driver.iscsi_deploy.ISCSIDeploy` for image
deployment. Implementations are in those respective classes; this class is
merely the glue between them.
"""
def __init__(self):
if not importutils.try_import('oneview_client.client'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_("Unable to import python-oneviewclient library"))
# Checks connectivity to OneView and version compatibility on driver
# initialization
oneview_client = common.get_oneview_client()
oneview_client.verify_oneview_version()
oneview_client.verify_credentials()
self.power = power.OneViewPower()
self.management = management.OneViewManagement()
self.boot = pxe.PXEBoot()
self.deploy = agent.AgentDeploy()
self.vendor = vendor.AgentVendorInterface()
class ISCSIPXEOneViewDriver(base.BaseDriver):
"""PXE + OneView driver.
This driver implements the `core` functionality, combining
:class:`ironic.drivers.ov.OVPower` for power on/off and reboot of virtual
machines, with :class:`ironic.driver.pxe.PXEBoot` for booting deploy kernel
and ramdisk and :class:`ironic.driver.iscsi_deploy.ISCSIDeploy` for image
deployment. Implementations are in those respective classes; this class is
merely the glue between them.
"""
def __init__(self):
if not importutils.try_import('oneview_client.client'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_("Unable to import python-oneviewclient library"))
# Checks connectivity to OneView and version compatibility on driver
# initialization
oneview_client = common.get_oneview_client()
oneview_client.verify_oneview_version()
oneview_client.verify_credentials()
self.power = power.OneViewPower()
self.management = management.OneViewManagement()
self.boot = pxe.PXEBoot()
self.deploy = iscsi_deploy.ISCSIDeploy()
self.vendor = iscsi_deploy.VendorPassthru()
class FakeOneViewDriver(base.BaseDriver):
"""Fake OneView driver. For testing purposes. """
def __init__(self):
if not importutils.try_import('oneview_client.client'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_("Unable to import python-oneviewclient library"))
# Checks connectivity to OneView and version compatibility on driver
# initialization
oneview_client = common.get_oneview_client()
oneview_client.verify_oneview_version()
oneview_client.verify_credentials()
self.power = power.OneViewPower()
self.management = management.OneViewManagement()
self.boot = fake.FakeBoot()
self.deploy = fake.FakeDeploy()

View File

@ -13,6 +13,7 @@
# under the License.
from oslo_log import log as logging
import six
from ironic.common import exception
from ironic.common.i18n import _
@ -216,3 +217,28 @@ def force_persistent_boot(task, device, persistent):
node.driver_internal_info = driver_internal_info
node.save()
def capabilities_to_dict(capabilities):
"""Parse the capabilities string into a dictionary
:param capabilities: the capabilities of the node as a formatted string.
:raises: InvalidParameterValue if capabilities is not an string or has a
malformed value
"""
capabilities_dict = {}
if capabilities:
if not isinstance(capabilities, six.string_types):
raise exception.InvalidParameterValue(
_("Value of 'capabilities' must be string. Got %s")
% type(capabilities))
try:
for capability in capabilities.split(','):
key, value = capability.split(':')
capabilities_dict[key] = value
except ValueError:
raise exception.InvalidParameterValue(
_("Malformed capabilities value: %s") % capability
)
return capabilities_dict

View File

@ -326,3 +326,21 @@ def get_test_cimc_info():
"cimc_password": "password",
"cimc_address": "1.2.3.4",
}
def get_test_oneview_properties():
return {
"cpu_arch": "x86_64",
"cpus": "8",
"local_gb": "10",
"memory_mb": "4096",
"capabilities": "server_hardware_type_uri:fake_sht_uri,"
"enclosure_group_uri:fake_eg_uri"
}
def get_test_oneview_driver_info():
return {
'server_hardware_uri': 'fake_uri',
'server_profile_template_uri': 'fake_spt_uri'
}

View File

@ -0,0 +1,173 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
import mock
from oslo_utils import importutils
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers.modules.oneview import common
from ironic.tests.unit.conductor import utils as mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.objects import utils as obj_utils
oneview_states = importutils.try_import('oneview_client.states')
PROPERTIES_DICT = {"cpu_arch": "x86_64",
"cpus": "8",
"local_gb": "10",
"memory_mb": "4096",
"capabilities": "server_hardware_type_uri:fake_sht_uri,"
"enclosure_group_uri:fake_eg_uri"}
DRIVER_INFO_DICT = {'server_hardware_uri': 'fake_sh_uri',
'server_profile_template_uri': 'fake_spt_uri'}
class OneViewCommonTestCase(db_base.DbTestCase):
def setUp(self):
super(OneViewCommonTestCase, self).setUp()
self.node = obj_utils.create_test_node(
self.context, driver='fake_oneview', properties=PROPERTIES_DICT,
driver_info=DRIVER_INFO_DICT,
)
self.config(manager_url='https://1.2.3.4', group='oneview')
self.config(username='user', group='oneview')
self.config(password='password', group='oneview')
mgr_utils.mock_the_extension_manager(driver="fake_oneview")
def test_verify_node_info(self):
common.verify_node_info(self.node)
def test_verify_node_info_missing_node_properties(self):
self.node.properties = {
"cpu_arch": "x86_64",
"cpus": "8",
"local_gb": "10",
"memory_mb": "4096",
"capabilities": "enclosure_group_uri:fake_eg_uri"
}
exc = self.assertRaises(
exception.MissingParameterValue,
common.verify_node_info,
self.node
)
self.assertEqual("Missing the keys for the following OneView data in "
"node's properties/capabilities: "
"server_hardware_type_uri.",
str(exc))
def test_verify_node_info_missing_node_driver_info(self):
self.node.driver_info = {
'server_hardware_uri': 'fake_sh_uri'
}
exc = self.assertRaises(
exception.MissingParameterValue,
common.verify_node_info,
self.node
)
self.assertEqual("Missing the keys for the following OneView data in "
"node's driver_info: server_profile_template_uri.",
str(exc))
def test_get_oneview_info(self):
complete_node = self.node
expected_node_info = {
'server_hardware_uri': 'fake_sh_uri',
'server_hardware_type_uri': 'fake_sht_uri',
'enclosure_group_uri': 'fake_eg_uri',
'server_profile_template_uri': 'fake_spt_uri',
}
self.assertEqual(
expected_node_info,
common.get_oneview_info(complete_node)
)
def test__verify_node_info(self):
common._verify_node_info("properties",
{"a": True,
"b": False,
"c": 0,
"d": "something",
"e": "somethingelse"},
["a", "b", "c", "e"])
def test__verify_node_info_fails(self):
self.assertRaises(
exception.MissingParameterValue,
common._verify_node_info,
"properties",
{"a": 1, "b": 2, "c": 3},
["x"]
)
def test__verify_node_info_missing_values_empty_string(self):
exc_expected_msg = ("Missing parameter value for: 'properties:a'"
", 'properties:b'")
self.assertRaisesRegexp(
exception.MissingParameterValue,
exc_expected_msg,
common._verify_node_info,
"properties",
{"a": '', "b": None, "c": "something"},
["a", "b", "c"]
)
def _test_translate_oneview_states(self, power_state_to_translate,
expected_translated_power_state):
translated_power_state = common.translate_oneview_power_state(
power_state_to_translate)
self.assertEqual(translated_power_state,
expected_translated_power_state)
def test_all_scenarios_for_translate_oneview_states(self):
self._test_translate_oneview_states(
oneview_states.ONEVIEW_POWERING_OFF, states.POWER_ON)
self._test_translate_oneview_states(
oneview_states.ONEVIEW_POWER_OFF, states.POWER_OFF)
self._test_translate_oneview_states(
oneview_states.ONEVIEW_POWERING_ON, states.POWER_OFF)
self._test_translate_oneview_states(
oneview_states.ONEVIEW_RESETTING, states.REBOOT)
self._test_translate_oneview_states("anything", states.ERROR)
@mock.patch.object(common, 'get_oneview_client', spec_set=True,
autospec=True)
def test_validate_oneview_resources_compatibility(self,
mock_get_ov_client):
oneview_client = mock_get_ov_client()
with task_manager.acquire(self.context, self.node.uuid) as task:
common.validate_oneview_resources_compatibility(task)
self.assertTrue(
oneview_client.validate_node_server_hardware.called)
self.assertTrue(
oneview_client.validate_node_server_hardware_type.called)
self.assertTrue(
oneview_client.check_server_profile_is_applied.called)
self.assertTrue(
oneview_client.is_node_port_mac_compatible_with_server_profile.
called)
self.assertTrue(
oneview_client.validate_node_enclosure_group.called)
self.assertTrue(
oneview_client.validate_node_server_profile_template.called)

View File

@ -0,0 +1,184 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
import mock
from oslo_utils import importutils
from oslo_utils import uuidutils
from ironic.common import boot_devices
from ironic.common import driver_factory
from ironic.common import exception
from ironic.conductor import task_manager
from ironic.drivers.modules.oneview import common
from ironic.drivers.modules.oneview import management
from ironic.tests.unit.conductor import utils as mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as obj_utils
oneview_exceptions = importutils.try_import('oneview_client.exceptions')
@mock.patch.object(common, 'get_oneview_client', spect_set=True, autospec=True)
class OneViewManagementDriverTestCase(db_base.DbTestCase):
def setUp(self):
super(OneViewManagementDriverTestCase, self).setUp()
self.config(manager_url='https://1.2.3.4', group='oneview')
self.config(username='user', group='oneview')
self.config(password='password', group='oneview')
mgr_utils.mock_the_extension_manager(driver="fake_oneview")
self.driver = driver_factory.get_driver("fake_oneview")
self.node = obj_utils.create_test_node(
self.context, driver='fake_oneview',
properties=db_utils.get_test_oneview_properties(),
driver_info=db_utils.get_test_oneview_driver_info(),
)
self.info = common.get_oneview_info(self.node)
@mock.patch.object(common, 'validate_oneview_resources_compatibility',
spect_set=True, autospec=True)
def test_validate(self, mock_validate, mock_get_ov_client):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.management.validate(task)
self.assertTrue(mock_validate.called)
def test_validate_fail(self, mock_get_ov_client):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid(),
id=999,
driver='fake_oneview')
with task_manager.acquire(self.context, node.uuid) as task:
self.assertRaises(exception.MissingParameterValue,
task.driver.management.validate, task)
@mock.patch.object(common, 'validate_oneview_resources_compatibility',
spect_set=True, autospec=True)
def test_validate_fail_exception(self, mock_validate, mock_get_ov_client):
mock_validate.side_effect = exception.OneViewError('message')
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.management.validate,
task)
def test_get_properties(self, mock_get_ov_client):
expected = common.COMMON_PROPERTIES
self.assertItemsEqual(expected,
self.driver.management.get_properties())
def test_set_boot_device(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.driver.management.set_boot_device(task, boot_devices.PXE)
oneview_client.set_boot_device.assert_called_once_with(
self.info,
management.BOOT_DEVICE_MAPPING_TO_OV.get(boot_devices.PXE)
)
def test_set_boot_device_invalid_device(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
self.driver.management.set_boot_device,
task, 'fake-device')
self.assertFalse(oneview_client.set_boot_device.called)
def test_set_boot_device_fail_to_get_server_profile(self,
mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.get_server_profile_from_hardware.side_effect = \
oneview_exceptions.OneViewException()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.OneViewError,
self.driver.management.set_boot_device,
task, 'disk')
self.assertFalse(oneview_client.set_boot_device.called)
def test_set_boot_device_without_server_profile(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.get_server_profile_from_hardware.return_value = False
with task_manager.acquire(self.context, self.node.uuid) as task:
expected_msg = (
'A Server Profile is not associated with node %s.'
% self.node.uuid
)
self.assertRaisesRegexp(
exception.OperationNotPermitted,
expected_msg,
self.driver.management.set_boot_device,
task,
'disk'
)
def test_get_supported_boot_devices(self, mock_get_ov_client):
with task_manager.acquire(self.context, self.node.uuid) as task:
expected = [boot_devices.PXE, boot_devices.DISK,
boot_devices.CDROM]
self.assertItemsEqual(
expected,
task.driver.management.get_supported_boot_devices(task),
)
def test_get_boot_device(self, mock_get_ov_client):
device_mapping = management.BOOT_DEVICE_MAPPING_TO_OV
oneview_client = mock_get_ov_client()
with task_manager.acquire(self.context, self.node.uuid) as task:
# For each known device on OneView, Ironic should return its
# counterpart value
for device_ironic, device_ov in device_mapping.items():
oneview_client.get_boot_order.return_value = [device_ov]
expected_response = {
'boot_device': device_ironic,
'persistent': True
}
response = self.driver.management.get_boot_device(task)
self.assertEqual(expected_response, response)
oneview_client.get_boot_order.assert_called_with(self.info)
def test_get_boot_device_fail(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.get_boot_order.side_effect = \
oneview_exceptions.OneViewException()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.OneViewError,
self.driver.management.get_boot_device,
task)
oneview_client.get_boot_order.assert_called_with(self.info)
def test_get_boot_device_unknown_device(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.get_boot_order.return_value = ["spam",
"bacon"]
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(
exception.InvalidParameterValue,
task.driver.management.get_boot_device,
task
)
def test_get_sensors_data_not_implemented(self, mock_get_ov_client):
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(
NotImplementedError,
task.driver.management.get_sensors_data,
task
)

View File

@ -0,0 +1,192 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2015 Hewlett Packard Development Company, LP
# Copyright 2015 Universidade Federal de Campina Grande
#
# 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.
import mock
from oslo_utils import importutils
from oslo_utils import uuidutils
from ironic.common import driver_factory
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers.modules.oneview import common
from ironic.tests.unit.conductor import utils as mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as obj_utils
oneview_exceptions = importutils.try_import('oneview_client.exceptions')
# TODO(afaranha) move this variable to db_utils.get_test_oneview_properties()
PROPERTIES_DICT = {'cpu_arch': 'x86_64',
'cpus': '8',
'local_gb': '10',
'memory_mb': '4096',
'capabilities': 'server_hardware_type_uri:fake_sht_uri,'
'enclosure_group_uri:fake_eg_uri',
}
DRIVER_INFO_DICT = {'server_hardware_uri': 'fake_uri',
'server_profile_template_uri': 'fake_spt_uri',
}
POWER_ON = 'On'
POWER_OFF = 'Off'
ERROR = 'error'
@mock.patch.object(common, 'get_oneview_client', spec_set=True, autospec=True)
class OneViewPowerDriverTestCase(db_base.DbTestCase):
def setUp(self):
super(OneViewPowerDriverTestCase, self).setUp()
self.config(manager_url='https://1.2.3.4', group='oneview')
self.config(username='user', group='oneview')
self.config(password='password', group='oneview')
mgr_utils.mock_the_extension_manager(driver='fake_oneview')
self.driver = driver_factory.get_driver('fake_oneview')
self.node = obj_utils.create_test_node(
self.context, driver='fake_oneview',
properties=db_utils.get_test_oneview_properties(),
driver_info=db_utils.get_test_oneview_driver_info(),
)
self.info = common.get_oneview_info(self.node)
@mock.patch.object(common, 'validate_oneview_resources_compatibility',
spect_set=True, autospec=True)
def test_power_interface_validate(self, mock_validate, mock_get_ov_client):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.power.validate(task)
self.assertTrue(mock_validate.called)
def test_power_interface_validate_fail(self, mock_get_ov_client):
node = obj_utils.create_test_node(self.context,
uuid=uuidutils.generate_uuid(),
id=999,
driver='fake_oneview')
with task_manager.acquire(self.context, node.uuid) as task:
self.assertRaises(exception.MissingParameterValue,
task.driver.power.validate, task)
@mock.patch.object(common, 'validate_oneview_resources_compatibility',
spect_set=True, autospec=True)
def test_power_interface_validate_fail_exception(self, mock_validate,
mock_get_ov_client):
mock_validate.side_effect = exception.OneViewError('message')
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
task.driver.power.validate,
task)
def test_power_interface_get_properties(self, mock_get_ov_client):
expected = common.COMMON_PROPERTIES
self.assertItemsEqual(expected, self.driver.power.get_properties())
def test_get_power_state(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.get_node_power_state.return_value = POWER_ON
with task_manager.acquire(self.context, self.node.uuid) as task:
self.driver.power.get_power_state(task)
oneview_client.get_node_power_state.assert_called_once_with(self.info)
def test_get_power_state_fail(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.get_node_power_state.side_effect = \
oneview_exceptions.OneViewException()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(
exception.OneViewError,
self.driver.power.get_power_state,
task
)
def test_set_power_on(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.power_on.return_value = POWER_ON
with task_manager.acquire(self.context, self.node.uuid) as task:
self.driver.power.set_power_state(task, states.POWER_ON)
oneview_client.power_on.assert_called_once_with(self.info)
def test_set_power_off(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.power_off.return_value = POWER_OFF
with task_manager.acquire(self.context, self.node.uuid) as task:
self.driver.power.set_power_state(task, states.POWER_OFF)
oneview_client.power_off.assert_called_once_with(self.info)
def test_set_power_on_fail(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.power_on.side_effect = \
oneview_exceptions.OneViewException()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.OneViewError,
self.driver.power.set_power_state, task,
states.POWER_ON)
oneview_client.power_on.assert_called_once_with(self.info)
def test_set_power_off_fail(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.power_off.side_effect = \
oneview_exceptions.OneViewException()
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.OneViewError,
self.driver.power.set_power_state, task,
states.POWER_OFF)
oneview_client.power_off.assert_called_once_with(self.info)
def test_set_power_invalid_state(self, mock_get_ov_client):
with task_manager.acquire(self.context, self.node.uuid) as task:
self.assertRaises(exception.InvalidParameterValue,
self.driver.power.set_power_state, task,
'fake state')
def test_set_power_reboot(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.power_off.return_value = POWER_OFF
oneview_client.power_on.return_value = POWER_ON
with task_manager.acquire(self.context, self.node.uuid) as task:
self.driver.power.set_power_state(task, states.REBOOT)
oneview_client.power_off.assert_called_once_with(self.info)
oneview_client.power_on.assert_called_once_with(self.info)
def test_reboot(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.power_off.return_value = POWER_OFF
oneview_client.power_on.return_value = POWER_ON
with task_manager.acquire(self.context, self.node.uuid) as task:
self.driver.power.reboot(task)
oneview_client.power_off.assert_called_once_with(self.info)
oneview_client.power_on.assert_called_once_with(self.info)
def test_reboot_fail(self, mock_get_ov_client):
oneview_client = mock_get_ov_client()
oneview_client.power_off.side_effect = \
oneview_exceptions.OneViewException()
with task_manager.acquire(self.context,
self.node.uuid) as task:
self.assertRaises(exception.OneViewError,
self.driver.power.reboot,
task)
oneview_client.power_off.assert_called_once_with(self.info)
self.assertFalse(oneview_client.power_on.called)

View File

@ -0,0 +1,248 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Red Hat, Inc.
# 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.
import time
import types
import mock
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils
from ironic.drivers.modules import agent_client
from ironic.drivers.modules.oneview import power
from ironic.drivers.modules.oneview import vendor
from ironic.drivers.modules import pxe
from ironic.tests.unit.conductor import utils as mgr_utils
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.objects import utils as obj_utils
GET_POWER_STATE_RETRIES = 5
class TestBaseAgentVendor(db_base.DbTestCase):
def setUp(self):
super(TestBaseAgentVendor, self).setUp()
self.config(
post_deploy_get_power_state_retries=GET_POWER_STATE_RETRIES,
group='agent')
mgr_utils.mock_the_extension_manager(driver="agent_pxe_oneview")
self.passthru = vendor.AgentVendorInterface()
self.node = obj_utils.create_test_node(
self.context, driver='agent_pxe_oneview',
properties=db_utils.get_test_oneview_properties(),
driver_info=db_utils.get_test_oneview_driver_info(),
)
@mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(power.OneViewPower, 'get_power_state',
spec=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'power_off',
spec=types.FunctionType)
@mock.patch('ironic.conductor.utils.node_set_boot_device', autospec=True)
def test_reboot_and_finish_deploy(self, set_bootdev_mock, power_off_mock,
get_power_state_mock,
node_power_action_mock):
self.node.provision_state = states.DEPLOYING
self.node.target_provision_state = states.ACTIVE
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
get_power_state_mock.side_effect = [states.POWER_ON,
states.POWER_OFF]
self.passthru.reboot_and_finish_deploy(task)
power_off_mock.assert_called_once_with(task.node)
self.assertEqual(2, get_power_state_mock.call_count)
set_bootdev_mock.assert_called_once_with(task, 'disk',
persistent=True)
node_power_action_mock.assert_called_once_with(
task, states.POWER_ON)
self.assertEqual(states.ACTIVE, task.node.provision_state)
self.assertEqual(states.NOSTATE, task.node.target_provision_state)
@mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(power.OneViewPower, 'get_power_state',
spec=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'power_off',
spec=types.FunctionType)
def test_reboot_and_finish_deploy_soft_poweroff_doesnt_complete(
self, power_off_mock, get_power_state_mock,
node_power_action_mock):
self.node.provision_state = states.DEPLOYING
self.node.target_provision_state = states.ACTIVE
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
get_power_state_mock.return_value = states.POWER_ON
self.passthru.reboot_and_finish_deploy(task)
power_off_mock.assert_called_once_with(task.node)
self.assertEqual(GET_POWER_STATE_RETRIES + 1,
get_power_state_mock.call_count)
node_power_action_mock.assert_has_calls([
mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)
])
self.assertEqual(states.ACTIVE, task.node.provision_state)
self.assertEqual(states.NOSTATE, task.node.target_provision_state)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(agent_client.AgentClient, 'power_off',
spec=types.FunctionType)
def test_reboot_and_finish_deploy_soft_poweroff_fails(
self, power_off_mock, node_power_action_mock):
power_off_mock.side_effect = iter([RuntimeError("boom")])
self.node.provision_state = states.DEPLOYING
self.node.target_provision_state = states.ACTIVE
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.passthru.reboot_and_finish_deploy(task)
power_off_mock.assert_called_once_with(task.node)
node_power_action_mock.assert_has_calls([
mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)
])
self.assertEqual(states.ACTIVE, task.node.provision_state)
self.assertEqual(states.NOSTATE, task.node.target_provision_state)
@mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(power.OneViewPower, 'get_power_state',
spec=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'power_off',
spec=types.FunctionType)
def test_reboot_and_finish_deploy_get_power_state_fails(
self, power_off_mock, get_power_state_mock,
node_power_action_mock):
self.node.provision_state = states.DEPLOYING
self.node.target_provision_state = states.ACTIVE
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
get_power_state_mock.side_effect = iter([RuntimeError("boom")])
self.passthru.reboot_and_finish_deploy(task)
power_off_mock.assert_called_once_with(task.node)
self.assertEqual(GET_POWER_STATE_RETRIES + 1,
get_power_state_mock.call_count)
node_power_action_mock.assert_has_calls([
mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)
])
self.assertEqual(states.ACTIVE, task.node.provision_state)
self.assertEqual(states.NOSTATE, task.node.target_provision_state)
@mock.patch.object(time, 'sleep', lambda seconds: None)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(power.OneViewPower, 'get_power_state',
spec=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'power_off',
spec=types.FunctionType)
def test_reboot_and_finish_deploy_power_action_fails(
self, power_off_mock, get_power_state_mock,
node_power_action_mock):
self.node.provision_state = states.DEPLOYING
self.node.target_provision_state = states.ACTIVE
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
get_power_state_mock.return_value = states.POWER_ON
node_power_action_mock.side_effect = iter([RuntimeError("boom")])
self.assertRaises(exception.InstanceDeployFailure,
self.passthru.reboot_and_finish_deploy,
task)
power_off_mock.assert_called_once_with(task.node)
self.assertEqual(GET_POWER_STATE_RETRIES + 1,
get_power_state_mock.call_count)
node_power_action_mock.assert_has_calls([
mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_OFF)])
self.assertEqual(states.DEPLOYFAIL, task.node.provision_state)
self.assertEqual(states.ACTIVE, task.node.target_provision_state)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(power.OneViewPower, 'get_power_state',
spec=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'power_off',
spec=types.FunctionType)
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
'.check_deploy_success', autospec=True)
@mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True)
def test_reboot_to_instance(self, clean_pxe_mock, check_deploy_mock,
power_off_mock, get_power_state_mock,
node_power_action_mock):
check_deploy_mock.return_value = None
self.node.provision_state = states.DEPLOYWAIT
self.node.target_provision_state = states.ACTIVE
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
get_power_state_mock.return_value = states.POWER_OFF
task.node.driver_internal_info['is_whole_disk_image'] = True
self.passthru.reboot_to_instance(task)
clean_pxe_mock.assert_called_once_with(task.driver.boot, task)
check_deploy_mock.assert_called_once_with(mock.ANY, task.node)
power_off_mock.assert_called_once_with(task.node)
get_power_state_mock.assert_called_once_with(task)
node_power_action_mock.assert_called_once_with(
task, states.POWER_ON)
self.assertEqual(states.ACTIVE, task.node.provision_state)
self.assertEqual(states.NOSTATE, task.node.target_provision_state)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(power.OneViewPower, 'get_power_state',
spec=types.FunctionType)
@mock.patch.object(agent_client.AgentClient, 'power_off',
spec=types.FunctionType)
@mock.patch('ironic.drivers.modules.agent.AgentVendorInterface'
'.check_deploy_success', autospec=True)
@mock.patch.object(pxe.PXEBoot, 'clean_up_ramdisk', autospec=True)
def test_reboot_to_instance_boot_none(self, clean_pxe_mock,
check_deploy_mock,
power_off_mock,
get_power_state_mock,
node_power_action_mock):
check_deploy_mock.return_value = None
self.node.provision_state = states.DEPLOYWAIT
self.node.target_provision_state = states.ACTIVE
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
get_power_state_mock.return_value = states.POWER_OFF
task.node.driver_internal_info['is_whole_disk_image'] = True
task.driver.boot = None
self.passthru.reboot_to_instance(task)
self.assertFalse(clean_pxe_mock.called)
check_deploy_mock.assert_called_once_with(mock.ANY, task.node)
power_off_mock.assert_called_once_with(task.node)
get_power_state_mock.assert_called_once_with(task)
node_power_action_mock.assert_called_once_with(
task, states.POWER_ON)
self.assertEqual(states.ACTIVE, task.node.provision_state)
self.assertEqual(states.NOSTATE, task.node.target_provision_state)

View File

@ -163,3 +163,68 @@ class UtilsTestCase(db_base.DbTestCase):
False,
task.node.driver_internal_info.get('is_next_boot_persistent')
)
def test_capabilities_to_dict(self):
capabilities_more_than_one_item = 'a:b,c:d'
capabilities_exactly_one_item = 'e:f'
# Testing empty capabilities
self.assertEqual(
{},
driver_utils.capabilities_to_dict('')
)
self.assertEqual(
{'e': 'f'},
driver_utils.capabilities_to_dict(capabilities_exactly_one_item)
)
self.assertEqual(
{'a': 'b', 'c': 'd'},
driver_utils.capabilities_to_dict(capabilities_more_than_one_item)
)
def test_capabilities_to_dict_with_only_key_or_value_fail(self):
capabilities_only_key_or_value = 'xpto'
exc = self.assertRaises(
exception.InvalidParameterValue,
driver_utils.capabilities_to_dict,
capabilities_only_key_or_value
)
self.assertEqual('Malformed capabilities value: xpto', str(exc))
def test_capabilities_to_dict_with_invalid_character_fail(self):
for test_capabilities in ('xpto:a,', ',xpto:a'):
exc = self.assertRaises(
exception.InvalidParameterValue,
driver_utils.capabilities_to_dict,
test_capabilities
)
self.assertEqual('Malformed capabilities value: ', str(exc))
def test_capabilities_to_dict_with_incorrect_format_fail(self):
for test_capabilities in (':xpto,', 'xpto:,', ':,'):
exc = self.assertRaises(
exception.InvalidParameterValue,
driver_utils.capabilities_to_dict,
test_capabilities
)
self.assertEqual('Malformed capabilities value: ', str(exc))
def test_capabilities_not_string(self):
capabilities_already_dict = {'a': 'b'}
capabilities_something_else = 42
exc = self.assertRaises(
exception.InvalidParameterValue,
driver_utils.capabilities_to_dict,
capabilities_already_dict
)
self.assertEqual("Value of 'capabilities' must be string. Got " +
str(dict), str(exc))
exc = self.assertRaises(
exception.InvalidParameterValue,
driver_utils.capabilities_to_dict,
capabilities_something_else
)
self.assertEqual("Value of 'capabilities' must be string. Got " +
str(int), str(exc))

View File

@ -103,6 +103,24 @@ SCCICLIENT_IRMC_SCCI_SPEC = (
'get_virtual_fd_set_params_cmd',
)
ONEVIEWCLIENT_SPEC = (
'client',
'states',
'exceptions',
)
ONEVIEWCLIENT_CLIENT_CLS_SPEC = (
)
ONEVIEWCLIENT_STATES_SPEC = (
'ONEVIEW_POWER_OFF',
'ONEVIEW_POWERING_OFF',
'ONEVIEW_POWER_ON',
'ONEVIEW_POWERING_ON',
'ONEVIEW_RESETTING',
'ONEVIEW_ERROR',
)
# seamicro
SEAMICRO_SPEC = (
'client',

View File

@ -27,6 +27,7 @@ Current list of mocked libraries:
- proliantutils
- pysnmp
- scciclient
- oneview_client
"""
import sys
@ -99,6 +100,30 @@ if not proliantutils:
six.moves.reload_module(sys.modules['ironic.drivers.ilo'])
oneview_client = importutils.try_import('oneview_client')
if not oneview_client:
oneview_client = mock.MagicMock(spec_set=mock_specs.ONEVIEWCLIENT_SPEC)
sys.modules['oneview_client'] = oneview_client
sys.modules['oneview_client.client'] = oneview_client.client
sys.modules['oneview_client.client.Client'] = mock.MagicMock(
spec_set=mock_specs.ONEVIEWCLIENT_CLIENT_CLS_SPEC
)
states = mock.MagicMock(
spec_set=mock_specs.ONEVIEWCLIENT_STATES_SPEC,
ONEVIEW_POWER_OFF='Off',
ONEVIEW_POWERING_OFF='PoweringOff',
ONEVIEW_POWER_ON='On',
ONEVIEW_POWERING_ON='PoweringOn',
ONEVIEW_RESETTING='Resetting',
ONEVIEW_ERROR='error')
sys.modules['oneview_client.states'] = states
sys.modules['oneview_client.exceptions'] = oneview_client.exceptions
oneview_client.exceptions.OneViewException = type('OneViewException',
(Exception,), {})
if 'ironic.drivers.oneview' in sys.modules:
six.moves.reload_module(sys.modules['ironic.drivers.modules.oneview'])
# attempt to load the external 'pywsman' library, which is required by
# the optional drivers.modules.drac and drivers.modules.amt module
pywsman = importutils.try_import('pywsman')

View File

@ -35,6 +35,7 @@ ironic.drivers =
agent_ilo = ironic.drivers.ilo:IloVirtualMediaAgentDriver
agent_ipmitool = ironic.drivers.agent:AgentAndIPMIToolDriver
agent_irmc = ironic.drivers.irmc:IRMCVirtualMediaAgentDriver
agent_pxe_oneview = ironic.drivers.oneview:AgentPXEOneViewDriver
agent_pyghmi = ironic.drivers.agent:AgentAndIPMINativeDriver
agent_ssh = ironic.drivers.agent:AgentAndSSHDriver
agent_vbox = ironic.drivers.agent:AgentAndVirtualBoxDriver
@ -58,8 +59,10 @@ ironic.drivers =
fake_ucs = ironic.drivers.fake:FakeUcsDriver
fake_cimc = ironic.drivers.fake:FakeCIMCDriver
fake_wol = ironic.drivers.fake:FakeWakeOnLanDriver
fake_oneview = ironic.drivers.oneview:FakeOneViewDriver
iscsi_ilo = ironic.drivers.ilo:IloVirtualMediaIscsiDriver
iscsi_irmc = ironic.drivers.irmc:IRMCVirtualMediaIscsiDriver
iscsi_pxe_oneview = ironic.drivers.oneview:ISCSIPXEOneViewDriver
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver