diff --git a/api-ref/source/v1/instances.inc b/api-ref/source/v1/instances.inc index a8690327..5b74ceaa 100644 --- a/api-ref/source/v1/instances.inc +++ b/api-ref/source/v1/instances.inc @@ -104,6 +104,7 @@ Response - description: instance_description - uuid: instance_uuid - status: instance_status + - power_state: instance_power_state - links: links **Example List of Instances: JSON response** diff --git a/api-ref/source/v1/samples/instances/instance-list-resp.json b/api-ref/source/v1/samples/instances/instance-list-resp.json index 3ed48a30..70c6cded 100644 --- a/api-ref/source/v1/samples/instances/instance-list-resp.json +++ b/api-ref/source/v1/samples/instances/instance-list-resp.json @@ -14,6 +14,7 @@ ], "name": "test_instance", "status": "building", + "power_state": "power on", "uuid": "f978ef48-d4af-4dad-beec-e6174309bc71" } ] diff --git a/mogan/api/controllers/v1/instances.py b/mogan/api/controllers/v1/instances.py index f7d227c5..304df972 100644 --- a/mogan/api/controllers/v1/instances.py +++ b/mogan/api/controllers/v1/instances.py @@ -30,14 +30,12 @@ from mogan.api.controllers.v1 import utils as api_utils from mogan.api import expose from mogan.common import exception from mogan.common.i18n import _ -from mogan.common.i18n import _LW from mogan.common import policy from mogan.engine.baremetal import ironic_states as ir_states from mogan import objects _DEFAULT_INSTANCE_RETURN_FIELDS = ('uuid', 'name', 'description', - 'status') -_NODE_FIELDS = ['power_state', 'instance_uuid'] + 'status', 'power_state') LOG = log.getLogger(__name__) @@ -286,27 +284,6 @@ class InstanceController(rest.RestController): project_only=project_only) instances_data = [instance.as_dict() for instance in instances] - if fields is None or 'power_state' in fields: - try: - nodes = pecan.request.engine_api.get_ironic_node_list( - pecan.request.context, fields=_NODE_FIELDS) - node_list = nodes['nodes'] - except Exception as e: - LOG.warning( - _LW("Failed to retrieve node list from" - "ironic api: %(msg)s") % {"msg": e}) - node_list = [] - - if node_list: - node_dict = {node['instance_uuid']: node for node in node_list - if node['instance_uuid']} - # Merge mogan instance info with ironic node power state - for instance_data in instances_data: - uuid = instance_data['uuid'] - if uuid in node_dict: - instance_data['power_state'] = \ - node_dict[uuid]['power_state'] - return InstanceCollection.convert_with_links(instances_data, fields=fields) @@ -337,21 +314,6 @@ class InstanceController(rest.RestController): """ rpc_instance = self._resource or self._get_resource(instance_uuid) instance_data = rpc_instance.as_dict() - if (fields is None or 'power_state' in fields and - instance_data['node_uuid']): - # Only fetch node info if fields parameter is not specified - # or node fields is not requested and when instance is really - # associated with a ironic node. - try: - node = pecan.request.engine_api.get_ironic_node( - pecan.request.context, instance_uuid, _NODE_FIELDS) - instance_data['power_state'] = node['power_state'] - except Exception as e: - LOG.warning( - _LW("Failed to retrieve node by instance_uuid" - " %(instance_uuid)s from ironic api: %(msg)s") % { - "instance_uuid": instance_uuid, - "msg": e}) return Instance.convert_with_links(instance_data, fields=fields) diff --git a/mogan/common/states.py b/mogan/common/states.py index 9d3fe4f2..618c80ff 100644 --- a/mogan/common/states.py +++ b/mogan/common/states.py @@ -27,6 +27,26 @@ from mogan.common import fsm LOG = logging.getLogger(__name__) +############## +# Power states +############## + +POWER_ON = 'power on' +""" Instance is powered on. """ + +POWER_OFF = 'power off' +""" Instance is powered off. """ + +NOSTATE = None +""" No state information """ + +POWER_ACTION_MAP = { + 'on': 'start', + 'off': 'stop', + 'reboot': 'reboot', +} + + ################# # Instance states ################# diff --git a/mogan/common/utils.py b/mogan/common/utils.py index fcc6889a..3b4257ce 100644 --- a/mogan/common/utils.py +++ b/mogan/common/utils.py @@ -17,6 +17,7 @@ import re +from oslo_concurrency import lockutils from oslo_log import log as logging import six @@ -25,6 +26,8 @@ from mogan.common.i18n import _LW LOG = logging.getLogger(__name__) +synchronized = lockutils.synchronized_with_prefix('mogan-') + def safe_rstrip(value, chars=None): """Removes trailing characters from a string if that does not make it empty diff --git a/mogan/conf/engine.py b/mogan/conf/engine.py index cad314b8..bea08b5a 100644 --- a/mogan/conf/engine.py +++ b/mogan/conf/engine.py @@ -40,7 +40,16 @@ opts = [ help=_('Default scheduler driver to use')), cfg.StrOpt('default_schedule_zone', help=_("Availability zone to use when user doesn't " - "specify one.")) + "specify one.")), + cfg.IntOpt('sync_power_state_pool_size', + default=1000, + help=_("Number of greenthreads available for use to sync " + "power states. Any positive integer representing " + "greenthreads count.")), + cfg.IntOpt('sync_power_state_interval', + default=600, + help=_("Interval to sync power states between the database " + "and Ironic, in seconds.")), ] diff --git a/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py b/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py index 6545bdb3..4c50c25c 100644 --- a/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py +++ b/mogan/db/sqlalchemy/alembic/versions/91941bf1ebc9_initial_migration.py @@ -79,6 +79,7 @@ def upgrade(): sa.Column('name', sa.String(length=255), nullable=True), sa.Column('description', sa.String(length=255), nullable=True), sa.Column('status', sa.String(length=255), nullable=True), + sa.Column('power_state', sa.String(length=15), nullable=True), sa.Column('instance_type_uuid', sa.String(length=36), nullable=True), sa.Column('image_uuid', sa.String(length=36), nullable=True), sa.Column('network_info', sa.Text(), nullable=True), diff --git a/mogan/db/sqlalchemy/models.py b/mogan/db/sqlalchemy/models.py index 3131a577..3b2230c5 100644 --- a/mogan/db/sqlalchemy/models.py +++ b/mogan/db/sqlalchemy/models.py @@ -73,6 +73,7 @@ class Instance(Base): project_id = Column(String(36), nullable=True) user_id = Column(String(36), nullable=True) status = Column(String(255), nullable=True) + power_state = Column(String(15), nullable=True) instance_type_uuid = Column(String(36), nullable=True) availability_zone = Column(String(255), nullable=True) image_uuid = Column(String(36), nullable=True) diff --git a/mogan/engine/api.py b/mogan/engine/api.py index f8ca6760..ccd0d385 100644 --- a/mogan/engine/api.py +++ b/mogan/engine/api.py @@ -125,16 +125,15 @@ class API(object): def _delete_instance(self, context, instance): # Initialize state machine fsm = states.machine.copy() - fsm.initialize(start_state=instance.status, - target_state=states.DELETED) + fsm.initialize(start_state=instance.status) fsm.process_event('delete') try: instance.status = fsm.current_state instance.save() except exception.InstanceNotFound: - LOG.debug("Instance %s is not found while deleting", - instance.uuid) + LOG.debug("Instance is not found while deleting", + instance=instance) return self.engine_rpcapi.delete_instance(context, instance) @@ -149,18 +148,21 @@ class API(object): def power(self, context, instance, target): """Set power state of an instance.""" + LOG.debug("Going to try to set instance power state to %s", + target, instance=instance) + fsm = states.machine.copy() + fsm.initialize(start_state=instance.status) + fsm.process_event(states.POWER_ACTION_MAP[target]) + try: + instance.status = fsm.current_state + instance.save() + except exception.InstanceNotFound: + LOG.debug("Instance is not found while setting power state", + instance=instance) + return + self.engine_rpcapi.set_power_state(context, instance, target) - def get_ironic_node(self, context, instance_uuid, fields): - """Get a ironic node by instance UUID.""" - return self.engine_rpcapi.get_ironic_node(context, - instance_uuid, - fields) - - def get_ironic_node_list(self, context, fields): - """Get a list of ironic node.""" - return self.engine_rpcapi.get_ironic_node_list(context, fields) - def list_availability_zones(self, context): """Get a list of availability zones.""" return self.engine_rpcapi.list_availability_zones(context) diff --git a/mogan/engine/baremetal/ironic.py b/mogan/engine/baremetal/ironic.py index adbf314d..7145440c 100644 --- a/mogan/engine/baremetal/ironic.py +++ b/mogan/engine/baremetal/ironic.py @@ -17,6 +17,8 @@ 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__) @@ -25,6 +27,29 @@ _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.""" diff --git a/mogan/engine/base_manager.py b/mogan/engine/base_manager.py index 5cff5675..daeca30e 100644 --- a/mogan/engine/base_manager.py +++ b/mogan/engine/base_manager.py @@ -41,6 +41,9 @@ class BaseEngineManager(periodic_task.PeriodicTasks): self.scheduler = importutils.import_object(scheduler_driver) self.ironicclient = ironic.IronicClientWrapper() self.engine_rpcapi = rpcapi.EngineAPI() + self._sync_power_pool = greenpool.GreenPool( + size=CONF.engine.sync_power_state_pool_size) + self._syncs_in_progress = {} self._started = False def init_host(self): diff --git a/mogan/engine/manager.py b/mogan/engine/manager.py index 023c660a..df16cc8b 100644 --- a/mogan/engine/manager.py +++ b/mogan/engine/manager.py @@ -30,12 +30,14 @@ from mogan.common.i18n import _LE from mogan.common.i18n import _LI 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 +from mogan import objects from mogan.objects import fields LOG = log.getLogger(__name__) @@ -71,6 +73,127 @@ class EngineManager(base_manager.BaseEngineManager): def _sync_node_resources(self, context): self._refresh_cache() + @periodic_task.periodic_task(spacing=CONF.engine.sync_power_state_interval, + run_immediately=True) + def _sync_power_states(self, context): + """Align power states between the database and the hypervisor.""" + + # 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) + except Exception as e: + LOG.warning( + _LW("Failed to retrieve node list when synchronizing power " + "states: %(msg)s") % {"msg": e}) + # Just retrun if we fail to get nodes real power state. + return + + node_dict = {node.instance_uuid: node for node in nodes + if node.target_power_state is None} + + if not node_dict: + LOG.warning(_LW("While synchronizing instance power states, " + "found none instance with stable power state " + "on the hypervisor.")) + return + + def _sync(db_instance, node_power_state): + # This must be synchronized as we query state from two separate + # sources, the driver (ironic) and the database. They are set + # (in stop_instance) and read, in sync. + @utils.synchronized(db_instance.uuid) + def sync_instance_power_state(): + self._sync_instance_power_state(context, db_instance, + node_power_state) + + try: + sync_instance_power_state() + except Exception: + LOG.exception(_LE("Periodic sync_power_state task had an " + "error while processing an instance."), + instance=db_instance) + + self._syncs_in_progress.pop(db_instance.uuid) + + db_instances = objects.Instance.list(context) + for db_instance in db_instances: + # process syncs asynchronously - don't want instance locking to + # block entire periodic task thread + uuid = db_instance.uuid + if uuid in self._syncs_in_progress: + LOG.debug('Sync power state already in progress for %s', uuid) + continue + + if db_instance.status not in (states.ACTIVE, states.STOPPED): + LOG.info(_LI("During sync_power_state the instance has a " + "pending task (%(task)s). Skip."), + {'task': db_instance.status}, + instance=db_instance) + continue + + if uuid not in node_dict: + continue + + node_power_state = node_dict[uuid].power_state + if db_instance.power_state != node_power_state: + LOG.debug('Triggering sync for uuid %s', uuid) + self._syncs_in_progress[uuid] = True + self._sync_power_pool.spawn_n(_sync, db_instance, + node_power_state) + + def _sync_instance_power_state(self, context, db_instance, + node_power_state): + """Align instance power state between the database and hypervisor. + + If the instance is not found on the hypervisor, but is in the database, + then a stop() API will be called on the instance. + """ + + # We re-query the DB to get the latest instance info to minimize + # (not eliminate) race condition. + db_instance.refresh() + db_power_state = db_instance.power_state + + if db_instance.status not in (states.ACTIVE, states.STOPPED): + # on the receiving end of mogan-engine, it could happen + # that the DB instance already report the new resident + # but the actual BM has not showed up on the hypervisor + # yet. In this case, let's allow the loop to continue + # and run the state sync in a later round + LOG.info(_LI("During sync_power_state the instance has a " + "pending task (%(task)s). Skip."), + {'task': db_instance.task_state}, + instance=db_instance) + return + + if node_power_state != db_power_state: + LOG.info(_LI('During _sync_instance_power_state the DB ' + 'power_state (%(db_power_state)s) does not match ' + 'the node_power_state from the hypervisor ' + '(%(node_power_state)s). Updating power_state in the ' + 'DB to match the hypervisor.'), + {'db_power_state': db_power_state, + 'node_power_state': node_power_state}, + instance=db_instance) + # power_state is always updated from hypervisor to db + db_instance.power_state = node_power_state + db_instance.save() + + def _set_instance_obj_error_state(self, context, instance): + try: + instance.status = states.ERROR + instance.save() + except exception.InstanceNotFound: + LOG.debug('Instance has been destroyed from under us while ' + 'trying to set it to ERROR', instance=instance) + def destroy_networks(self, context, instance): LOG.debug("unplug: instance_uuid=%(uuid)s vif=%(network_info)s", {'uuid': instance.uuid, @@ -195,6 +318,7 @@ class EngineManager(base_manager.BaseEngineManager): _run_flow() except Exception as e: fsm.process_event('error') + instance.power_state = states.NOSTATE instance.status = fsm.current_state instance.save() LOG.error(_LE("Created instance %(uuid)s failed." @@ -206,6 +330,8 @@ 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.status = fsm.current_state instance.launched_at = timeutils.utcnow() instance.save() @@ -240,11 +366,13 @@ class EngineManager(base_manager.BaseEngineManager): "instance resources."), instance=instance) fsm.process_event('error') + instance.power_state = states.NOSTATE instance.status = fsm.current_state instance.save() return fsm.process_event('done') + instance.power_state = states.NOSTATE instance.status = fsm.current_state instance.deleted_at = timeutils.utcnow() instance.save() @@ -279,14 +407,31 @@ class EngineManager(base_manager.BaseEngineManager): def set_power_state(self, context, instance, state): """Set power state for the specified instance.""" - 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() + # Initialize state machine + fsm = states.machine.copy() + fsm.initialize(start_state=instance.status) + + @utils.synchronized(instance.uuid) + def do_set_power_state(): + 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() + + do_set_power_state() LOG.info(_LI('Successfully set node power state: %s'), state, instance=instance) diff --git a/mogan/objects/instance.py b/mogan/objects/instance.py index 069ebfbf..4fd37602 100644 --- a/mogan/objects/instance.py +++ b/mogan/objects/instance.py @@ -36,6 +36,7 @@ class Instance(base.MoganObject, object_base.VersionedObjectDictCompat): 'project_id': object_fields.UUIDField(nullable=True), 'user_id': object_fields.UUIDField(nullable=True), 'status': object_fields.StringField(nullable=True), + 'power_state': object_fields.StringField(nullable=True), 'instance_type_uuid': object_fields.UUIDField(nullable=True), 'availability_zone': object_fields.StringField(nullable=True), 'image_uuid': object_fields.UUIDField(nullable=True), diff --git a/mogan/tests/functional/api/v1/test_instances.py b/mogan/tests/functional/api/v1/test_instances.py index 2c94d7a4..60bcc71b 100644 --- a/mogan/tests/functional/api/v1/test_instances.py +++ b/mogan/tests/functional/api/v1/test_instances.py @@ -178,7 +178,7 @@ class TestInstances(v1_test.APITestV1): self._prepare_instance(4) resps = self.get_json('/instances/detail')['instances'] self.assertEqual(4, len(resps)) - self.assertEqual(15, len(resps[0].keys())) + self.assertEqual(16, len(resps[0].keys())) self.assertEqual('test_instance_0', resps[0]['name']) self.assertEqual('just test instance 0', resps[0]['description']) self.assertEqual('building', resps[0]['status']) diff --git a/mogan/tests/unit/api/v1/test_instance.py b/mogan/tests/unit/api/v1/test_instance.py index 10822c86..142a676c 100644 --- a/mogan/tests/unit/api/v1/test_instance.py +++ b/mogan/tests/unit/api/v1/test_instance.py @@ -70,17 +70,13 @@ class TestInstanceAuthorization(v1_test.APITestV1): headers = self.gen_headers(self.context) self.post_json('/instances', body, headers=headers, status=201) - @mock.patch('mogan.engine.api.API.get_ironic_node') - def test_instance_get_one_by_owner(self, mock_get_node): - mock_get_node.return_value = {'power_state': 'power on'} + def test_instance_get_one_by_owner(self): # not admin but the owner self.context.tenant = self.instance1.project_id headers = self.gen_headers(self.context, roles="no-admin") self.get_json('/instances/%s' % self.instance1.uuid, headers=headers) - @mock.patch('mogan.engine.api.API.get_ironic_node') - def test_instance_get_one_by_admin(self, mock_get_node): - mock_get_node.return_value = {'power_state': 'power on'} + def test_instance_get_one_by_admin(self): # when the evil tenant is admin, he can do everything. self.context.tenant = self.evil_project headers = self.gen_headers(self.context, roles="admin") @@ -116,10 +112,8 @@ class TestPatch(v1_test.APITestV1): self.assertEqual('application/json', response.content_type) self.assertTrue(response.json['error_message']) - @mock.patch('mogan.engine.api.API.get_ironic_node') @mock.patch.object(timeutils, 'utcnow') - def test_replace_singular(self, mock_utcnow, mock_get_node): - mock_get_node.return_value = {'power_state': 'power on'} + def test_replace_singular(self, mock_utcnow): description = 'instance-new-description' test_time = datetime.datetime(2000, 1, 1, 0, 0) @@ -137,9 +131,7 @@ class TestPatch(v1_test.APITestV1): result['updated_at']).replace(tzinfo=None) self.assertEqual(test_time, return_updated_at) - @mock.patch('mogan.engine.api.API.get_ironic_node') - def test_replace_multi(self, mock_get_node): - mock_get_node.return_value = {'power_state': 'power on'} + def test_replace_multi(self): extra = {"foo1": "bar1", "foo2": "bar2", "foo3": "bar3"} uuid = uuidutils.generate_uuid() instance = utils.create_test_instance(name='test1', uuid=uuid, @@ -157,9 +149,7 @@ class TestPatch(v1_test.APITestV1): extra["foo2"] = new_value self.assertEqual(extra, result['extra']) - @mock.patch('mogan.engine.api.API.get_ironic_node') - def test_remove_singular(self, mock_get_node): - mock_get_node.return_value = {'power_state': 'power on'} + def test_remove_singular(self): uuid = uuidutils.generate_uuid() instance = utils.create_test_instance(name='test2', uuid=uuid, extra={'a': 'b'}) @@ -176,9 +166,7 @@ class TestPatch(v1_test.APITestV1): self.assertEqual(instance.uuid, result['uuid']) self.assertEqual(instance.extra, result['extra']) - @mock.patch('mogan.engine.api.API.get_ironic_node') - def test_remove_multi(self, mock_get_node): - mock_get_node.return_value = {'power_state': 'power on'} + def test_remove_multi(self): extra = {"foo1": "bar1", "foo2": "bar2", "foo3": "bar3"} uuid = uuidutils.generate_uuid() instance = utils.create_test_instance(name='test3', extra=extra, @@ -235,9 +223,7 @@ class TestPatch(v1_test.APITestV1): self.assertEqual(http_client.BAD_REQUEST, response.status_int) self.assertTrue(response.json['error_message']) - @mock.patch('mogan.engine.api.API.get_ironic_node') - def test_add_multi(self, mock_get_node): - mock_get_node.return_value = {'power_state': 'power on'} + def test_add_multi(self): response = self.patch_json('/instances/%s' % self.instance.uuid, [{'path': '/extra/foo1', 'value': 'bar1', 'op': 'add'}, diff --git a/mogan/tests/unit/db/utils.py b/mogan/tests/unit/db/utils.py index 0934c73b..86588fcc 100644 --- a/mogan/tests/unit/db/utils.py +++ b/mogan/tests/unit/db/utils.py @@ -47,6 +47,7 @@ def get_test_instance(**kw): 'c18e8a1a870d4c08a0b51ced6e0b6459'), 'user_id': kw.get('user_id', 'cdbf77d47f1d4d04ad9b7ff62b672467'), 'status': kw.get('status', states.ACTIVE), + 'power_state': kw.get('power_state', 'power on'), 'instance_type_uuid': kw.get('instance_type_uuid', '28708dff-283c-449e-9bfa-a48c93480c86'), 'availability_zone': kw.get('availability_zone', 'test_az'), diff --git a/mogan/tests/unit/engine/test_manager.py b/mogan/tests/unit/engine/test_manager.py index e016c62e..97a04e7b 100644 --- a/mogan/tests/unit/engine/test_manager.py +++ b/mogan/tests/unit/engine/test_manager.py @@ -151,14 +151,18 @@ 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') def test_change_instance_power_state( - self, set_power_mock, get_node_mock, refresh_cache_mock): - instance = obj_utils.create_test_instance(self.context) + self, set_power_mock, get_node_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() @@ -169,6 +173,7 @@ class ManageInstanceTestCase(mgr_utils.ServiceSetUpMixin, set_power_mock.assert_called_once_with(mock.ANY, instance.node_uuid, 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) @mock.patch.object(ironic, 'get_node_states') def test_get_instance_states(self, get_states_mock, refresh_cache_mock): diff --git a/mogan/tests/unit/objects/test_objects.py b/mogan/tests/unit/objects/test_objects.py index b5c32865..da08c4ce 100644 --- a/mogan/tests/unit/objects/test_objects.py +++ b/mogan/tests/unit/objects/test_objects.py @@ -382,7 +382,7 @@ class _TestObject(object): # version bump. It is md5 hash of object fields and remotable methods. # The fingerprint values should only be changed if there is a version bump. expected_object_fingerprints = { - 'Instance': '1.0-dc4b1e2710dddf6fe42582c90403c0b0', + 'Instance': '1.0-c3a73e3ec189aa09dc430b389c81b11f', 'InstanceType': '1.0-589b096651fcdb30898ff50f748dd948', 'MyObj': '1.1-aad62eedc5a5cc8bcaf2982c285e753f', 'FakeNode': '1.0-295d1b08ce3048535926c47dedd27211',