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:
parent
cee8b22a11
commit
460b9f1f5f
@ -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
|
||||
|
@ -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]
|
||||
|
||||
#
|
||||
|
@ -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")
|
||||
|
0
ironic/drivers/modules/oneview/__init__.py
Normal file
0
ironic/drivers/modules/oneview/__init__.py
Normal file
259
ironic/drivers/modules/oneview/common.py
Normal file
259
ironic/drivers/modules/oneview/common.py
Normal 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
|
172
ironic/drivers/modules/oneview/management.py
Normal file
172
ironic/drivers/modules/oneview/management.py
Normal 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()
|
133
ironic/drivers/modules/oneview/power.py
Normal file
133
ironic/drivers/modules/oneview/power.py
Normal 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)
|
113
ironic/drivers/modules/oneview/vendor.py
Normal file
113
ironic/drivers/modules/oneview/vendor.py
Normal 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
110
ironic/drivers/oneview.py
Normal 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()
|
@ -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
|
||||
|
@ -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'
|
||||
}
|
||||
|
173
ironic/tests/unit/drivers/modules/oneview/test_common.py
Normal file
173
ironic/tests/unit/drivers/modules/oneview/test_common.py
Normal 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)
|
184
ironic/tests/unit/drivers/modules/oneview/test_management.py
Normal file
184
ironic/tests/unit/drivers/modules/oneview/test_management.py
Normal 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
|
||||
)
|
192
ironic/tests/unit/drivers/modules/oneview/test_power.py
Normal file
192
ironic/tests/unit/drivers/modules/oneview/test_power.py
Normal 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)
|
248
ironic/tests/unit/drivers/modules/oneview/test_vendor.py
Normal file
248
ironic/tests/unit/drivers/modules/oneview/test_vendor.py
Normal 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)
|
@ -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))
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user