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.