From 8a0046c784ae3255daf06747d840e88679062b03 Mon Sep 17 00:00:00 2001 From: WosunOoO Date: Fri, 10 Feb 2017 16:11:05 +0800 Subject: [PATCH] Add engine driver framework base driver. Add BaseEngineDriver class, copy current ironic operations to IronicDriver, modify mananger and base manger to separate driver interface and manger. Co-Authored-By: Zhenguo Niu Change-Id: I1f503f925f24975a20ec5892b4aa107ae194fbca Implements: blueprint engine-driver-framework --- .gitignore | 1 + mogan/api/controllers/v1/instances.py | 7 +- mogan/common/states.py | 11 + mogan/common/utils.py | 7 + mogan/conf/engine.py | 4 + mogan/engine/baremetal/cloudboot/__init__.py | 0 mogan/engine/baremetal/driver.py | 198 +++++++++ mogan/engine/baremetal/ironic.py | 197 --------- mogan/engine/baremetal/ironic/__init__.py | 18 + mogan/engine/baremetal/ironic/driver.py | 389 ++++++++++++++++++ .../baremetal/{ => ironic}/ironic_states.py | 0 mogan/engine/base_manager.py | 4 +- mogan/engine/flows/create_instance.py | 54 ++- mogan/engine/manager.py | 186 ++------- .../engine/flows/test_create_instance_flow.py | 23 +- mogan/tests/unit/engine/test_manager.py | 65 +-- 16 files changed, 714 insertions(+), 450 deletions(-) create mode 100644 mogan/engine/baremetal/cloudboot/__init__.py create mode 100644 mogan/engine/baremetal/driver.py delete mode 100644 mogan/engine/baremetal/ironic.py create mode 100644 mogan/engine/baremetal/ironic/__init__.py create mode 100644 mogan/engine/baremetal/ironic/driver.py rename mogan/engine/baremetal/{ => ironic}/ironic_states.py (100%) diff --git a/.gitignore b/.gitignore index 963e589a..56e048c9 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ nosetests.xml *.mo # Mr Developer +.idea .mr.developer.cfg .project .pydevproject diff --git a/mogan/api/controllers/v1/instances.py b/mogan/api/controllers/v1/instances.py index 2ddc9676..f2c2e520 100644 --- a/mogan/api/controllers/v1/instances.py +++ b/mogan/api/controllers/v1/instances.py @@ -36,7 +36,6 @@ from mogan.common.i18n import _ from mogan.common.i18n import _LW from mogan.common import policy from mogan.common import states -from mogan.engine.baremetal import ironic_states as ir_states from mogan import network from mogan import objects @@ -60,7 +59,7 @@ class InstanceStates(base.APIBase): @classmethod def sample(cls): - sample = cls(power_state=ir_states.POWER_ON, + sample = cls(power_state=states.POWER_ON, status=states.ACTIVE, locked=False) return sample @@ -172,13 +171,13 @@ class InstanceStatesController(InstanceControllerBase): """ # Currently we only support rebuild target - if target not in (ir_states.REBUILD,): + if target not in (states.REBUILD,): raise exception.InvalidActionParameterValue( value=target, action="provision", instance=instance_uuid) rpc_instance = self._resource or self._get_resource(instance_uuid) - if target == ir_states.REBUILD: + if target == states.REBUILD: try: pecan.request.engine_api.rebuild(pecan.request.context, rpc_instance) diff --git a/mogan/common/states.py b/mogan/common/states.py index 6af4865a..d9e940d9 100644 --- a/mogan/common/states.py +++ b/mogan/common/states.py @@ -49,6 +49,17 @@ POWER_ACTION_MAP = { } +##################### +# Provisioning states +##################### + +REBUILD = 'rebuild' +""" Node is to be rebuilt. +This is not used as a state, but rather as a "verb" when changing the node's +provision_state via the REST API. +""" + + ################# # Instance states ################# diff --git a/mogan/common/utils.py b/mogan/common/utils.py index 3b4257ce..95c1a89a 100644 --- a/mogan/common/utils.py +++ b/mogan/common/utils.py @@ -87,3 +87,10 @@ def make_pretty_name(method): except AttributeError: pass return ".".join(meth_pieces) + + +def check_isinstance(obj, cls): + """Checks that obj is of type cls, and lets PyLint infer types.""" + if isinstance(obj, cls): + return obj + raise Exception(_('Expected object of type: %s') % (str(cls))) diff --git a/mogan/conf/engine.py b/mogan/conf/engine.py index a181bc02..482aeecd 100644 --- a/mogan/conf/engine.py +++ b/mogan/conf/engine.py @@ -57,6 +57,10 @@ opts = [ default=600, help=_("Interval to sync maintenance states between the " "database and Ironic, in seconds.")), + cfg.StrOpt('engine_driver', + default='ironic.IronicDriver', + choices=['ironic.IronicDriver'], + help=_("Which driver to use, default to ironic driver.")) ] diff --git a/mogan/engine/baremetal/cloudboot/__init__.py b/mogan/engine/baremetal/cloudboot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mogan/engine/baremetal/driver.py b/mogan/engine/baremetal/driver.py new file mode 100644 index 00000000..4a11463a --- /dev/null +++ b/mogan/engine/baremetal/driver.py @@ -0,0 +1,198 @@ +# Copyright 2017 Hengfeng Bank Co.,Ltd. +# 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. + +"""Base engine driver functionality.""" + +import sys + +from oslo_log import log as logging +from oslo_utils import importutils + +from mogan.common.i18n import _LE +from mogan.common.i18n import _LI +from mogan.common import utils + +LOG = logging.getLogger(__name__) + + +class BaseEngineDriver(object): + + """Base class for mogan baremetal drivers. + """ + def __init__(self): + """Add init staff here. + """ + + def init_host(self, host): + """Initialize anything that is necessary for the engine driver to + function. + """ + + def get_available_node_list(self): + """Return all available nodes. + + """ + raise NotImplementedError() + + def get_maintenance_node_list(self): + """Return maintenance nodes. + + """ + raise NotImplementedError() + + def get_nodes_power_state(self): + """Return nodes power state. + + """ + raise NotImplementedError() + + def get_port_list(self): + """Return all ports. + """ + raise NotImplementedError() + + def get_portgroup_list(self): + """Return all portgroups. + """ + raise NotImplementedError() + + def get_node_by_instance(self, instance_uuid): + """Return node info associated with certain instance. + + :param instance_uuid: uuid of mogan instance to get node. + """ + raise NotImplementedError() + + def get_power_state(self, instance_uuid): + """Return a node's power state by passing instance uuid. + + :param instance_uuid: mogan instance uuid to get power state. + """ + raise NotImplementedError() + + def set_power_state(self, node_uuid, state): + """Set a node's power state. + + :param node_uuid: node id to change power state. + :param state: mogan states to change to. + """ + raise NotImplementedError() + + def get_ports_from_node(self, node_uuid, detail=True): + """Get a node's ports info. + + :param node_uuid: node id to get ports info. + :param detail: whether to get detailed info of all the ports, + default to False. + """ + raise NotImplementedError() + + def plug_vif(self, node_interface, neutron_port_id): + """Plug a neutron port to a baremetal port. + + :param node_interface: bare metal interface to plug neutron port. + :param neutron_port_id: neutron port id to plug. + """ + raise NotImplementedError() + + def unplug_vif(self, node_interface): + """Unplug a neutron port from a baremetal port. + + :param node_interface: bare metal interface id to unplug port. + """ + raise NotImplementedError() + + def set_instance_info(self, instance, node): + """Associate the node with an instance. + + :param instance: mogan instance object. + :param node: node object. + """ + raise NotImplementedError() + + def unset_instance_info(self, instance): + """Disassociate the node with an instance. + + :param instance: mogan instance object. + """ + raise NotImplementedError() + + def do_node_deploy(self, instance): + """Trigger node deploy process. + + :param instance: instance to deploy. + """ + raise NotImplementedError() + + def get_node(self, node_uuid): + """Get node info by node id. + + :param node_uuid: node id to get info. + """ + raise NotImplementedError() + + def destroy(self, instance): + """Trigger node destroy process. + + :param instance: the instance to destory. + """ + raise NotImplementedError() + + def validate_node(self, node_uuid): + """Validate whether the node's driver has enough information to + manage the Node. + + :param node_uuid: node id to validate. + """ + raise NotImplementedError() + + def is_node_unprovision(self, node): + """Validate whether the node is in unprovision state. + + :param node: node object to get state. + """ + raise NotImplementedError() + + def do_node_rebuild(self, instance): + """Trigger node deploy process. + + :param instance: instance to rebuild. + """ + raise NotImplementedError() + + +def load_engine_driver(engine_driver): + """Load a engine driver module. + + Load the engine driver module specified by the engine_driver + configuration option or, if supplied, the driver name supplied as an + argument. + + :param engine_driver: a engine driver name to override the config opt + :returns: a EngineDriver instance + """ + + if not engine_driver: + LOG.error(_LE("Engine driver option required, but not specified")) + sys.exit(1) + + LOG.info(_LI("Loading engine driver '%s'"), engine_driver) + try: + driver = importutils.import_object( + 'mogan.engine.baremetal.%s' % engine_driver) + return utils.check_isinstance(driver, BaseEngineDriver) + except ImportError: + LOG.exception(_LE("Unable to load the baremetal driver")) + sys.exit(1) diff --git a/mogan/engine/baremetal/ironic.py b/mogan/engine/baremetal/ironic.py deleted file mode 100644 index 601461b9..00000000 --- a/mogan/engine/baremetal/ironic.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright 2016 Huawei Technologies Co.,LTD. -# 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 ironicclient import exceptions as client_e -from oslo_log import log as logging - -from mogan.common.i18n import _LE -from mogan.common.i18n import _LW -from mogan.common import states -from mogan.engine.baremetal import ironic_states - -LOG = logging.getLogger(__name__) - -_NODE_FIELDS = ('uuid', 'power_state', 'target_power_state', 'provision_state', - 'target_provision_state', 'last_error', 'maintenance', - 'properties', 'instance_uuid') - -_POWER_STATE_MAP = { - ironic_states.POWER_ON: states.POWER_ON, - ironic_states.NOSTATE: states.NOSTATE, - ironic_states.POWER_OFF: states.POWER_OFF, -} - - -def map_power_state(state): - try: - return _POWER_STATE_MAP[state] - except KeyError: - LOG.warning(_LW("Power state %s not found."), state) - return states.NOSTATE - - -def get_power_state(ironicclient, instance_uuid): - try: - node = ironicclient.call('node.get_by_instance_uuid', - instance_uuid, fields=('power_state',)) - return map_power_state(node.power_state) - except client_e.NotFound: - return map_power_state(ironic_states.NOSTATE) - - -def get_ports_from_node(ironicclient, node_uuid, detail=False): - """List the MAC addresses and the port types from a node.""" - ports = ironicclient.call("node.list_ports", node_uuid, detail=detail) - portgroups = ironicclient.call("portgroup.list", node=node_uuid, - detail=detail) - return ports + portgroups - - -def plug_vif(ironicclient, ironic_port_id, port_id): - patch = [{'op': 'add', - 'path': '/extra/vif_port_id', - 'value': port_id}] - ironicclient.call("port.update", ironic_port_id, patch) - - -def unplug_vif(ironicclient, ironic_port_id): - patch = [{'op': 'remove', - 'path': '/extra/vif_port_id'}] - try: - ironicclient.call("port.update", ironic_port_id, patch) - except client_e.BadRequest: - pass - - -def set_instance_info(ironicclient, instance, node): - - patch = [] - # Associate the node with an instance - patch.append({'path': '/instance_uuid', 'op': 'add', - 'value': instance.uuid}) - # Add the required fields to deploy a node. - patch.append({'path': '/instance_info/image_source', 'op': 'add', - 'value': instance.image_uuid}) - # TODO(zhenguo) Add partition support - patch.append({'path': '/instance_info/root_gb', 'op': 'add', - 'value': str(node.properties.get('local_gb', 0))}) - - ironicclient.call("node.update", instance.node_uuid, patch) - - -def unset_instance_info(ironicclient, instance): - - patch = [{'path': '/instance_info', 'op': 'remove'}, - {'path': '/instance_uuid', 'op': 'remove'}] - - ironicclient.call("node.update", instance.node_uuid, patch) - - -def do_node_deploy(ironicclient, node_uuid): - # trigger the node deploy - ironicclient.call("node.set_provision_state", node_uuid, - ironic_states.ACTIVE) - - -def do_node_rebuild(ironicclient, node_uuid): - # trigger the node rebuild - ironicclient.call("node.set_provision_state", node_uuid, - ironic_states.REBUILD) - - -def get_node_by_instance(ironicclient, instance_uuid, fields=None): - if fields is None: - fields = _NODE_FIELDS - return ironicclient.call('node.get_by_instance_uuid', - instance_uuid, fields=fields) - - -def get_node(ironicclient, node_uuid, fields=None): - if fields is None: - fields = _NODE_FIELDS - """Get a node by its UUID.""" - return ironicclient.call('node.get', node_uuid, fields=fields) - - -def destroy_node(ironicclient, node_uuid): - # trigger the node destroy - ironicclient.call("node.set_provision_state", node_uuid, - ironic_states.DELETED) - - -def validate_node(ironicclient, node_uuid): - return ironicclient.call("node.validate", node_uuid) - - -def get_node_list(ironicclient, **kwargs): - """Helper function to return the list of nodes. - - If unable to connect ironic server, an empty list is returned. - - :returns: a list of raw node from ironic - - """ - try: - node_list = ironicclient.call("node.list", **kwargs) - except client_e.ClientException as e: - LOG.exception(_LE("Could not get nodes from ironic. Reason: " - "%(detail)s"), {'detail': e.message}) - node_list = [] - return node_list - - -def get_port_list(ironicclient, **kwargs): - """Helper function to return the list of ports. - - If unable to connect ironic server, an empty list is returned. - - :returns: a list of raw port from ironic - - """ - try: - port_list = ironicclient.call("port.list", **kwargs) - except client_e.ClientException as e: - LOG.exception(_LE("Could not get ports from ironic. Reason: " - "%(detail)s"), {'detail': e.message}) - port_list = [] - return port_list - - -def get_portgroup_list(ironicclient, **kwargs): - """Helper function to return the list of portgroups. - - If unable to connect ironic server, an empty list is returned. - - :returns: a list of raw port from ironic - - """ - try: - portgroup_list = ironicclient.call("portgroup.list", **kwargs) - except client_e.ClientException as e: - LOG.exception(_LE("Could not get portgroups from ironic. Reason: " - "%(detail)s"), {'detail': e.message}) - portgroup_list = [] - return portgroup_list - - -def set_power_state(ironicclient, node_uuid, state): - if state == "soft_off": - ironicclient.call("node.set_power_state", - node_uuid, "off", soft=True) - elif state == "soft_reboot": - ironicclient.call("node.set_power_state", - node_uuid, "reboot", soft=True) - else: - ironicclient.call("node.set_power_state", node_uuid, state) diff --git a/mogan/engine/baremetal/ironic/__init__.py b/mogan/engine/baremetal/ironic/__init__.py new file mode 100644 index 00000000..0676b955 --- /dev/null +++ b/mogan/engine/baremetal/ironic/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# 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 mogan.engine.baremetal.ironic import driver + +IronicDriver = driver.IronicDriver diff --git a/mogan/engine/baremetal/ironic/driver.py b/mogan/engine/baremetal/ironic/driver.py new file mode 100644 index 00000000..cecae24b --- /dev/null +++ b/mogan/engine/baremetal/ironic/driver.py @@ -0,0 +1,389 @@ +# Copyright 2016 Huawei Technologies Co.,LTD. +# 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 ironicclient import exc as ironic_exc +from ironicclient import exceptions as client_e +from oslo_log import log as logging +from oslo_service import loopingcall +import six + +from mogan.common import exception +from mogan.common.i18n import _ +from mogan.common.i18n import _LE +from mogan.common.i18n import _LI +from mogan.common.i18n import _LW +from mogan.common import ironic +from mogan.common import states +from mogan.conf import CONF +from mogan.engine.baremetal import driver as base_driver +from mogan.engine.baremetal.ironic import ironic_states + +LOG = logging.getLogger(__name__) + +_POWER_STATE_MAP = { + ironic_states.POWER_ON: states.POWER_ON, + ironic_states.NOSTATE: states.NOSTATE, + ironic_states.POWER_OFF: states.POWER_OFF, +} + +_UNPROVISION_STATES = (ironic_states.ACTIVE, ironic_states.DEPLOYFAIL, + ironic_states.ERROR, ironic_states.DEPLOYWAIT, + ironic_states.DEPLOYING) + +_NODE_FIELDS = ('uuid', 'power_state', 'target_power_state', 'provision_state', + 'target_provision_state', 'last_error', 'maintenance', + 'properties', 'instance_uuid') + + +class IronicDriver(base_driver.BaseEngineDriver): + + def __init__(self): + super(IronicDriver, self).__init__() + self.ironicclient = ironic.IronicClientWrapper() + + def map_power_state(self, state): + try: + return _POWER_STATE_MAP[state] + except KeyError: + LOG.warning(_LW("Power state %s not found."), state) + return states.NOSTATE + + def get_power_state(self, instance_uuid): + try: + node = self.ironicclient.call('node.get_by_instance_uuid', + instance_uuid, + fields=('power_state',)) + return self.map_power_state(node.power_state) + except client_e.NotFound: + return self.map_power_state(ironic_states.NOSTATE) + + def get_ports_from_node(self, node_uuid, detail=True): + """List the MAC addresses and the port types from a node.""" + ports = self.ironicclient.call("node.list_ports", + node_uuid, detail=detail) + portgroups = self.ironicclient.call("portgroup.list", node=node_uuid, + detail=detail) + return ports + portgroups + + def plug_vif(self, ironic_port_id, port_id): + patch = [{'op': 'add', + 'path': '/extra/vif_port_id', + 'value': port_id}] + self.ironicclient.call("port.update", ironic_port_id, patch) + + def unplug_vif(self, node_interface): + patch = [{'op': 'remove', + 'path': '/extra/vif_port_id'}] + try: + if 'vif_port_id' in node_interface.extra: + self.ironicclient.call("port.update", + node_interface.uuid, patch) + except client_e.BadRequest: + pass + + def set_instance_info(self, instance, node): + + patch = list() + # Associate the node with an instance + patch.append({'path': '/instance_uuid', 'op': 'add', + 'value': instance.uuid}) + # Add the required fields to deploy a node. + patch.append({'path': '/instance_info/image_source', 'op': 'add', + 'value': instance.image_uuid}) + # TODO(zhenguo) Add partition support + patch.append({'path': '/instance_info/root_gb', 'op': 'add', + 'value': str(node.properties.get('local_gb', 0))}) + + self.ironicclient.call("node.update", instance.node_uuid, patch) + + def unset_instance_info(self, instance): + + patch = [{'path': '/instance_info', 'op': 'remove'}, + {'path': '/instance_uuid', 'op': 'remove'}] + try: + self.ironicclient.call("node.update", instance.node_uuid, patch) + except ironic_exc.BadRequest as e: + raise exception.Invalid(msg=six.text_type(e)) + + def do_node_deploy(self, instance): + # trigger the node deploy + self.ironicclient.call("node.set_provision_state", instance.node_uuid, + ironic_states.ACTIVE) + timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_active, + instance) + timer.start(interval=CONF.ironic.api_retry_interval).wait() + + def _wait_for_active(self, instance): + """Wait for the node to be marked as ACTIVE in Ironic.""" + instance.refresh() + if instance.status in (states.DELETING, states.ERROR, states.DELETED): + raise exception.InstanceDeployFailure( + _("Instance %s provisioning was aborted") % instance.uuid) + + node = self.get_node_by_instance(instance.uuid) + LOG.debug('Current ironic node state is %s', node.provision_state) + if node.provision_state == ironic_states.ACTIVE: + # job is done + LOG.debug("Ironic node %(node)s is now ACTIVE", + dict(node=node.uuid)) + raise loopingcall.LoopingCallDone() + + if node.target_provision_state in (ironic_states.DELETED, + ironic_states.AVAILABLE): + # ironic is trying to delete it now + raise exception.InstanceNotFound(instance_id=instance.uuid) + + if node.provision_state in (ironic_states.NOSTATE, + ironic_states.AVAILABLE): + # ironic already deleted it + raise exception.InstanceNotFound(instance_id=instance.uuid) + + if node.provision_state == ironic_states.DEPLOYFAIL: + # ironic failed to deploy + msg = (_("Failed to provision instance %(inst)s: %(reason)s") + % {'inst': instance.uuid, 'reason': node.last_error}) + raise exception.InstanceDeployFailure(msg) + + def get_node_by_instance(self, instance_uuid): + fields = _NODE_FIELDS + try: + return self.ironicclient.call('node.get_by_instance_uuid', + instance_uuid, fields=fields) + except ironic_exc.NotFound: + raise exception.NotFound + + def get_node(self, node_uuid, fields=None): + if fields is None: + fields = _NODE_FIELDS + """Get a node by its UUID.""" + return self.ironicclient.call('node.get', node_uuid, fields=fields) + + def destroy(self, instance): + node_uuid = instance.node_uuid + # trigger the node destroy + try: + self.ironicclient.call("node.set_provision_state", node_uuid, + ironic_states.DELETED) + except Exception as e: + # if the node is already in a deprovisioned state, continue + # This should be fixed in Ironic. + # TODO(deva): This exception should be added to + # python-ironicclient and matched directly, + # rather than via __name__. + if getattr(e, '__name__', None) != 'InstanceDeployFailure': + raise + + # using a dict because this is modified in the local method + data = {'tries': 0} + + def _wait_for_provision_state(): + + try: + node = self.get_node_by_instance(instance.uuid) + except exception.NotFound: + LOG.debug("Instance already removed from Ironic", + instance=instance) + raise loopingcall.LoopingCallDone() + LOG.debug('Current ironic node state is %s', node.provision_state) + if node.provision_state in (ironic_states.NOSTATE, + ironic_states.CLEANING, + ironic_states.CLEANWAIT, + ironic_states.CLEANFAIL, + ironic_states.AVAILABLE): + # From a user standpoint, the node is unprovisioned. If a node + # gets into CLEANFAIL state, it must be fixed in Ironic, but we + # can consider the instance unprovisioned. + LOG.debug("Ironic node %(node)s is in state %(state)s, " + "instance is now unprovisioned.", + dict(node=node.uuid, state=node.provision_state), + instance=instance) + raise loopingcall.LoopingCallDone() + + if data['tries'] >= CONF.ironic.api_max_retries + 1: + msg = (_("Error destroying the instance on node %(node)s. " + "Provision state still '%(state)s'.") + % {'state': node.provision_state, + 'node': node.uuid}) + LOG.error(msg) + raise exception.MoganException(msg) + else: + data['tries'] += 1 + + # wait for the state transition to finish + timer = loopingcall.FixedIntervalLoopingCall(_wait_for_provision_state) + timer.start(interval=CONF.ironic.api_retry_interval).wait() + + LOG.info(_LI('Successfully destroyed Ironic node %s'), node_uuid) + + def validate_node(self, node_uuid): + return self.ironicclient.call("node.validate", node_uuid) + + def get_available_node_list(self): + """Helper function to return the list of nodes. + + If unable to connect ironic server, an empty list is returned. + + :returns: a list of raw node from ironic + + """ + params = { + 'maintenance': False, + 'detail': True, + 'provision_state': ironic_states.AVAILABLE, + 'associated': False, + 'limit': 0 + } + try: + node_list = self.ironicclient.call("node.list", **params) + except client_e.ClientException as e: + LOG.exception(_LE("Could not get nodes from ironic. Reason: " + "%(detail)s"), {'detail': e.message}) + node_list = [] + return node_list + + def get_maintenance_node_list(self): + """Helper function to return the list of maintenance nodes. + + If unable to connect ironic server, an empty list is returned. + + :returns: a list of maintenance node from ironic + + """ + params = { + 'associated': True, + 'fields': ('instance_uuid', 'maintenance'), + 'limit': 0 + } + try: + node_list = self.ironicclient.call("node.list", **params) + except client_e.ClientException as e: + LOG.exception(_LE("Could not get nodes from ironic. Reason: " + "%(detail)s"), {'detail': e.message}) + node_list = [] + return node_list + + def get_node_power_states(self): + """Helper function to return the node power states. + + If unable to connect ironic server, an empty list is returned. + + :returns: a list of node power states from ironic + + """ + params = { + 'maintenance': False, + 'associated': True, + 'fields': ('instance_uuid', 'power_state', 'target_power_state'), + 'limit': 0 + } + try: + node_list = self.ironicclient.call("node.list", **params) + except client_e.ClientException as e: + LOG.exception(_LE("Could not get nodes from ironic. Reason: " + "%(detail)s"), {'detail': e.message}) + node_list = [] + return node_list + + def get_port_list(self): + """Helper function to return the list of ports. + + If unable to connect ironic server, an empty list is returned. + + :returns: a list of raw port from ironic + + """ + params = { + 'limit': 0, + 'fields': ('uuid', 'node_uuid', 'extra', 'address') + } + + try: + port_list = self.ironicclient.call("port.list", **params) + except client_e.ClientException as e: + LOG.exception(_LE("Could not get ports from ironic. Reason: " + "%(detail)s"), {'detail': e.message}) + port_list = [] + return port_list + + def get_portgroup_list(self, **kwargs): + """Helper function to return the list of portgroups. + + If unable to connect ironic server, an empty list is returned. + + :returns: a list of raw port from ironic + + """ + params = { + 'limit': 0, + 'fields': ('uuid', 'node_uuid', 'extra', 'address') + } + + try: + portgroup_list = self.ironicclient.call("portgroup.list", **params) + except client_e.ClientException as e: + LOG.exception(_LE("Could not get portgroups from ironic. Reason: " + "%(detail)s"), {'detail': e.message}) + portgroup_list = [] + return portgroup_list + + def set_power_state(self, instance, state): + if state == "soft_off": + self.ironicclient.call("node.set_power_state", + instance.node_uuid, "off", soft=True) + elif state == "soft_reboot": + self.ironicclient.call("node.set_power_state", + instance.node_uuid, "reboot", soft=True) + else: + self.ironicclient.call("node.set_power_state", + instance.node_uuid, state) + timer = loopingcall.FixedIntervalLoopingCall( + self._wait_for_power_state, instance) + timer.start(interval=CONF.ironic.api_retry_interval).wait() + + def is_node_unprovision(self, node): + return node.provision_state in _UNPROVISION_STATES + + def _wait_for_power_state(self, instance): + """Wait for the node to complete a power state change.""" + try: + node = self.get_node_by_instance(self.ironicclient, + instance.uuid) + except exception.NotFound: + LOG.debug("While waiting for node to complete a power state " + "change, it dissociate with the instance.", + instance=instance) + raise exception.NodeNotFound() + + if node.target_power_state == ironic_states.NOSTATE: + raise loopingcall.LoopingCallDone() + + def do_node_rebuild(self, instance): + # trigger the node rebuild + try: + self.ironicclient.call("node.set_provision_state", + instance.node_uuid, + ironic_states.REBUILD) + except (ironic_exc.InternalServerError, + ironic_exc.BadRequest) as e: + msg = (_("Failed to request Ironic to rebuild instance " + "%(inst)s: %(reason)s") % {'inst': instance.uuid, + 'reason': six.text_type(e)}) + raise exception.InstanceDeployFailure(msg) + + # Although the target provision state is REBUILD, it will actually go + # to ACTIVE once the redeploy is finished. + timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_active, + instance) + timer.start(interval=CONF.ironic.api_retry_interval).wait() diff --git a/mogan/engine/baremetal/ironic_states.py b/mogan/engine/baremetal/ironic/ironic_states.py similarity index 100% rename from mogan/engine/baremetal/ironic_states.py rename to mogan/engine/baremetal/ironic/ironic_states.py diff --git a/mogan/engine/base_manager.py b/mogan/engine/base_manager.py index daeca30e..5c2f4a32 100644 --- a/mogan/engine/base_manager.py +++ b/mogan/engine/base_manager.py @@ -20,9 +20,9 @@ from oslo_service import periodic_task from oslo_utils import importutils from mogan.common.i18n import _ -from mogan.common import ironic from mogan.conf import CONF from mogan.db import api as dbapi +from mogan.engine.baremetal import driver from mogan.engine import rpcapi from mogan import network @@ -39,7 +39,7 @@ class BaseEngineManager(periodic_task.PeriodicTasks): self.network_api = network.API() scheduler_driver = CONF.scheduler.scheduler_driver self.scheduler = importutils.import_object(scheduler_driver) - self.ironicclient = ironic.IronicClientWrapper() + self.driver = driver.load_engine_driver(CONF.engine.engine_driver) self.engine_rpcapi = rpcapi.EngineAPI() self._sync_power_pool = greenpool.GreenPool( size=CONF.engine.sync_power_state_pool_size) diff --git a/mogan/engine/flows/create_instance.py b/mogan/engine/flows/create_instance.py index aec2fe44..73ef2fba 100644 --- a/mogan/engine/flows/create_instance.py +++ b/mogan/engine/flows/create_instance.py @@ -27,9 +27,9 @@ from mogan.common.i18n import _ from mogan.common.i18n import _LE from mogan.common.i18n import _LI from mogan.common import utils -from mogan.engine.baremetal import ironic from mogan import objects + LOG = logging.getLogger(__name__) ACTION = 'instance:create' @@ -131,13 +131,13 @@ class OnFailureRescheduleTask(flow_utils.MoganTask): class SetInstanceInfoTask(flow_utils.MoganTask): - """Set instance info to ironic node and validate it.""" + """Set instance info to baremetal node and validate it.""" - def __init__(self, ironicclient): + def __init__(self, driver): requires = ['instance', 'context'] super(SetInstanceInfoTask, self).__init__(addons=[ACTION], requires=requires) - self.ironicclient = ironicclient + self.driver = driver # These exception types will trigger the instance info to be cleaned. self.instance_info_cleaned_exc_types = [ exception.ValidationError, @@ -146,11 +146,11 @@ class SetInstanceInfoTask(flow_utils.MoganTask): ] def execute(self, context, instance): - node = ironic.get_node(self.ironicclient, instance.node_uuid) - ironic.set_instance_info(self.ironicclient, instance, node) + node = self.driver.get_node(instance.node_uuid) + self.driver.set_instance_info(instance, node) # validate we are ready to do the deploy - validate_chk = ironic.validate_node(self.ironicclient, - instance.node_uuid) + validate_chk = self.driver.validate_node(instance.node_uuid) + if (not validate_chk.deploy.get('result') or not validate_chk.power.get('result')): raise exception.ValidationError(_( @@ -161,13 +161,13 @@ class SetInstanceInfoTask(flow_utils.MoganTask): 'power': validate_chk.power}) def revert(self, context, result, flow_failures, instance, **kwargs): - # Check if we have a cause which need to clean up ironic node + # Check if we have a cause which need to clean up baremetal node # instance info. for failure in flow_failures.values(): if failure.check(*self.instance_info_cleaned_exc_types): LOG.debug("Instance %s: cleaning up node instance info", instance.uuid) - ironic.unset_instance_info(self.ironicclient, instance) + self.driver.unset_instance_info(instance) return True return False @@ -191,31 +191,31 @@ class BuildNetworkTask(flow_utils.MoganTask): def _build_networks(self, context, instance, requested_networks): node_uuid = instance.node_uuid - ironic_ports = ironic.get_ports_from_node(self.manager.ironicclient, - node_uuid, - detail=True) + bm_ports = self.manager.driver.get_ports_from_node(node_uuid, + detail=True) + LOG.debug(_('Find ports %(ports)s for node %(node)s') % - {'ports': ironic_ports, 'node': node_uuid}) - if len(requested_networks) > len(ironic_ports): + {'ports': bm_ports, 'node': node_uuid}) + if len(requested_networks) > len(bm_ports): raise exception.InterfacePlugException(_( "Ironic node: %(id)s virtual to physical interface count" " mismatch" " (Vif count: %(vif_count)d, Pif count: %(pif_count)d)") % {'id': instance.node_uuid, 'vif_count': len(requested_networks), - 'pif_count': len(ironic_ports)}) + 'pif_count': len(bm_ports)}) nics_obj = objects.InstanceNics(context) for vif in requested_networks: - for pif in ironic_ports: + for pif in bm_ports: # Match the specified port type with physical interface type if vif.get('port_type') == pif.extra.get('port_type'): try: port = self.manager.network_api.create_port( context, vif['net_id'], pif.address, instance.uuid) port_dict = port['port'] - ironic.plug_vif(self.manager.ironicclient, pif.uuid, - port_dict['id']) + + self.manager.driver.plug_vif(pif.uuid, port_dict['id']) nic_dict = {'port_id': port_dict['id'], 'network_id': port_dict['network_id'], 'mac_address': port_dict['mac_address'], @@ -224,6 +224,7 @@ class BuildNetworkTask(flow_utils.MoganTask): 'instance_uuid': instance.uuid} nics_obj.objects.append(objects.InstanceNic( context, **nic_dict)) + except Exception: # Set nics here, so we can clean up the # created networks during reverting. @@ -273,11 +274,7 @@ class CreateInstanceTask(flow_utils.MoganTask): ] def _build_instance(self, context, instance): - ironic.do_node_deploy(self.manager.ironicclient, instance.node_uuid) - - timer = loopingcall.FixedIntervalLoopingCall( - self.manager.wait_for_active, instance) - timer.start(interval=CONF.ironic.api_retry_interval).wait() + self.manager.driver.do_node_deploy(instance) LOG.info(_LI('Successfully provisioned Ironic node %s'), instance.node_uuid) @@ -289,8 +286,7 @@ class CreateInstanceTask(flow_utils.MoganTask): for failure in flow_failures.values(): if failure.check(*self.instance_cleaned_exc_types): LOG.debug("Instance %s: destroy ironic node", instance.uuid) - ironic.destroy_node(self.manager.ironicclient, - instance.node_uuid) + self.manger.driver.destroy(instance) return True return False @@ -304,8 +300,8 @@ def get_flow(context, manager, instance, requested_networks, request_spec, This flow will do the following: 1. Schedule a node to create instance - 2. Set instance info to ironic node and validate it's ready to deploy - 3. Build networks for the instance and set port id back to ironic port + 2. Set instance info to baremetal node and validate it's ready to deploy + 3. Build networks for the instance and set port id back to baremetal port 4. Do node deploy and handle errors. """ @@ -325,7 +321,7 @@ def get_flow(context, manager, instance, requested_networks, request_spec, instance_flow.add(ScheduleCreateInstanceTask(manager), OnFailureRescheduleTask(manager.engine_rpcapi), - SetInstanceInfoTask(manager.ironicclient), + SetInstanceInfoTask(manager.driver), BuildNetworkTask(manager), CreateInstanceTask(manager)) diff --git a/mogan/engine/manager.py b/mogan/engine/manager.py index f9c8e9d4..b9c2b09f 100644 --- a/mogan/engine/manager.py +++ b/mogan/engine/manager.py @@ -15,13 +15,12 @@ import threading -from ironicclient import exc as ironic_exc from oslo_log import log import oslo_messaging as messaging -from oslo_service import loopingcall from oslo_service import periodic_task from oslo_utils import timeutils import six +import sys from mogan.common import exception from mogan.common import flow_utils @@ -32,8 +31,6 @@ from mogan.common.i18n import _LW from mogan.common import states from mogan.common import utils from mogan.conf import CONF -from mogan.engine.baremetal import ironic -from mogan.engine.baremetal import ironic_states from mogan.engine import base_manager from mogan.engine.flows import create_instance from mogan.notifications import base as notifications @@ -42,10 +39,6 @@ from mogan.objects import fields LOG = log.getLogger(__name__) -_UNPROVISION_STATES = (ironic_states.ACTIVE, ironic_states.DEPLOYFAIL, - ironic_states.ERROR, ironic_states.DEPLOYWAIT, - ironic_states.DEPLOYING) - class EngineManager(base_manager.BaseEngineManager): """Mogan Engine manager main class.""" @@ -57,16 +50,9 @@ class EngineManager(base_manager.BaseEngineManager): def _refresh_cache(self): node_cache = {} - nodes = ironic.get_node_list(self.ironicclient, detail=True, - maintenance=False, - provision_state=ironic_states.AVAILABLE, - associated=False, limit=0) - ports = ironic.get_port_list(self.ironicclient, limit=0, - fields=('uuid', 'node_uuid', 'extra', - 'address')) - portgroups = ironic.get_portgroup_list(self.ironicclient, limit=0, - fields=('uuid', 'node_uuid', - 'extra', 'address')) + nodes = self.driver.get_available_node_list() + ports = self.driver.get_port_list() + portgroups = self.driver.get_portgroup_list() ports += portgroups for node in nodes: # Add ports to the associated node @@ -90,14 +76,9 @@ class EngineManager(base_manager.BaseEngineManager): # Only fetching the necessary fields, will skip synchronizing if # target_power_state is not None. - node_fields = ('instance_uuid', 'power_state', 'target_power_state') try: - nodes = ironic.get_node_list(self.ironicclient, - maintenance=False, - associated=True, - fields=node_fields, - limit=0) + nodes = self.driver.get_nodes_power_state() except Exception as e: LOG.warning( _LW("Failed to retrieve node list when synchronizing power " @@ -202,14 +183,8 @@ class EngineManager(base_manager.BaseEngineManager): def _sync_maintenance_states(self, context): """Align maintenance states between the database and the hypervisor.""" - # Only fetching the necessary fields - node_fields = ('instance_uuid', 'maintenance') - try: - nodes = ironic.get_node_list(self.ironicclient, - associated=True, - fields=node_fields, - limit=0) + nodes = self.driver.get_maintenance_node_list() except Exception as e: LOG.warning( _LW("Failed to retrieve node list when synchronizing " @@ -264,16 +239,16 @@ class EngineManager(base_manager.BaseEngineManager): for port in ports: self.network_api.delete_port(context, port, instance.uuid) - ironic_ports = ironic.get_ports_from_node(self.ironicclient, - instance.node_uuid, - detail=True) - for pif in ironic_ports: - if 'vif_port_id' in pif.extra: - ironic.unplug_vif(self.ironicclient, pif.uuid) + bm_interface = self.driver.get_ports_from_node(instance.node_uuid) + + for pif in bm_interface: + self.driver.unplug_vif(pif) def _destroy_instance(self, context, instance): try: - ironic.destroy_node(self.ironicclient, instance.node_uuid) + self.driver.destroy(instance) + except exception.MoganException as e: + six.reraise(type(e), e, sys.exc_info()[2]) except Exception as e: # if the node is already in a deprovisioned state, continue # This should be fixed in Ironic. @@ -283,92 +258,19 @@ class EngineManager(base_manager.BaseEngineManager): if getattr(e, '__name__', None) != 'InstanceDeployFailure': raise - # using a dict because this is modified in the local method - data = {'tries': 0} - - def _wait_for_provision_state(): - - try: - node = ironic.get_node_by_instance(self.ironicclient, - instance.uuid) - except ironic_exc.NotFound: - LOG.debug("Instance already removed from Ironic", - instance=instance) - raise loopingcall.LoopingCallDone() - LOG.debug('Current ironic node state is %s', node.provision_state) - if node.provision_state in (ironic_states.NOSTATE, - ironic_states.CLEANING, - ironic_states.CLEANWAIT, - ironic_states.CLEANFAIL, - ironic_states.AVAILABLE): - # From a user standpoint, the node is unprovisioned. If a node - # gets into CLEANFAIL state, it must be fixed in Ironic, but we - # can consider the instance unprovisioned. - LOG.debug("Ironic node %(node)s is in state %(state)s, " - "instance is now unprovisioned.", - dict(node=node.uuid, state=node.provision_state), - instance=instance) - raise loopingcall.LoopingCallDone() - - if data['tries'] >= CONF.ironic.api_max_retries + 1: - msg = (_("Error destroying the instance on node %(node)s. " - "Provision state still '%(state)s'.") - % {'state': node.provision_state, - 'node': node.uuid}) - LOG.error(msg) - raise exception.MoganException(msg) - else: - data['tries'] += 1 - - # wait for the state transition to finish - timer = loopingcall.FixedIntervalLoopingCall(_wait_for_provision_state) - timer.start(interval=CONF.ironic.api_retry_interval).wait() - LOG.info(_LI('Successfully destroyed Ironic node %s'), instance.node_uuid) def _remove_instance_info_from_node(self, instance): try: - ironic.unset_instance_info(self.ironicclient, instance) - except ironic_exc.BadRequest as e: + self.driver.unset_instance_info(instance) + except exception.Invalid as e: LOG.warning(_LW("Failed to remove deploy parameters from node " "%(node)s when unprovisioning the instance " "%(instance)s: %(reason)s"), {'node': instance.node_uuid, 'instance': instance.uuid, 'reason': six.text_type(e)}) - def wait_for_active(self, instance): - """Wait for the node to be marked as ACTIVE in Ironic.""" - instance.refresh() - if instance.status in (states.DELETING, states.ERROR, states.DELETED): - raise exception.InstanceDeployFailure( - _("Instance %s provisioning was aborted") % instance.uuid) - - node = ironic.get_node_by_instance(self.ironicclient, - instance.uuid) - LOG.debug('Current ironic node state is %s', node.provision_state) - if node.provision_state == ironic_states.ACTIVE: - # job is done - LOG.debug("Ironic node %(node)s is now ACTIVE", - dict(node=node.uuid)) - raise loopingcall.LoopingCallDone() - - if node.target_provision_state in (ironic_states.DELETED, - ironic_states.AVAILABLE): - # ironic is trying to delete it now - raise exception.InstanceNotFound(instance_id=instance.uuid) - - if node.provision_state in (ironic_states.NOSTATE, - ironic_states.AVAILABLE): - # ironic already deleted it - raise exception.InstanceNotFound(instance_id=instance.uuid) - - if node.provision_state == ironic_states.DEPLOYFAIL: - # ironic failed to deploy - msg = (_("Failed to provision instance %(inst)s: %(reason)s") - % {'inst': instance.uuid, 'reason': node.last_error}) - raise exception.InstanceDeployFailure(msg) - def create_instance(self, context, instance, requested_networks, request_spec=None, filter_properties=None): """Perform a deployment.""" @@ -423,8 +325,7 @@ class EngineManager(base_manager.BaseEngineManager): # doesn't alter the instance in any way. This may raise # InvalidState, if this event is not allowed in the current state. fsm.process_event('done') - instance.power_state = ironic.get_power_state(self.ironicclient, - instance.uuid) + instance.power_state = self.driver.get_power_state(instance.uuid) instance.status = fsm.current_state instance.launched_at = timeutils.utcnow() instance.save() @@ -442,14 +343,13 @@ class EngineManager(base_manager.BaseEngineManager): target_state=states.DELETED) try: - node = ironic.get_node_by_instance(self.ironicclient, - instance.uuid) - except ironic_exc.NotFound: + node = self.driver.get_node_by_instance(instance.uuid) + except exception.NotFound: node = None if node: try: - if node.provision_state in _UNPROVISION_STATES: + if self.driver.is_node_unprovision(node): self.destroy_networks(context, instance) self._destroy_instance(context, instance) else: @@ -471,20 +371,6 @@ class EngineManager(base_manager.BaseEngineManager): instance.save() instance.destroy() - def _wait_for_power_state(self, instance): - """Wait for the node to complete a power state change.""" - try: - node = ironic.get_node_by_instance(self.ironicclient, - instance.uuid) - except ironic_exc.NotFound: - LOG.debug("While waiting for node to complete a power state " - "change, it dissociate with the instance.", - instance=instance) - raise exception.NodeNotFound() - - if node.target_power_state == ironic_states.NOSTATE: - raise loopingcall.LoopingCallDone() - def set_power_state(self, context, instance, state): """Set power state for the specified instance.""" @@ -497,21 +383,13 @@ class EngineManager(base_manager.BaseEngineManager): LOG.debug('Power %(state)s called for instance %(instance)s', {'state': state, 'instance': instance}) - ironic.set_power_state(self.ironicclient, - instance.node_uuid, - state) - - timer = loopingcall.FixedIntervalLoopingCall( - self._wait_for_power_state, instance) - timer.start(interval=CONF.ironic.api_retry_interval).wait() - - fsm.process_event('done') - instance.power_state = ironic.get_power_state(self.ironicclient, - instance.uuid) - instance.status = fsm.current_state - instance.save() + self.driver.set_power_state(instance, state) do_set_power_state() + fsm.process_event('done') + instance.power_state = self.driver.get_power_state(instance.uuid) + instance.status = fsm.current_state + instance.save() LOG.info(_LI('Successfully set node power state: %s'), state, instance=instance) @@ -519,19 +397,9 @@ class EngineManager(base_manager.BaseEngineManager): """Perform rebuild action on the specified instance.""" try: - ironic.do_node_rebuild(self.ironicclient, instance.node_uuid) - except (ironic_exc.InternalServerError, - ironic_exc.BadRequest) as e: - msg = (_("Failed to request Ironic to rebuild instance " - "%(inst)s: %(reason)s") % {'inst': instance.uuid, - 'reason': six.text_type(e)}) - raise exception.InstanceDeployFailure(msg) - - # Although the target provision state is REBUILD, it will actually go - # to ACTIVE once the redeploy is finished. - timer = loopingcall.FixedIntervalLoopingCall(self.wait_for_active, - instance) - timer.start(interval=CONF.ironic.api_retry_interval).wait() + self.driver.do_node_rebuild(instance) + except exception.InstanceDeployFailure as e: + six.reraise(type(e), e, sys.exc_info()[2]) def rebuild(self, context, instance): """Perform rebuild action on the specified instance.""" diff --git a/mogan/tests/unit/engine/flows/test_create_instance_flow.py b/mogan/tests/unit/engine/flows/test_create_instance_flow.py index 2fe901f3..8567e606 100644 --- a/mogan/tests/unit/engine/flows/test_create_instance_flow.py +++ b/mogan/tests/unit/engine/flows/test_create_instance_flow.py @@ -18,8 +18,9 @@ import mock from oslo_context import context from oslo_utils import uuidutils -from mogan.engine.baremetal import ironic +from mogan.engine.baremetal.ironic import IronicDriver from mogan.engine.flows import create_instance +from mogan.engine import manager from mogan.engine.scheduler import filter_scheduler as scheduler from mogan import objects from mogan.tests import base @@ -56,22 +57,22 @@ class CreateInstanceFlowTestCase(base.TestCase): fake_filter_props) self.assertEqual(fake_uuid, instance_obj.node_uuid) - @mock.patch.object(ironic, 'validate_node') - @mock.patch.object(ironic, 'set_instance_info') - def test_set_instance_info_task_execute(self, mock_set_inst, + @mock.patch.object(IronicDriver, 'validate_node') + @mock.patch.object(IronicDriver, 'set_instance_info') + @mock.patch.object(IronicDriver, 'get_node') + def test_set_instance_info_task_execute(self, mock_get_node, mock_set_inst, mock_validate): - fake_ironicclient = mock.MagicMock() - task = create_instance.SetInstanceInfoTask( - fake_ironicclient) + flow_manager = manager.EngineManager('test-host', 'test-topic') + task = create_instance.SetInstanceInfoTask(flow_manager.driver) instance_obj = obj_utils.get_test_instance(self.ctxt) + mock_get_node.side_effect = None mock_set_inst.side_effect = None mock_validate.side_effect = None task.execute(self.ctxt, instance_obj) - mock_set_inst.assert_called_once_with(fake_ironicclient, - instance_obj, mock.ANY) - mock_validate.assert_called_once_with(fake_ironicclient, - instance_obj.node_uuid) + mock_get_node.assert_called_once_with(instance_obj.node_uuid) + mock_set_inst.assert_called_once_with(instance_obj, mock.ANY) + mock_validate.assert_called_once_with(instance_obj.node_uuid) @mock.patch.object(objects.instance.Instance, 'save') @mock.patch.object(create_instance.BuildNetworkTask, '_build_networks') diff --git a/mogan/tests/unit/engine/test_manager.py b/mogan/tests/unit/engine/test_manager.py index a62a39c5..49cb0aa5 100644 --- a/mogan/tests/unit/engine/test_manager.py +++ b/mogan/tests/unit/engine/test_manager.py @@ -18,10 +18,9 @@ import mock from oslo_config import cfg -from mogan.common import exception from mogan.common import states -from mogan.engine.baremetal import ironic -from mogan.engine.baremetal import ironic_states +from mogan.engine.baremetal.ironic.driver import ironic_states +from mogan.engine.baremetal.ironic import IronicDriver from mogan.engine import manager from mogan.network import api as network_api from mogan.tests.unit.db import base as tests_db_base @@ -35,8 +34,8 @@ CONF = cfg.CONF class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, tests_db_base.DbTestCase): - @mock.patch.object(ironic, 'unplug_vif') - @mock.patch.object(ironic, 'get_ports_from_node') + @mock.patch.object(IronicDriver, 'unplug_vif') + @mock.patch.object(IronicDriver, 'get_ports_from_node') @mock.patch.object(network_api.API, 'delete_port') def test_destroy_networks(self, delete_port_mock, get_ports_mock, unplug_vif_mock, @@ -57,18 +56,14 @@ class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, delete_port_mock.assert_called_once_with( self.context, inst_port_id, instance.uuid) - get_ports_mock.assert_called_once_with( - mock.ANY, instance.node_uuid, detail=True) - unplug_vif_mock.assert_called_once_with(mock.ANY, 'fake-uuid') + get_ports_mock.assert_called_once_with(instance.node_uuid) + unplug_vif_mock.assert_called_once_with(port) - @mock.patch.object(ironic, 'get_node_by_instance') - @mock.patch.object(ironic, 'destroy_node') + @mock.patch.object(IronicDriver, 'destroy') def _test__destroy_instance(self, destroy_node_mock, - get_node_mock, refresh_cache_mock, - state=None): + refresh_cache_mock, state=None): fake_node = mock.MagicMock() fake_node.provision_state = state - get_node_mock.return_value = fake_node instance = obj_utils.create_test_instance(self.context) destroy_node_mock.side_effect = None refresh_cache_mock.side_effect = None @@ -77,8 +72,7 @@ class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, self.service._destroy_instance(self.context, instance) self._stop_service() - get_node_mock.assert_called_once_with(mock.ANY, instance.uuid) - destroy_node_mock.assert_called_once_with(mock.ANY, instance.node_uuid) + destroy_node_mock.assert_called_once_with(instance) def test__destroy_instance_cleaning(self, refresh_cache_mock): self._test__destroy_instance(state=ironic_states.CLEANING, @@ -88,29 +82,7 @@ class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, self._test__destroy_instance(state=ironic_states.CLEANWAIT, refresh_cache_mock=refresh_cache_mock) - @mock.patch.object(ironic, 'get_node_by_instance') - @mock.patch.object(ironic, 'destroy_node') - def test__destroy_instance_fail_max_retries(self, destroy_node_mock, - get_node_mock, - refresh_cache_mock): - CONF.set_default('api_max_retries', default=2, group='ironic') - fake_node = mock.MagicMock() - fake_node.provision_state = ironic_states.ACTIVE - get_node_mock.return_value = fake_node - instance = obj_utils.create_test_instance(self.context) - destroy_node_mock.side_effect = None - refresh_cache_mock.side_effect = None - self._start_service() - - self.assertRaises(exception.MoganException, - self.service._destroy_instance, - self.context, instance) - self._stop_service() - - self.assertTrue(get_node_mock.called) - destroy_node_mock.assert_called_once_with(mock.ANY, instance.node_uuid) - - @mock.patch.object(ironic, 'get_node_by_instance') + @mock.patch.object(IronicDriver, 'get_node_by_instance') @mock.patch.object(manager.EngineManager, '_destroy_instance') @mock.patch.object(manager.EngineManager, 'destroy_networks') def test_delete_instance(self, destroy_net_mock, @@ -131,9 +103,9 @@ class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, destroy_net_mock.assert_called_once_with(mock.ANY, instance) destroy_inst_mock.assert_called_once_with(mock.ANY, instance) - get_node_mock.assert_called_once_with(mock.ANY, instance.uuid) + get_node_mock.assert_called_once_with(instance.uuid) - @mock.patch.object(ironic, 'get_node_by_instance') + @mock.patch.object(IronicDriver, 'get_node_by_instance') @mock.patch.object(manager.EngineManager, '_destroy_instance') def test_delete_instance_without_node_destroy( self, destroy_inst_mock, get_node_mock, refresh_cache_mock): @@ -151,17 +123,15 @@ class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, self.assertFalse(destroy_inst_mock.called) - @mock.patch.object(ironic, 'get_power_state') - @mock.patch.object(ironic, 'get_node_by_instance') - @mock.patch.object(ironic, 'set_power_state') + @mock.patch.object(IronicDriver, 'get_power_state') + @mock.patch.object(IronicDriver, 'set_power_state') def test_change_instance_power_state( - self, set_power_mock, get_node_mock, get_power_mock, + self, set_power_mock, get_power_mock, refresh_cache_mock): instance = obj_utils.create_test_instance( self.context, status=states.POWERING_ON) fake_node = mock.MagicMock() fake_node.target_power_state = ironic_states.NOSTATE - get_node_mock.return_value = fake_node get_power_mock.return_value = states.POWER_ON refresh_cache_mock.side_effect = None self._start_service() @@ -170,10 +140,9 @@ class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, ironic_states.POWER_ON) self._stop_service() - set_power_mock.assert_called_once_with(mock.ANY, instance.node_uuid, + set_power_mock.assert_called_once_with(instance, ironic_states.POWER_ON) - get_node_mock.assert_called_once_with(mock.ANY, instance.uuid) - get_power_mock.assert_called_once_with(mock.ANY, instance.uuid) + get_power_mock.assert_called_once_with(instance.uuid) def test_list_availability_zone(self, refresh_cache_mock): refresh_cache_mock.side_effect = None