From 4483de30ba28e60d0f742441d928c3e28a291f90 Mon Sep 17 00:00:00 2001 From: Sinval Vieira Date: Mon, 29 Feb 2016 18:09:55 +0000 Subject: [PATCH] Add Dynamic Allocation feature for the OneView drivers This change is about adding the ability to the OneView drivers of dynamically allocate OneView resources to Ironic. The current version of the drivers consider what we call "pre-allocation" of nodes, meaning that when a node is registered in Ironic, even if it is not in use, this resource is still reserved in OneView. This change will prevent such situations by allocating OneView resources only at boot time, allowing both systems to really share the same pool of hardware. Change-Id: I43d1db490b4834080562946b8a6ca584ea36864d Co-Authored-By: Lilia Sampaio Co-Authored-By: Xavier Co-Authored-By: Hugo Nicodemos Co-Authored-By: Thiago Paiva Brito Co-Authored-By: Caio Oliveira Partial-Bug: #1541096 --- devstack/lib/ironic | 5 +- etc/ironic/ironic.conf.sample | 23 +- ironic/common/exception.py | 5 + ironic/conf/oneview.py | 24 +- ironic/drivers/modules/oneview/common.py | 79 +++- ironic/drivers/modules/oneview/deploy.py | 264 +++++++++++++ .../drivers/modules/oneview/deploy_utils.py | 335 +++++++++++++++++ ironic/drivers/modules/oneview/management.py | 4 - ironic/drivers/modules/oneview/power.py | 4 +- ironic/drivers/oneview.py | 31 +- .../drivers/modules/oneview/test_common.py | 142 ++++++- .../drivers/modules/oneview/test_deploy.py | 144 ++++++++ .../modules/oneview/test_deploy_utils.py | 349 ++++++++++++++++++ .../drivers/third_party_driver_mock_specs.py | 2 + .../unit/drivers/third_party_driver_mocks.py | 2 + ...c-allocation-feature-2fd6b4df7943f178.yaml | 5 + 16 files changed, 1359 insertions(+), 59 deletions(-) create mode 100644 ironic/drivers/modules/oneview/deploy.py create mode 100644 ironic/drivers/modules/oneview/deploy_utils.py create mode 100644 ironic/tests/unit/drivers/modules/oneview/test_deploy.py create mode 100644 ironic/tests/unit/drivers/modules/oneview/test_deploy_utils.py create mode 100644 releasenotes/notes/add-dynamic-allocation-feature-2fd6b4df7943f178.yaml diff --git a/devstack/lib/ironic b/devstack/lib/ironic index a6df583cfe..88953d0671 100644 --- a/devstack/lib/ironic +++ b/devstack/lib/ironic @@ -83,7 +83,7 @@ IRONIC_HW_ARCH=${IRONIC_HW_ARCH:-x86_64} # # # *_oneview: -# +# # # IRONIC_IPMIINFO_FILE is deprecated, please use IRONIC_HWINFO_FILE. IRONIC_IPMIINFO_FILE will be removed in Ocata. IRONIC_IPMIINFO_FILE=${IRONIC_IPMIINFO_FILE:-""} @@ -1053,8 +1053,11 @@ function enroll_nodes { local server_profile_template_uri server_profile_template_uri=$(echo $hardware_info |awk '{print $4}') mac_address=$(echo $hardware_info |awk '{print $5}') + local applied_server_profile_uri + applied_server_profile_uri=$(echo $hardware_info |awk '{print $6}') node_options+=" -i server_hardware_uri=$server_hardware_uri" + node_options+=" -i applied_server_profile_uri=$applied_server_profile_uri" node_options+=" -p capabilities=" node_options+="server_hardware_type_uri:$server_hardware_type_uri," node_options+="enclosure_group_uri:$enclosure_group_uri," diff --git a/etc/ironic/ironic.conf.sample b/etc/ironic/ironic.conf.sample index 62724b44f2..0ada7a9fff 100644 --- a/etc/ironic/ironic.conf.sample +++ b/etc/ironic/ironic.conf.sample @@ -1909,26 +1909,37 @@ # From ironic # -# URL where OneView is available (string value) +# URL where OneView is available. (string value) #manager_url = -# OneView username to be used (string value) +# OneView username to be used. (string value) #username = -# OneView password to be used (string value) +# OneView password to be used. (string value) #password = -# Option to allow insecure connection with OneView (boolean +# Option to allow insecure connection with OneView. (boolean # value) #allow_insecure_connections = false -# Path to CA certificate (string value) +# Path to CA certificate. (string value) #tls_cacert_file = -# Max connection retries to check changes on OneView (integer +# Max connection retries to check changes on OneView. (integer # value) #max_polling_attempts = 12 +# Period (in seconds) for periodic tasks to be executed. +# (integer value) +#periodic_check_interval = 300 + +# Whether to enable the periodic tasks for OneView driver be +# aware when OneView hardware resources are taken and released +# by Ironic or OneView users and proactively manage nodes in +# clean fail state according to Dynamic Allocation model of +# hardware resources allocation in OneView. (boolean value) +#enable_periodic_tasks = true + [oslo_concurrency] diff --git a/ironic/common/exception.py b/ironic/common/exception.py index bdb308daa4..7d8341c15e 100644 --- a/ironic/common/exception.py +++ b/ironic/common/exception.py @@ -595,6 +595,11 @@ class OneViewError(IronicException): _msg_fmt = _("OneView exception occurred. Error: %(error)s") +class OneViewInvalidNodeParameter(OneViewError): + _msg_fmt = _("Error while obtaining OneView info from node %(node_uuid)s. " + "Error: %(error)s") + + class NodeTagNotFound(IronicException): _msg_fmt = _("Node %(node_id)s doesn't have a tag '%(tag)s'") diff --git a/ironic/conf/oneview.py b/ironic/conf/oneview.py index 47f5bd7868..6dc5da6bae 100644 --- a/ironic/conf/oneview.py +++ b/ironic/conf/oneview.py @@ -20,20 +20,32 @@ from ironic.common.i18n import _ opts = [ cfg.StrOpt('manager_url', - help=_('URL where OneView is available')), + help=_('URL where OneView is available.')), cfg.StrOpt('username', - help=_('OneView username to be used')), + help=_('OneView username to be used.')), cfg.StrOpt('password', secret=True, - help=_('OneView password to be used')), + help=_('OneView password to be used.')), cfg.BoolOpt('allow_insecure_connections', default=False, - help=_('Option to allow insecure connection with OneView')), + help=_('Option to allow insecure connection with OneView.')), cfg.StrOpt('tls_cacert_file', - help=_('Path to CA certificate')), + help=_('Path to CA certificate.')), cfg.IntOpt('max_polling_attempts', default=12, - help=_('Max connection retries to check changes on OneView')), + help=_('Max connection retries to check changes on OneView.')), + cfg.BoolOpt('enable_periodic_tasks', + default=True, + help=_('Whether to enable the periodic tasks for OneView ' + 'driver be aware when OneView hardware resources are ' + 'taken and released by Ironic or OneView users ' + 'and proactively manage nodes in clean fail state ' + 'according to Dynamic Allocation model of hardware ' + 'resources allocation in OneView.')), + cfg.IntOpt('periodic_check_interval', + default=300, + help=_('Period (in seconds) for periodic tasks to be ' + 'executed when enable_periodic_tasks=True.')), ] diff --git a/ironic/drivers/modules/oneview/common.py b/ironic/drivers/modules/oneview/common.py index 275c65fcda..a40d4d549c 100644 --- a/ironic/drivers/modules/oneview/common.py +++ b/ironic/drivers/modules/oneview/common.py @@ -1,4 +1,3 @@ -# # Copyright 2015 Hewlett Packard Development Company, LP # Copyright 2015 Universidade Federal de Campina Grande # @@ -61,6 +60,15 @@ COMMON_PROPERTIES.update(REQUIRED_ON_DRIVER_INFO) COMMON_PROPERTIES.update(REQUIRED_ON_PROPERTIES) COMMON_PROPERTIES.update(OPTIONAL_ON_PROPERTIES) +ISCSI_PXE_ONEVIEW = 'iscsi_pxe_oneview' +AGENT_PXE_ONEVIEW = 'agent_pxe_oneview' + +# NOTE(xavierr): We don't want to translate NODE_IN_USE_BY_ONEVIEW and +# SERVER_HARDWARE_ALLOCATION_ERROR to avoid inconsistency in the nodes +# caused by updates on translation in upgrades of ironic. +NODE_IN_USE_BY_ONEVIEW = 'node in use by OneView' +SERVER_HARDWARE_ALLOCATION_ERROR = 'server hardware allocation error' + def get_oneview_client(): """Generates an instance of the OneView client. @@ -70,7 +78,6 @@ def get_oneview_client(): :returns: an instance of the OneView client """ - oneview_client = client.Client( manager_url=CONF.oneview.manager_url, username=CONF.oneview.username, @@ -140,12 +147,16 @@ def get_oneview_info(node): :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 + :raises OneViewInvalidNodeParameter if node capabilities are malformed """ - capabilities_dict = utils.capabilities_to_dict( - node.properties.get('capabilities', '') - ) + try: + capabilities_dict = utils.capabilities_to_dict( + node.properties.get('capabilities', '') + ) + except exception.InvalidParameterValue as e: + raise exception.OneViewInvalidNodeParameter(node_uuid=node.uuid, + error=e) driver_info = node.driver_info @@ -159,6 +170,8 @@ def get_oneview_info(node): 'server_profile_template_uri': capabilities_dict.get('server_profile_template_uri') or driver_info.get('server_profile_template_uri'), + 'applied_server_profile_uri': + driver_info.get('applied_server_profile_uri'), } return oneview_info @@ -180,25 +193,41 @@ def validate_oneview_resources_compatibility(task): node = task.node node_ports = task.ports + + try: + oneview_info = get_oneview_info(task.node) + except exception.InvalidParameterValue as e: + msg = (_("Error while obtaining OneView info from node " + "%(node_uuid)s. Error: %(error)s") % + {'node_uuid': node.uuid, 'error': e}) + raise exception.OneViewError(error=msg) + 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) + + # NOTE(thiagop): Support to pre-allocation will be dropped in 'P' + # release + if is_dynamic_allocation_enabled(task.node): + oneview_client.is_node_port_mac_compatible_with_server_hardware( + oneview_info, node_ports + ) + oneview_client.validate_node_server_profile_template(oneview_info) + else: + oneview_client.check_server_profile_is_applied(oneview_info) + oneview_client.is_node_port_mac_compatible_with_server_profile( + oneview_info, node_ports + ) except oneview_exceptions.OneViewException as oneview_exc: - msg = (_("Error validating node resources with OneView: %s") - % oneview_exc) - LOG.error(msg) + msg = (_("Error validating node resources with OneView: %s") % + oneview_exc) raise exception.OneViewError(error=msg) @@ -252,7 +281,13 @@ def node_has_server_profile(func): """ def inner(*args, **kwargs): task = args[1] - oneview_info = get_oneview_info(task.node) + try: + oneview_info = get_oneview_info(task.node) + except exception.InvalidParameterValue as e: + msg = (_("Error while obtaining OneView info from node " + "%(node_uuid)s. Error: %(error)s") % + {'node_uuid': task.node.uuid, 'error': e}) + raise exception.OneViewError(error=msg) oneview_client = get_oneview_client() try: node_has_server_profile = ( @@ -272,3 +307,17 @@ def node_has_server_profile(func): ) return func(*args, **kwargs) return inner + + +def is_dynamic_allocation_enabled(node): + flag = node.driver_info.get('dynamic_allocation') + if flag: + if isinstance(flag, bool): + return flag is True + else: + msg = (_LE("Invalid dynamic_allocation parameter value in " + "node's %(node_uuid)s driver_info. Valid values " + "are booleans true or false.") % + {"node_uuid": node.uuid}) + raise exception.InvalidParameterValue(msg) + return False diff --git a/ironic/drivers/modules/oneview/deploy.py b/ironic/drivers/modules/oneview/deploy.py new file mode 100644 index 0000000000..455e6e27e0 --- /dev/null +++ b/ironic/drivers/modules/oneview/deploy.py @@ -0,0 +1,264 @@ +# Copyright 2016 Hewlett Packard Enterprise Development LP. +# Copyright 2016 Universidade Federal de Campina Grande +# 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 abc + +from futurist import periodics +from oslo_log import log as logging +import six + +from ironic.common import exception +from ironic.common.i18n import _LE +from ironic.common.i18n import _LI +from ironic.common import states +from ironic.drivers.modules import agent +from ironic.drivers.modules import iscsi_deploy +from ironic.drivers.modules.oneview import common +from ironic.drivers.modules.oneview import deploy_utils +from ironic import objects + +LOG = logging.getLogger(__name__) + +CONF = common.CONF + + +@six.add_metaclass(abc.ABCMeta) +class OneViewPeriodicTasks(object): + + @abc.abstractproperty + def oneview_driver(self): + pass + + @periodics.periodic(spacing=CONF.oneview.periodic_check_interval, + enabled=CONF.oneview.enable_periodic_tasks) + def _periodic_check_nodes_taken_by_oneview(self, manager, context): + """Checks if nodes in Ironic were taken by OneView users. + + This driver periodic task will check for nodes that were taken by + OneView users while the node is in available state, set the node to + maintenance mode with an appropriate maintenance reason message and + move the node to manageable state. + + :param manager: a ConductorManager instance + :param context: request context + :returns: None. + """ + + filters = { + 'provision_state': states.AVAILABLE, + 'maintenance': False, + 'driver': self.oneview_driver + } + node_iter = manager.iter_nodes(filters=filters) + + for node_uuid, driver in node_iter: + + node = objects.Node.get(context, node_uuid) + + try: + oneview_using = deploy_utils.is_node_in_use_by_oneview(node) + except exception.OneViewError as e: + LOG.error(_LE("Error while determining if node " + "%(node_uuid)s is in use by OneView. " + "Error: %(error)s"), + {'node_uuid': node.uuid, 'error': e}) + + if oneview_using: + purpose = (_LI('Updating node %(node_uuid)s in use ' + 'by OneView from %(provision_state)s state ' + 'to %(target_state)s state and maintenance ' + 'mode %(maintenance)s.'), + {'node_uuid': node_uuid, + 'provision_state': states.AVAILABLE, + 'target_state': states.MANAGEABLE, + 'maintenance': True}) + + LOG.info(purpose) + + node.maintenance = True + node.maintenance_reason = common.NODE_IN_USE_BY_ONEVIEW + manager.update_node(context, node) + manager.do_provisioning_action(context, node.uuid, 'manage') + + @periodics.periodic(spacing=CONF.oneview.periodic_check_interval, + enabled=CONF.oneview.enable_periodic_tasks) + def _periodic_check_nodes_freed_by_oneview(self, manager, context): + """Checks if nodes taken by OneView users were freed. + + This driver periodic task will be responsible to poll the nodes that + are in maintenance mode and on manageable state to check if the Server + Profile was removed, indicating that the node was freed by the OneView + user. If so, it'll provide the node, that will pass through the + cleaning process and become available to be provisioned. + + :param manager: a ConductorManager instance + :param context: request context + :returns: None. + """ + + filters = { + 'provision_state': states.MANAGEABLE, + 'maintenance': True, + 'driver': self.oneview_driver + } + node_iter = manager.iter_nodes(fields=['maintenance_reason'], + filters=filters) + for node_uuid, driver, maintenance_reason in node_iter: + + if maintenance_reason == common.NODE_IN_USE_BY_ONEVIEW: + + node = objects.Node.get(context, node_uuid) + + try: + oneview_using = deploy_utils.is_node_in_use_by_oneview( + node + ) + except exception.OneViewError as e: + LOG.error(_LE("Error while determining if node " + "%(node_uuid)s is in use by OneView. " + "Error: %(error)s"), + {'node_uuid': node.uuid, 'error': e}) + + if not oneview_using: + purpose = (_LI('Bringing node %(node_uuid)s back from ' + 'use by OneView from %(provision_state)s ' + 'state to %(target_state)s state and ' + 'maintenance mode %(maintenance)s.'), + {'node_uuid': node_uuid, + 'provision_state': states.MANAGEABLE, + 'target_state': states.AVAILABLE, + 'maintenance': False}) + + LOG.info(purpose) + + node.maintenance = False + node.maintenance_reason = None + manager.update_node(context, node) + manager.do_provisioning_action( + context, node.uuid, 'provide' + ) + + @periodics.periodic(spacing=CONF.oneview.periodic_check_interval, + enabled=CONF.oneview.enable_periodic_tasks) + def _periodic_check_nodes_taken_on_cleanfail(self, manager, context): + """Checks failed deploys due to Oneview users taking Server Hardware. + + This last driver periodic task will take care of nodes that would be + caught on a race condition between OneView and a deploy by Ironic. In + such cases, the validation will fail, throwing the node on deploy fail + and, afterwards on clean fail. + + This task will set the node to maintenance mode with a proper reason + message and move it to manageable state, from where the second task + can rescue the node as soon as the Server Profile is removed. + + :param manager: a ConductorManager instance + :param context: request context + :returns: None. + """ + + filters = { + 'provision_state': states.CLEANFAIL, + 'driver': self.oneview_driver + } + node_iter = manager.iter_nodes(fields=['driver_internal_info'], + filters=filters) + + for node_uuid, driver, driver_internal_info in node_iter: + + node_oneview_error = driver_internal_info.get('oneview_error') + if node_oneview_error == common.SERVER_HARDWARE_ALLOCATION_ERROR: + + node = objects.Node.get(context, node_uuid) + + purpose = (_LI('Bringing node %(node_uuid)s back from use ' + 'by OneView from %(provision_state)s state ' + 'to %(target_state)s state and ' + 'maintenance mode %(maintenance)s.'), + {'node_uuid': node_uuid, + 'provision_state': states.CLEANFAIL, + 'target_state': states.MANAGEABLE, + 'maintenance': False}) + + LOG.info(purpose) + + node.maintenance = True + node.maintenance_reason = common.NODE_IN_USE_BY_ONEVIEW + driver_internal_info = node.driver_internal_info + driver_internal_info.pop('oneview_error', None) + node.driver_internal_info = driver_internal_info + manager.update_node(context, node) + manager.do_provisioning_action(context, node.uuid, 'manage') + + +class OneViewIscsiDeploy(iscsi_deploy.ISCSIDeploy, OneViewPeriodicTasks): + """Class for OneView ISCSI deployment driver.""" + + oneview_driver = common.ISCSI_PXE_ONEVIEW + + def get_properties(self): + deploy_utils.get_properties() + + def prepare(self, task): + if common.is_dynamic_allocation_enabled(task.node): + deploy_utils.prepare(task) + super(OneViewIscsiDeploy, self).prepare(task) + + def tear_down(self, task): + if (common.is_dynamic_allocation_enabled(task.node) and + not CONF.conductor.automated_clean): + deploy_utils.tear_down(task) + super(OneViewIscsiDeploy, self).tear_down(task) + + def prepare_cleaning(self, task): + if common.is_dynamic_allocation_enabled(task.node): + deploy_utils.prepare_cleaning(task) + return super(OneViewIscsiDeploy, self).prepare_cleaning(task) + + def tear_down_cleaning(self, task): + if common.is_dynamic_allocation_enabled(task.node): + deploy_utils.tear_down_cleaning(task) + return super(OneViewIscsiDeploy, self).tear_down_cleaning(task) + + +class OneViewAgentDeploy(agent.AgentDeploy, OneViewPeriodicTasks): + """Class for OneView Agent deployment driver.""" + + oneview_driver = common.AGENT_PXE_ONEVIEW + + def get_properties(self): + deploy_utils.get_properties() + + def prepare(self, task): + if common.is_dynamic_allocation_enabled(task.node): + deploy_utils.prepare(task) + super(OneViewAgentDeploy, self).prepare(task) + + def tear_down(self, task): + if (common.is_dynamic_allocation_enabled(task.node) and + not CONF.conductor.automated_clean): + deploy_utils.tear_down(task) + super(OneViewAgentDeploy, self).tear_down(task) + + def prepare_cleaning(self, task): + if common.is_dynamic_allocation_enabled(task.node): + deploy_utils.prepare_cleaning(task) + return super(OneViewAgentDeploy, self).prepare_cleaning(task) + + def tear_down_cleaning(self, task): + if common.is_dynamic_allocation_enabled(task.node): + deploy_utils.tear_down_cleaning(task) + return super(OneViewAgentDeploy, self).tear_down_cleaning(task) diff --git a/ironic/drivers/modules/oneview/deploy_utils.py b/ironic/drivers/modules/oneview/deploy_utils.py new file mode 100644 index 0000000000..7bb00727ee --- /dev/null +++ b/ironic/drivers/modules/oneview/deploy_utils.py @@ -0,0 +1,335 @@ +# Copyright 2016 Hewlett Packard Enterprise Development LP. +# Copyright 2016 Universidade Federal de Campina Grande +# 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. + +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.i18n import _LI +from ironic.common.i18n import _LW +from ironic.common import states +from ironic.drivers.modules.oneview import common + +LOG = logging.getLogger(__name__) + +oneview_exception = importutils.try_import('oneview_client.exceptions') +oneview_utils = importutils.try_import('oneview_client.utils') + + +def get_properties(): + return common.COMMON_PROPERTIES + + +def prepare(task): + """Applies Server Profile and update the node when preparing. + + This method is responsible for applying a Server Profile to the Server + Hardware and add the uri of the applied Server Profile in the node's + 'applied_server_profile_uri' field on properties/capabilities. + + :param task: A TaskManager object + :raises InstanceDeployFailure: If the node doesn't have the needed OneView + informations, if Server Hardware is in use by an OneView user, or + if the Server Profile can't be applied. + + """ + if task.node.provision_state == states.DEPLOYING: + try: + instance_display_name = task.node.instance_info.get('display_name') + instance_uuid = task.node.instance_uuid + server_profile_name = ( + "%(instance_name)s [%(instance_uuid)s]" % + {"instance_name": instance_display_name, + "instance_uuid": instance_uuid} + ) + _allocate_server_hardware_to_ironic(task.node, server_profile_name) + except exception.OneViewError as e: + raise exception.InstanceDeployFailure(node=task.node.uuid, + reason=e) + + +def tear_down(task): + """Remove Server profile and update the node when tear down. + + This method is responsible for power a Server Hardware off, remove a Server + Profile from the Server Hardware and remove the uri of the applied Server + Profile from the node's 'applied_server_profile_uri' in + properties/capabilities. + + :param task: A TaskManager object + :raises InstanceDeployFailure: If node has no uri of applied Server + Profile, or if some error occur while deleting Server Profile. + + """ + try: + _deallocate_server_hardware_from_ironic(task.node) + except exception.OneViewError as e: + raise exception.InstanceDeployFailure(node=task.node.uuid, reason=e) + + +def prepare_cleaning(task): + """Applies Server Profile and update the node when preparing cleaning. + + This method is responsible for applying a Server Profile to the Server + Hardware and add the uri of the applied Server Profile in the node's + 'applied_server_profile_uri' field on properties/capabilities. + + :param task: A TaskManager object + :raises NodeCleaningFailure: If the node doesn't have the needed OneView + informations, if Server Hardware is in use by an OneView user, or + if the Server Profile can't be applied. + + """ + try: + server_profile_name = "Ironic Cleaning [%s]" % task.node.uuid + _allocate_server_hardware_to_ironic(task.node, server_profile_name) + except exception.OneViewError as e: + oneview_error = common.SERVER_HARDWARE_ALLOCATION_ERROR + driver_internal_info = task.node.driver_internal_info + driver_internal_info['oneview_error'] = oneview_error + task.node.driver_internal_info = driver_internal_info + task.node.save() + raise exception.NodeCleaningFailure(node=task.node.uuid, + reason=e) + + +def tear_down_cleaning(task): + """Remove Server profile and update the node when tear down cleaning. + + This method is responsible for power a Server Hardware off, remove a Server + Profile from the Server Hardware and remove the uri of the applied Server + Profile from the node's 'applied_server_profile_uri' in + properties/capabilities. + + :param task: A TaskManager object + :raises NodeCleaningFailure: If node has no uri of applied Server Profile, + or if some error occur while deleting Server Profile. + + """ + try: + _deallocate_server_hardware_from_ironic(task.node) + except exception.OneViewError as e: + raise exception.NodeCleaningFailure(node=task.node.uuid, reason=e) + + +def is_node_in_use_by_oneview(node): + """Check if node is in use by OneView user. + + :param node: an ironic node object + :returns: Boolean value. True if node is in use by OneView, + False otherwise. + :raises OneViewError: if not possible to get OneView's informations + for the given node, if not possible to retrieve Server Hardware + from OneView. + + """ + try: + oneview_info = common.get_oneview_info(node) + except exception.InvalidParameterValue as e: + msg = (_("Error while obtaining OneView info from node " + "%(node_uuid)s. Error: %(error)s") % + {'node_uuid': node.uuid, 'error': e}) + raise exception.OneViewError(error=msg) + + oneview_client = common.get_oneview_client() + + sh_uuid = oneview_utils.get_uuid_from_uri( + oneview_info.get("server_hardware_uri") + ) + + try: + server_hardware = oneview_client.get_server_hardware_by_uuid( + sh_uuid + ) + except oneview_exception.OneViewResourceNotFoundError as e: + msg = (_("Error while obtaining Server Hardware from node " + "%(node_uuid)s. Error: %(error)s") % + {'node_uuid': node.uuid, 'error': e}) + raise exception.OneViewError(error=msg) + + applied_sp_uri = ( + node.driver_info.get('applied_server_profile_uri') + ) + + # Check if Profile exists in Oneview and it is different of the one + # applied by ironic + if (server_hardware.server_profile_uri not in (None, '') and + applied_sp_uri != server_hardware.server_profile_uri): + + LOG.warning(_LW("Node %s is already in use by OneView."), + node.uuid) + + return True + + else: + LOG.debug(_( + "Hardware %(hardware_uri)s is free for use by " + "ironic on node %(node_uuid)s."), + {"hardware_uri": server_hardware.uri, + "node_uuid": node.uuid}) + + return False + + +def _add_applied_server_profile_uri_field(node, applied_profile): + """Adds the applied Server Profile uri to a node. + + :param node: an ironic node object + + """ + driver_info = node.driver_info + driver_info['applied_server_profile_uri'] = applied_profile.uri + node.driver_info = driver_info + node.save() + + +def _del_applied_server_profile_uri_field(node): + """Delete the applied Server Profile uri from a node if it exists. + + :param node: an ironic node object + + """ + driver_info = node.driver_info + driver_info.pop('applied_server_profile_uri', None) + node.driver_info = driver_info + node.save() + + +def _allocate_server_hardware_to_ironic(node, server_profile_name): + """Allocate Server Hardware to ironic. + + :param node: an ironic node object + :param server_profile_name: a formatted string with the Server Profile + name + :raises OneViewError: if an error occurs while allocating the Server + Hardware to ironic + + """ + node_in_use_by_oneview = is_node_in_use_by_oneview(node) + + if not node_in_use_by_oneview: + + try: + oneview_info = common.get_oneview_info(node) + except exception.InvalidParameterValue as e: + msg = (_("Error while obtaining OneView info from node " + "%(node_uuid)s. Error: %(error)s") % + {'node_uuid': node.uuid, 'error': e}) + raise exception.OneViewError(error=msg) + + applied_sp_uri = node.driver_info.get('applied_server_profile_uri') + + sh_uuid = oneview_utils.get_uuid_from_uri( + oneview_info.get("server_hardware_uri") + ) + spt_uuid = oneview_utils.get_uuid_from_uri( + oneview_info.get("server_profile_template_uri") + ) + oneview_client = common.get_oneview_client() + server_hardware = oneview_client.get_server_hardware_by_uuid(sh_uuid) + + # Don't have Server Profile on OneView but has + # `applied_server_profile_uri` on driver_info + if (server_hardware.server_profile_uri in (None, '') and + applied_sp_uri is not (None, '')): + + _del_applied_server_profile_uri_field(node) + LOG.info(_LI( + "Inconsistent 'applied_server_profile_uri' parameter " + "value in driver_info. There is no Server Profile " + "applied to node %(node_uuid)s. Value deleted."), + {"node_uuid": node.uuid} + ) + + # applied_server_profile_uri exists and is equal to Server profile + # applied on Hardware. Do not apply again. + if (applied_sp_uri and server_hardware.server_profile_uri and + server_hardware.server_profile_uri == applied_sp_uri): + LOG.info(_LI( + "The Server Profile %(applied_sp_uri)s was already applied " + "by ironic on node %(node_uuid)s. Reusing."), + {"node_uuid": node.uuid, "applied_sp_uri": applied_sp_uri} + ) + return + + try: + applied_profile = oneview_client.clone_template_and_apply( + server_profile_name, sh_uuid, spt_uuid + ) + _add_applied_server_profile_uri_field(node, applied_profile) + + LOG.info( + _LI("Server Profile %(server_profile_uuid)s was successfully" + " applied to node %(node_uuid)s."), + {"node_uuid": node.uuid, + "server_profile_uuid": applied_profile.uri} + ) + + except oneview_exception.OneViewServerProfileAssignmentError as e: + LOG.error(_LE("An error occurred during allocating server " + "hardware to ironic during prepare: %s"), e) + raise exception.OneViewError(error=e) + else: + msg = (_("Node %s is already in use by OneView.") % + node.uuid) + + raise exception.OneViewError(error=msg) + + +def _deallocate_server_hardware_from_ironic(node): + """Deallocate Server Hardware from ironic. + + :param node: an ironic node object + :raises OneViewError: if an error occurs while deallocating the Server + Hardware to ironic + + """ + try: + oneview_info = common.get_oneview_info(node) + except exception.InvalidParameterValue as e: + msg = (_("Error while obtaining OneView info from node " + "%(node_uuid)s. Error: %(error)s") % + {'node_uuid': node.uuid, 'error': e}) + raise exception.OneViewError(error=msg) + + oneview_client = common.get_oneview_client() + oneview_client.power_off(oneview_info) + + applied_sp_uuid = oneview_utils.get_uuid_from_uri( + oneview_info.get('applied_server_profile_uri') + ) + + try: + oneview_client.delete_server_profile(applied_sp_uuid) + _del_applied_server_profile_uri_field(node) + + LOG.info( + _LI("Server Profile %(server_profile_uuid)s was successfully" + " deleted from node %(node_uuid)s." + ), + {"node_uuid": node.uuid, "server_profile_uuid": applied_sp_uuid} + ) + except oneview_exception.OneViewException as e: + + msg = (_("Error while deleting applied Server Profile from node " + "%(node_uuid)s. Error: %(error)s") % + {'node_uuid': node.uuid, 'error': e}) + + raise exception.OneViewError( + node=node.uuid, reason=msg + ) diff --git a/ironic/drivers/modules/oneview/management.py b/ironic/drivers/modules/oneview/management.py index 790246fa9d..2f48323403 100644 --- a/ironic/drivers/modules/oneview/management.py +++ b/ironic/drivers/modules/oneview/management.py @@ -97,7 +97,6 @@ class OneViewManagement(base.ManagementInterface): 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): @@ -115,7 +114,6 @@ class OneViewManagement(base.ManagementInterface): "Error setting boot device on OneView. Error: %s") % oneview_exc ) - LOG.error(msg) raise exception.OneViewError(error=msg) @common.node_has_server_profile @@ -135,7 +133,6 @@ class OneViewManagement(base.ManagementInterface): :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: @@ -146,7 +143,6 @@ class OneViewManagement(base.ManagementInterface): "Error getting boot device from OneView. Error: %s") % oneview_exc ) - LOG.error(msg) raise exception.OneViewError(msg) primary_device = boot_order[0] diff --git a/ironic/drivers/modules/oneview/power.py b/ironic/drivers/modules/oneview/power.py index f53fbfdbff..17801e1827 100644 --- a/ironic/drivers/modules/oneview/power.py +++ b/ironic/drivers/modules/oneview/power.py @@ -69,8 +69,8 @@ class OneViewPower(base.PowerInterface): :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) @@ -95,8 +95,8 @@ class OneViewPower(base.PowerInterface): :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 ' diff --git a/ironic/drivers/oneview.py b/ironic/drivers/oneview.py index 8da4537b1a..4d5992d2b1 100644 --- a/ironic/drivers/oneview.py +++ b/ironic/drivers/oneview.py @@ -1,4 +1,3 @@ -# # Copyright 2015 Hewlett Packard Development Company, LP # Copyright 2015 Universidade Federal de Campina Grande # @@ -22,9 +21,9 @@ 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 iscsi_deploy from ironic.drivers.modules.oneview import common +from ironic.drivers.modules.oneview import deploy from ironic.drivers.modules.oneview import management from ironic.drivers.modules.oneview import power from ironic.drivers.modules.oneview import vendor @@ -32,14 +31,12 @@ from ironic.drivers.modules import pxe class AgentPXEOneViewDriver(base.BaseDriver): - """Agent + OneView driver. + """OneViewDriver using OneViewClient interface. - 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. + This driver implements the `core` functionality using + :class:ironic.drivers.modules.oneview.power.OneViewPower for power + management. And + :class:ironic.drivers.modules.oneview.deploy.OneViewAgentDeploy for deploy. """ def __init__(self): @@ -56,19 +53,17 @@ class AgentPXEOneViewDriver(base.BaseDriver): self.power = power.OneViewPower() self.management = management.OneViewManagement() self.boot = pxe.PXEBoot() - self.deploy = agent.AgentDeploy() + self.deploy = deploy.OneViewAgentDeploy() self.vendor = vendor.AgentVendorInterface() class ISCSIPXEOneViewDriver(base.BaseDriver): - """PXE + OneView driver. + """OneViewDriver using OneViewClient interface. - 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. + This driver implements the `core` functionality using + :class:ironic.drivers.modules.oneview.power.OneViewPower for power + management. And + :class:ironic.drivers.modules.oneview.deploy.OneViewIscsiDeploy for deploy. """ def __init__(self): @@ -85,5 +80,5 @@ class ISCSIPXEOneViewDriver(base.BaseDriver): self.power = power.OneViewPower() self.management = management.OneViewManagement() self.boot = pxe.PXEBoot() - self.deploy = iscsi_deploy.ISCSIDeploy() + self.deploy = deploy.OneViewIscsiDeploy() self.vendor = iscsi_deploy.VendorPassthru() diff --git a/ironic/tests/unit/drivers/modules/oneview/test_common.py b/ironic/tests/unit/drivers/modules/oneview/test_common.py index 81d1fa1112..928f3a574d 100644 --- a/ironic/tests/unit/drivers/modules/oneview/test_common.py +++ b/ironic/tests/unit/drivers/modules/oneview/test_common.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- -# # Copyright 2015 Hewlett Packard Development Company, LP # Copyright 2015 Universidade Federal de Campina Grande # @@ -115,6 +113,7 @@ class OneViewCommonTestCase(db_base.DbTestCase): 'server_hardware_type_uri': 'fake_sht_uri', 'enclosure_group_uri': 'fake_eg_uri', 'server_profile_template_uri': 'fake_spt_uri', + 'applied_server_profile_uri': None, } self.assertEqual( @@ -124,7 +123,6 @@ class OneViewCommonTestCase(db_base.DbTestCase): def test_get_oneview_info_missing_spt(self): driver_info = db_utils.get_test_oneview_driver_info() - properties = db_utils.get_test_oneview_properties() properties["capabilities"] = ("server_hardware_type_uri:fake_sht_uri," "enclosure_group_uri:fake_eg_uri") @@ -138,6 +136,7 @@ class OneViewCommonTestCase(db_base.DbTestCase): 'server_hardware_type_uri': 'fake_sht_uri', 'enclosure_group_uri': 'fake_eg_uri', 'server_profile_template_uri': None, + 'applied_server_profile_uri': None, } self.assertEqual( @@ -165,6 +164,7 @@ class OneViewCommonTestCase(db_base.DbTestCase): 'server_hardware_type_uri': 'fake_sht_uri', 'enclosure_group_uri': 'fake_eg_uri', 'server_profile_template_uri': 'fake_spt_uri', + 'applied_server_profile_uri': None, } self.assertEqual( @@ -172,6 +172,20 @@ class OneViewCommonTestCase(db_base.DbTestCase): common.get_oneview_info(incomplete_node) ) + def test_get_oneview_info_malformed_capabilities(self): + driver_info = db_utils.get_test_oneview_driver_info() + + del driver_info["server_hardware_uri"] + properties = db_utils.get_test_oneview_properties() + properties["capabilities"] = "anything,000" + + self.node.driver_info = driver_info + self.node.properties = properties + + self.assertRaises(exception.OneViewInvalidNodeParameter, + common.get_oneview_info, + self.node) + # TODO(gabriel-bezerra): Remove this after Mitaka @mock.patch.object(common, 'LOG', autospec=True) def test_deprecated_spt_in_driver_info(self, log_mock): @@ -194,6 +208,7 @@ class OneViewCommonTestCase(db_base.DbTestCase): 'server_hardware_type_uri': 'fake_sht_uri', 'enclosure_group_uri': 'fake_eg_uri', 'server_profile_template_uri': 'fake_spt_uri', + 'applied_server_profile_uri': None, } self.assertEqual( @@ -226,6 +241,7 @@ class OneViewCommonTestCase(db_base.DbTestCase): 'server_hardware_type_uri': 'fake_sht_uri', 'enclosure_group_uri': 'fake_eg_uri', 'server_profile_template_uri': 'fake_spt_uri', + 'applied_server_profile_uri': None, } self.assertEqual( @@ -281,8 +297,9 @@ class OneViewCommonTestCase(db_base.DbTestCase): @mock.patch.object(common, 'get_oneview_client', spec_set=True, autospec=True) - def test_validate_oneview_resources_compatibility(self, - mock_get_ov_client): + 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) @@ -290,12 +307,123 @@ class OneViewCommonTestCase(db_base.DbTestCase): oneview_client.validate_node_server_hardware.called) self.assertTrue( oneview_client.validate_node_server_hardware_type.called) + self.assertTrue( + oneview_client.validate_node_enclosure_group.called) + self.assertTrue( + oneview_client.validate_node_server_profile_template.called) self.assertTrue( oneview_client.check_server_profile_is_applied.called) self.assertTrue( - oneview_client.is_node_port_mac_compatible_with_server_profile. - called) + oneview_client. + is_node_port_mac_compatible_with_server_profile.called) + self.assertFalse( + oneview_client. + is_node_port_mac_compatible_with_server_hardware.called) + self.assertFalse( + oneview_client.validate_spt_primary_boot_connection.called) + + @mock.patch.object(common, 'get_oneview_client', spec_set=True, + autospec=True) + def test_validate_oneview_resources_compatibility_dynamic_allocation( + self, mock_get_ov_client + ): + """Validate compatibility of resources for Dynamic Allocation model. + + 1) Set 'dynamic_allocation' flag as True on node's driver_info + 2) Check validate_node_server_hardware method is called + 3) Check validate_node_server_hardware_type method is called + 4) Check validate_node_enclosure_group method is called + 5) Check validate_node_server_profile_template method is called + 6) Check is_node_port_mac_compatible_with_server_hardware method + is called + 7) Check validate_node_server_profile_template method is called + 8) Check check_server_profile_is_applied method is not called + 9) Check is_node_port_mac_compatible_with_server_profile method is + not called + + """ + oneview_client = mock_get_ov_client() + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['dynamic_allocation'] = True + task.node.driver_info = driver_info + + 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.validate_node_enclosure_group.called) self.assertTrue( oneview_client.validate_node_server_profile_template.called) + self.assertTrue( + oneview_client. + is_node_port_mac_compatible_with_server_hardware.called) + self.assertTrue( + oneview_client.validate_node_server_profile_template.called) + self.assertFalse( + oneview_client.check_server_profile_is_applied.called) + self.assertFalse( + oneview_client. + is_node_port_mac_compatible_with_server_profile.called) + + def test_is_dynamic_allocation_enabled(self): + """Ensure Dynamic Allocation is enabled when flag is True. + + 1) Set 'dynamic_allocation' flag as True on node's driver_info + 2) Check Dynamic Allocation is enabled for the given node + + """ + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['dynamic_allocation'] = True + task.node.driver_info = driver_info + + self.assertTrue( + common.is_dynamic_allocation_enabled(task.node) + ) + + def test_is_dynamic_allocation_enabled_false(self): + """Ensure Dynamic Allocation is disabled when flag is False. + + 1) Set 'dynamic_allocation' flag as False on node's driver_info + 2) Check Dynamic Allocation is disabled for the given node + + """ + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['dynamic_allocation'] = False + task.node.driver_info = driver_info + + self.assertFalse( + common.is_dynamic_allocation_enabled(task.node) + ) + + def test_is_dynamic_allocation_enabled_none(self): + """Ensure Dynamic Allocation is disabled when flag is None. + + 1) Set 'dynamic_allocation' flag as None on node's driver_info + 2) Check Dynamic Allocation is disabled for the given node + + """ + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['dynamic_allocation'] = None + task.node.driver_info = driver_info + + self.assertFalse( + common.is_dynamic_allocation_enabled(task.node) + ) + + def test_is_dynamic_allocation_enabled_without_flag(self): + """Ensure Dynamic Allocation is disabled when node doesnt't have flag. + + 1) Create a node without 'dynamic_allocation' flag + 2) Check Dynamic Allocation is disabled for the given node + + """ + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertFalse( + common.is_dynamic_allocation_enabled(task.node) + ) diff --git a/ironic/tests/unit/drivers/modules/oneview/test_deploy.py b/ironic/tests/unit/drivers/modules/oneview/test_deploy.py new file mode 100644 index 0000000000..e4df3b44b1 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/oneview/test_deploy.py @@ -0,0 +1,144 @@ +# Copyright 2016 Hewlett Packard Enterprise Development LP. +# Copyright 2016 Universidade Federal de Campina Grande +# 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 mock + +from oslo_utils import importutils + +from ironic.common import driver_factory +from ironic.drivers.modules.oneview import common +from ironic.drivers.modules.oneview import deploy +from ironic.drivers.modules.oneview import deploy_utils +from ironic import objects +from ironic.tests.unit.conductor import 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_models = importutils.try_import('oneview_client.models') + + +@mock.patch.object(common, 'get_oneview_client', spec_set=True, autospec=True) +class OneViewPeriodicTasks(db_base.DbTestCase): + + def setUp(self): + super(OneViewPeriodicTasks, 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(objects.Node, 'get') + @mock.patch.object(deploy_utils, 'is_node_in_use_by_oneview') + def test__periodic_check_nodes_taken_by_oneview( + self, mock_is_node_in_use_by_oneview, mock_get_node, + mock_get_ov_client + ): + + manager = mock.MagicMock( + spec=['iter_nodes', 'update_node', 'do_provisioning_action'] + ) + + manager.iter_nodes.return_value = [ + (self.node.uuid, 'fake_oneview') + ] + + mock_get_node.return_value = self.node + mock_is_node_in_use_by_oneview.return_value = True + + class OneViewDriverDeploy(deploy.OneViewPeriodicTasks): + oneview_driver = 'fake_oneview' + + oneview_driver_deploy = OneViewDriverDeploy() + oneview_driver_deploy._periodic_check_nodes_taken_by_oneview( + manager, self.context + ) + self.assertTrue(manager.update_node.called) + self.assertTrue(manager.do_provisioning_action.called) + self.assertTrue(self.node.maintenance) + self.assertEqual(common.NODE_IN_USE_BY_ONEVIEW, + self.node.maintenance_reason) + + @mock.patch.object(deploy_utils, 'is_node_in_use_by_oneview') + def test__periodic_check_nodes_freed_by_oneview( + self, mock_is_node_in_use_by_oneview, mock_get_ov_client + ): + + manager = mock.MagicMock( + spec=['iter_nodes', 'update_node', 'do_provisioning_action'] + ) + + manager.iter_nodes.return_value = [ + (self.node.uuid, 'fake_oneview', + common.NODE_IN_USE_BY_ONEVIEW) + ] + + mock_is_node_in_use_by_oneview.return_value = False + + class OneViewDriverDeploy(deploy.OneViewPeriodicTasks): + oneview_driver = 'fake_oneview' + + oneview_driver_deploy = OneViewDriverDeploy() + oneview_driver_deploy._periodic_check_nodes_freed_by_oneview( + manager, self.context + ) + self.assertTrue(manager.update_node.called) + self.assertTrue(manager.do_provisioning_action.called) + self.assertFalse(self.node.maintenance) + self.assertIsNone(self.node.maintenance_reason) + + @mock.patch.object(objects.Node, 'get') + def test__periodic_check_nodes_taken_on_cleanfail( + self, mock_get_node, mock_get_ov_client + ): + + driver_internal_info = { + 'oneview_error': common.SERVER_HARDWARE_ALLOCATION_ERROR + } + + manager = mock.MagicMock( + spec=['iter_nodes', 'update_node', 'do_provisioning_action'] + ) + + manager.iter_nodes.return_value = [ + (self.node.uuid, 'fake_oneview', driver_internal_info) + ] + + self.node.driver_internal_info = driver_internal_info + mock_get_node.return_value = self.node + + class OneViewDriverDeploy(deploy.OneViewPeriodicTasks): + oneview_driver = 'fake_oneview' + + oneview_driver_deploy = OneViewDriverDeploy() + oneview_driver_deploy._periodic_check_nodes_taken_on_cleanfail( + manager, self.context + ) + self.assertTrue(manager.update_node.called) + self.assertTrue(manager.do_provisioning_action.called) + self.assertTrue(self.node.maintenance) + self.assertEqual(common.NODE_IN_USE_BY_ONEVIEW, + self.node.maintenance_reason) + self.assertDictEqual({}, self.node.driver_internal_info) diff --git a/ironic/tests/unit/drivers/modules/oneview/test_deploy_utils.py b/ironic/tests/unit/drivers/modules/oneview/test_deploy_utils.py new file mode 100644 index 0000000000..695ed7fb15 --- /dev/null +++ b/ironic/tests/unit/drivers/modules/oneview/test_deploy_utils.py @@ -0,0 +1,349 @@ +# Copyright 2016 Hewlett Packard Enterprise Development LP. +# Copyright 2016 Universidade Federal de Campina Grande +# 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 mock + +from oslo_utils import importutils + +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.drivers.modules.oneview import deploy_utils +from ironic import objects +from ironic.tests.unit.conductor import 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_models = importutils.try_import('oneview_client.models') + + +@mock.patch.object(common, 'get_oneview_client', spec_set=True, autospec=True) +class OneViewDeployUtilsTestCase(db_base.DbTestCase): + + def setUp(self): + super(OneViewDeployUtilsTestCase, 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) + + # Tests for prepare + def test_prepare_node_is_in_use_by_oneview(self, mock_get_ov_client): + """`prepare` behavior when the node already has a Profile on OneView. + + """ + oneview_client = mock_get_ov_client() + fake_server_hardware = oneview_models.ServerHardware() + fake_server_hardware.server_profile_uri = "/any/sp_uri" + oneview_client.get_server_hardware.return_value = fake_server_hardware + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['dynamic_allocation'] = True + task.node.driver_info = driver_info + task.node.provision_state = states.DEPLOYING + self.assertRaises( + exception.InstanceDeployFailure, + deploy_utils.prepare, + task + ) + + @mock.patch.object(objects.Node, 'save') + def test_prepare_node_is_successfuly_allocated_to_ironic( + self, mock_node_save, mock_get_ov_client + ): + """`prepare` behavior when the node is free from OneView standpoint. + + """ + ov_client = mock_get_ov_client() + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = None + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + with task_manager.acquire(self.context, self.node.uuid) as task: + task.node.provision_state = states.DEPLOYING + deploy_utils.prepare(task) + self.assertTrue(ov_client.clone_template_and_apply.called) + self.assertTrue(ov_client.get_server_profile_from_hardware) + + # Tests for tear_down + def test_tear_down(self, mock_get_ov_client): + """`tear_down` behavior when node already has Profile applied + + """ + ov_client = mock_get_ov_client() + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['applied_server_profile_uri'] = \ + '/rest/server-profiles/1234556789' + task.node.driver_info = driver_info + + self.assertTrue( + 'applied_server_profile_uri' in task.node.driver_info + ) + deploy_utils.tear_down(task) + self.assertFalse( + 'applied_server_profile_uri' in task.node.driver_info + ) + self.assertTrue( + ov_client.delete_server_profile.called + ) + + # Tests for prepare_cleaning + @mock.patch.object(objects.Node, 'save') + def test_prepare_cleaning_when_node_does_not_have_sp_applied( + self, mock_node_save, mock_get_ov_client + ): + """`prepare_cleaning` behavior when node is free + + """ + ov_client = mock_get_ov_client() + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = None + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + with task_manager.acquire(self.context, self.node.uuid) as task: + deploy_utils.prepare_cleaning(task) + self.assertTrue(ov_client.clone_template_and_apply.called) + + @mock.patch.object(objects.Node, 'save') + def test_prepare_cleaning_when_node_has_sp_applied( + self, mock_node_save, mock_get_ov_client + ): + """`prepare_cleaning` behavior when node already has Profile applied + + """ + ov_client = mock_get_ov_client() + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = 'same/sp_applied' + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['applied_server_profile_uri'] = 'same/sp_applied' + task.node.driver_info = driver_info + + deploy_utils.prepare_cleaning(task) + self.assertFalse(ov_client.clone_template_and_apply.called) + + def test_prepare_cleaning_node_is_in_use_by_oneview( + self, mock_get_ov_client + ): + """`prepare_cleaning` behavior when node has Server Profile on OneView + + """ + oneview_client = mock_get_ov_client() + fake_server_hardware = oneview_models.ServerHardware() + fake_server_hardware.server_profile_uri = "/any/sp_uri" + oneview_client.get_server_hardware.return_value = fake_server_hardware + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['dynamic_allocation'] = True + task.node.driver_info = driver_info + task.node.provision_state = states.DEPLOYING + self.assertRaises( + exception.NodeCleaningFailure, + deploy_utils.prepare_cleaning, + task + ) + + # Tests for tear_down_cleaning + def test_tear_down_cleaning(self, mock_get_ov_client): + """Checks if Server Profile was deleted and its uri removed + + """ + ov_client = mock_get_ov_client() + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['applied_server_profile_uri'] = \ + '/rest/server-profiles/1234556789' + task.node.driver_info = driver_info + + self.assertIn('applied_server_profile_uri', task.node.driver_info) + deploy_utils.tear_down_cleaning(task) + self.assertNotIn('applied_server_profile_uri', + task.node.driver_info) + self.assertTrue(ov_client.delete_server_profile.called) + + # Tests for is_node_in_use_by_oneview + def test_is_node_in_use_by_oneview(self, mock_get_ov_client): + """Node has a Server Profile applied by a third party user. + + """ + fake_server_hardware = oneview_models.ServerHardware() + fake_server_hardware.server_profile_uri = "/any/sp_uri" + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['dynamic_allocation'] = True + task.node.driver_info = driver_info + self.assertTrue( + deploy_utils.is_node_in_use_by_oneview(task.node) + ) + + def test_is_node_in_use_by_oneview_no_server_profile( + self, mock_get_ov_client + ): + """Node has no Server Profile. + + """ + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = None + + ov_client = mock_get_ov_client.return_value + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertFalse( + deploy_utils.is_node_in_use_by_oneview(task.node) + ) + + def test_is_node_in_use_by_oneview_same_server_profile_applied( + self, mock_get_ov_client + ): + """Node's Server Profile uri is the same applied by ironic. + + """ + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = 'same/applied_sp_uri/' + + ov_client = mock_get_ov_client.return_value + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['applied_server_profile_uri'] = 'same/applied_sp_uri/' + task.node.driver_info = driver_info + self.assertFalse( + deploy_utils.is_node_in_use_by_oneview(task.node) + ) + + # Tests for _add_applied_server_profile_uri_field + def test__add_applied_server_profile_uri_field(self, mock_get_ov_client): + """Checks if applied_server_profile_uri was added to driver_info. + + """ + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + task.node.driver_info = driver_info + fake_server_profile = oneview_models.ServerProfile() + fake_server_profile.uri = 'any/applied_sp_uri/' + + self.assertNotIn('applied_server_profile_uri', + task.node.driver_info) + deploy_utils._add_applied_server_profile_uri_field( + task.node, + fake_server_profile + ) + self.assertIn('applied_server_profile_uri', task.node.driver_info) + + # Tests for _del_applied_server_profile_uri_field + def test__del_applied_server_profile_uri_field(self, mock_get_ov_client): + """Checks if applied_server_profile_uri was removed from driver_info. + + """ + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['applied_server_profile_uri'] = 'any/applied_sp_uri/' + task.node.driver_info = driver_info + + self.assertIn('applied_server_profile_uri', task.node.driver_info) + deploy_utils._del_applied_server_profile_uri_field(task.node) + self.assertNotIn('applied_server_profile_uri', + task.node.driver_info) + + # Tests for _allocate_server_hardware_to_ironic + @mock.patch.object(objects.Node, 'save') + def test__allocate_server_hardware_to_ironic( + self, mock_node_save, mock_get_ov_client + ): + """Checks if a Server Profile was created and its uri is in driver_info. + + """ + ov_client = mock_get_ov_client.return_value + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = None + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + mock_get_ov_client.return_value = ov_client + + with task_manager.acquire(self.context, self.node.uuid) as task: + deploy_utils._allocate_server_hardware_to_ironic( + task.node, 'serverProfileName' + ) + self.assertTrue(ov_client.clone_template_and_apply.called) + self.assertIn('applied_server_profile_uri', task.node.driver_info) + + @mock.patch.object(objects.Node, 'save') + @mock.patch.object(deploy_utils, + '_del_applied_server_profile_uri_field') + def test__allocate_server_hardware_to_ironic_node_has_server_profile( + self, mock_delete_applied_sp, mock_node_save, mock_get_ov_client + ): + """Tests server profile allocation when applied_server_profile_uri exists. + + This test consider that no Server Profile is applied on the Server + Hardware but the applied_server_profile_uri remained on the node. Thus, + the conductor should remove the value and apply a new server profile to + use the node. + """ + ov_client = mock_get_ov_client.return_value + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = None + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + mock_get_ov_client.return_value = ov_client + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['applied_server_profile_uri'] = 'any/applied_sp_uri/' + task.node.driver_info = driver_info + + deploy_utils._allocate_server_hardware_to_ironic( + task.node, 'serverProfileName' + ) + self.assertTrue(mock_delete_applied_sp.called) + + # Tests for _deallocate_server_hardware_from_ironic + @mock.patch.object(objects.Node, 'save') + def test__deallocate_server_hardware_from_ironic( + self, mock_node_save, mock_get_ov_client + ): + ov_client = mock_get_ov_client.return_value + fake_sh = oneview_models.ServerHardware() + fake_sh.server_profile_uri = 'any/applied_sp_uri/' + ov_client.get_server_hardware_by_uuid.return_value = fake_sh + mock_get_ov_client.return_value = ov_client + + with task_manager.acquire(self.context, self.node.uuid) as task: + driver_info = task.node.driver_info + driver_info['applied_server_profile_uri'] = 'any/applied_sp_uri/' + task.node.driver_info = driver_info + + deploy_utils._deallocate_server_hardware_from_ironic(task.node) + self.assertTrue(ov_client.delete_server_profile.called) + self.assertTrue( + 'applied_server_profile_uri' not in task.node.driver_info + ) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index 1d2b6cea0d..d84f9e895a 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -126,6 +126,8 @@ ONEVIEWCLIENT_SPEC = ( 'client', 'states', 'exceptions', + 'models', + 'utils', ) ONEVIEWCLIENT_CLIENT_CLS_SPEC = ( diff --git a/ironic/tests/unit/drivers/third_party_driver_mocks.py b/ironic/tests/unit/drivers/third_party_driver_mocks.py index 065dd67b4b..8fb57c971b 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mocks.py +++ b/ironic/tests/unit/drivers/third_party_driver_mocks.py @@ -126,8 +126,10 @@ if not oneview_client: ONEVIEW_ERROR='error') sys.modules['oneview_client.states'] = states sys.modules['oneview_client.exceptions'] = oneview_client.exceptions + sys.modules['oneview_client.utils'] = oneview_client.utils oneview_client.exceptions.OneViewException = type('OneViewException', (Exception,), {}) + sys.modules['oneview_client.models'] = oneview_client.models if 'ironic.drivers.oneview' in sys.modules: six.moves.reload_module(sys.modules['ironic.drivers.modules.oneview']) diff --git a/releasenotes/notes/add-dynamic-allocation-feature-2fd6b4df7943f178.yaml b/releasenotes/notes/add-dynamic-allocation-feature-2fd6b4df7943f178.yaml new file mode 100644 index 0000000000..119a155931 --- /dev/null +++ b/releasenotes/notes/add-dynamic-allocation-feature-2fd6b4df7943f178.yaml @@ -0,0 +1,5 @@ +--- +features: + - Add Dynamic Allocation feature for the OneView drivers. +deprecations: + - Deprecates pre-allocation feature for the OneView drivers.