API microversion 2.76: Add 'power-update' external event

This patch adds a new external event called "power-update"
through which ironic will convey all (power_off and power_on)
power state changes (running -> shutdown or shutdown -> running
will be the only ones handled by nova and the rest will be ignored)
on a physical instance to nova. The database will be updated
accordingly to reflect the real vm_state and power_state of the
instance. This way nova will not be able to enforce
an incorrect power state on the physical instance during
the periodic "sync_power_states" task.

Implements blueprint nova-support-instance-power-update
Story: 2004969
Task: 29423

Change-Id: I2b292050cc3ce5ef625659f5a1fe56bb76072496
This commit is contained in:
Surya Seetharaman 2019-03-22 16:03:56 +01:00 committed by Matt Riedemann
parent b26bc7fd7a
commit 62f6a0a1bc
23 changed files with 635 additions and 20 deletions

View File

@ -7,11 +7,11 @@
.. warning::
This is an ``admin`` level service API only designed to be used by
other OpenStack services. The point of this API is to coordinate
between Nova and Neutron, Nova and Cinder (and potentially future
services) on activities they both need to be involved in,
between Nova and Neutron, Nova and Cinder, Nova and Ironic (and potentially
future services) on activities they both need to be involved in,
such as network hotplugging.
Unless you are writing Neutron or Cinder code you **should not**
Unless you are writing Neutron, Cinder or Ironic code you **should not**
be using this API.
Creates one or more external events. The API dispatches each event to a

View File

@ -2587,9 +2587,15 @@ event_hostId:
type: string
event_name:
description: |
The event name. A valid value is ``network-changed``, ``network-vif-plugged``,
``network-vif-unplugged``, ``network-vif-deleted``, or ``volume-extended``.
The event name ``volume-extended`` is added since microversion ``2.51``.
The event name. A valid value is:
- ``network-changed``
- ``network-vif-plugged``
- ``network-vif-unplugged``
- ``network-vif-deleted``
- ``volume-extended`` (since microversion ``2.51``)
- ``power-update`` (since microversion ``2.76``)
in: body
required: true
type: string
@ -2623,7 +2629,13 @@ event_status:
type: string
event_tag:
description: |
A string value that identifies the event.
A string value that identifies the event. Certain types of events require
specific tags:
- For the ``power-update`` event the tag must be either be ``POWER_ON``
or ``POWER_OFF``.
- For the ``volume-extended`` event the tag must be the volume id.
in: body
required: false
type: string

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.75",
"version": "2.76",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.75",
"version": "2.76",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -196,6 +196,11 @@ REST_API_VERSION_HISTORY = """REST API Version History:
string to 0 (integer) in flavor APIs.
- Return ``servers`` field always in the response of GET
hypervisors API even there are no servers on hypervisor.
* 2.76 - Adds ``power-update`` event to ``os-server-external-events`` API.
The changes to the power state of an instance caused by this event
can be viewed through
``GET /servers/{server_id}/os-instance-actions`` and
``GET /servers/{server_id}/os-instance-actions/{request_id}``.
"""
# The minimum and maximum versions of the API supported
@ -204,7 +209,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.75"
_MAX_API_VERSION = "2.76"
DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal

View File

@ -41,6 +41,9 @@ LOG = logging.getLogger(__name__)
QUOTAS = quota.QUOTAS
POWER_ON = 'POWER_ON'
POWER_OFF = 'POWER_OFF'
_STATE_MAP = {
vm_states.ACTIVE: {
'default': 'ACTIVE',

View File

@ -974,3 +974,11 @@ Multiple API cleanups is done in API microversion 2.75:
* Return ``servers`` field always in the response of GET
hypervisors API even there are no servers on hypervisor.
2.76
----
Adds ``power-update`` event name to ``os-server-external-events`` API. The
changes to the power state of an instance caused by this event can be viewed
through ``GET /servers/{server_id}/os-instance-actions`` and
``GET /servers/{server_id}/os-instance-actions/{request_id}``.

View File

@ -55,3 +55,7 @@ create = {
create_v251 = copy.deepcopy(create)
name = create_v251['properties']['events']['items']['properties']['name']
name['enum'].append('volume-extended')
create_v276 = copy.deepcopy(create_v251)
name = create_v276['properties']['events']['items']['properties']['name']
name['enum'].append('power-update')

View File

@ -28,6 +28,9 @@ from nova.policies import server_external_events as see_policies
LOG = logging.getLogger(__name__)
TAG_REQUIRED = ('volume-extended', 'power-update')
class ServerExternalEventsController(wsgi.Controller):
def __init__(self):
@ -36,7 +39,7 @@ class ServerExternalEventsController(wsgi.Controller):
@staticmethod
def _is_event_tag_present_when_required(event):
if event.name == 'volume-extended' and event.tag is None:
if event.name in TAG_REQUIRED and event.tag is None:
return False
return True
@ -65,7 +68,8 @@ class ServerExternalEventsController(wsgi.Controller):
@wsgi.expected_errors((403, 404))
@wsgi.response(200)
@validation.schema(server_external_events.create, '2.0', '2.50')
@validation.schema(server_external_events.create_v251, '2.51')
@validation.schema(server_external_events.create_v251, '2.51', '2.75')
@validation.schema(server_external_events.create_v276, '2.76')
def create(self, req, body):
"""Creates a new instance event."""
context = req.environ['nova.context']

View File

@ -68,6 +68,7 @@ from nova.network.security_group import security_group_base
from nova import objects
from nova.objects import base as obj_base
from nova.objects import block_device as block_device_obj
from nova.objects import external_event as external_event_obj
from nova.objects import fields as fields_obj
from nova.objects import keypair as keypair_obj
from nova.objects import quotas as quotas_obj
@ -4694,6 +4695,22 @@ class API(base.Base):
objects.InstanceAction.action_start(
cell_context, event.instance_uuid,
instance_actions.EXTEND_VOLUME, want_result=False)
elif event.name == 'power-update':
host = hosts_by_instance[event.instance_uuid][0]
cell_context = cell_contexts_by_host[host]
if event.tag == external_event_obj.POWER_ON:
inst_action = instance_actions.START
elif event.tag == external_event_obj.POWER_OFF:
inst_action = instance_actions.STOP
else:
LOG.warning("Invalid power state %s. Cannot process "
"the event %s. Skipping it.", event.tag,
event)
continue
objects.InstanceAction.action_start(
cell_context, event.instance_uuid, inst_action,
want_result=False)
for host in hosts_by_instance[event.instance_uuid]:
events_by_host[host].append(event)

View File

@ -83,6 +83,7 @@ from nova.network import model as network_model
from nova.network.security_group import openstack_driver
from nova import objects
from nova.objects import base as obj_base
from nova.objects import external_event as external_event_obj
from nova.objects import fields
from nova.objects import instance as obj_instance
from nova.objects import migrate_data as migrate_data_obj
@ -8652,6 +8653,106 @@ class ComputeManager(manager.Manager):
instance=instance)
raise
@staticmethod
def _is_state_valid_for_power_update_event(instance, target_power_state):
"""Check if the current state of the instance allows it to be
a candidate for the power-update event.
:param instance: The nova instance object.
:param target_power_state: The desired target power state; this should
either be "POWER_ON" or "POWER_OFF".
:returns Boolean: True if the instance can be subjected to the
power-update event.
"""
if ((target_power_state == external_event_obj.POWER_ON and
instance.task_state is None and
instance.vm_state == vm_states.STOPPED and
instance.power_state == power_state.SHUTDOWN) or
(target_power_state == external_event_obj.POWER_OFF and
instance.task_state is None and
instance.vm_state == vm_states.ACTIVE and
instance.power_state == power_state.RUNNING)):
return True
return False
@wrap_exception()
@reverts_task_state
@wrap_instance_event(prefix='compute')
@wrap_instance_fault
def power_update(self, context, instance, target_power_state):
"""Power update of an instance prompted by an external event.
:param context: The API request context.
:param instance: The nova instance object.
:param target_power_state: The desired target power state;
this should either be "POWER_ON" or
"POWER_OFF".
"""
@utils.synchronized(instance.uuid)
def do_power_update():
LOG.debug('Handling power-update event with target_power_state %s '
'for instance', target_power_state, instance=instance)
if not self._is_state_valid_for_power_update_event(
instance, target_power_state):
pow_state = fields.InstancePowerState.from_index(
instance.power_state)
LOG.info('The power-update %(tag)s event for instance '
'%(uuid)s is a no-op since the instance is in '
'vm_state %(vm_state)s, task_state '
'%(task_state)s and power_state '
'%(power_state)s.',
{'tag': target_power_state, 'uuid': instance.uuid,
'vm_state': instance.vm_state,
'task_state': instance.task_state,
'power_state': pow_state})
return
LOG.debug("Trying to %s instance",
target_power_state, instance=instance)
if target_power_state == external_event_obj.POWER_ON:
action = fields.NotificationAction.POWER_ON
notification_name = "power_on."
instance.task_state = task_states.POWERING_ON
else:
# It's POWER_OFF
action = fields.NotificationAction.POWER_OFF
notification_name = "power_off."
instance.task_state = task_states.POWERING_OFF
instance.progress = 0
try:
# Note that the task_state is set here rather than the API
# because this is a best effort operation and deferring
# updating the task_state until we get to the compute service
# avoids error handling in the API and needing to account for
# older compute services during rolling upgrades from Stein.
# If we lose a race, UnexpectedTaskStateError is handled
# below.
instance.save(expected_task_state=[None])
self._notify_about_instance_usage(context, instance,
notification_name + "start")
compute_utils.notify_about_instance_action(context, instance,
self.host, action=action,
phase=fields.NotificationPhase.START)
# UnexpectedTaskStateError raised from the driver will be
# handled below and not result in a fault, error notification
# or failure of the instance action. Other driver errors like
# NotImplementedError will be record a fault, send an error
# notification and mark the instance action as failed.
self.driver.power_update_event(instance, target_power_state)
self._notify_about_instance_usage(context, instance,
notification_name + "end")
compute_utils.notify_about_instance_action(context, instance,
self.host, action=action,
phase=fields.NotificationPhase.END)
except exception.UnexpectedTaskStateError as e:
# Handling the power-update event is best effort and if we lost
# a race with some other action happening to the instance we
# just log it and return rather than fail the action.
LOG.info("The power-update event was possibly preempted: %s ",
e.format_message(), instance=instance)
return
do_power_update()
@wrap_exception()
def external_instance_event(self, context, instances, events):
# NOTE(danms): Some event types are handled by the manager, such
@ -8687,6 +8788,8 @@ class ComputeManager(manager.Manager):
instance=instance)
elif event.name == 'volume-extended':
self.extend_volume(context, instance, event.tag)
elif event.name == 'power-update':
self.power_update(context, instance, event.tag)
else:
self._process_instance_event(instance, event)

View File

@ -26,10 +26,17 @@ EVENT_NAMES = [
# Volume was extended for this instance, tag is volume_id
'volume-extended',
# Power state has changed for this instance
'power-update',
]
EVENT_STATUSES = ['failed', 'completed', 'in-progress']
# Possible tag values for the power-update event.
POWER_ON = 'POWER_ON'
POWER_OFF = 'POWER_OFF'
@obj_base.NovaObjectRegistry.register
class InstanceExternalEvent(obj_base.NovaObject):
@ -37,7 +44,8 @@ class InstanceExternalEvent(obj_base.NovaObject):
# Supports network-changed and vif-plugged
# Version 1.1: adds network-vif-deleted event
# Version 1.2: adds volume-extended event
VERSION = '1.2'
# Version 1.3: adds power-update event
VERSION = '1.3'
fields = {
'instance_uuid': fields.UUIDField(),

View File

@ -421,6 +421,10 @@ class TestOpenStackClient(object):
def delete_server_group(self, group_id):
self.api_delete('/os-server-groups/%s' % group_id)
def create_server_external_events(self, events):
body = {'events': events}
return self.api_post('/os-server-external-events', body).body['events']
def get_instance_actions(self, server_id):
return self.api_get('/servers/%s/os-instance-actions' %
(server_id)).body['instanceActions']

View File

@ -0,0 +1,104 @@
# 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 nova.compute import instance_actions
from nova.compute import power_state
from nova.compute import vm_states
from nova.tests.functional import integrated_helpers
from nova.tests.unit import fake_notifier
class ServerExternalEventsTestV276(
integrated_helpers.ProviderUsageBaseTestCase):
microversion = '2.76'
compute_driver = 'fake.PowerUpdateFakeDriver'
def setUp(self):
super(ServerExternalEventsTestV276, self).setUp()
self.compute = self.start_service('compute', host='compute')
flavors = self.api.get_flavors()
server_req = self._build_minimal_create_server_request(
self.api, "some-server", flavor_id=flavors[0]["id"],
image_uuid="155d900f-4e14-4e4c-a73d-069cbf4541e6",
networks='none')
created_server = self.api.post_server({'server': server_req})
self.server = self._wait_for_state_change(
self.api, created_server, 'ACTIVE')
self.power_off = {'name': 'power-update',
'tag': 'POWER_OFF',
'server_uuid': self.server["id"]}
self.power_on = {'name': 'power-update',
'tag': 'POWER_ON',
'server_uuid': self.server["id"]}
def test_server_power_update(self):
# This test checks the functionality of handling the "power-update"
# external events.
self.assertEqual(
power_state.RUNNING, self.server['OS-EXT-STS:power_state'])
self.api.create_server_external_events(events=[self.power_off])
expected_params = {'OS-EXT-STS:task_state': None,
'OS-EXT-STS:vm_state': vm_states.STOPPED,
'OS-EXT-STS:power_state': power_state.SHUTDOWN}
server = self._wait_for_server_parameter(self.api, self.server,
expected_params)
msg = ' with target power state POWER_OFF.'
self.assertIn(msg, self.stdlog.logger.output)
# Test if this is logged in the instance action list.
actions = self.api.get_instance_actions(server['id'])
self.assertEqual(2, len(actions))
acts = {action['action']: action for action in actions}
self.assertEqual(['create', 'stop'], sorted(acts))
stop_action = acts[instance_actions.STOP]
detail = self.api.api_get(
'/servers/%s/os-instance-actions/%s' % (
server['id'], stop_action['request_id'])
).body['instanceAction']
events_by_name = {event['event']: event for event in detail['events']}
self.assertEqual(1, len(detail['events']), detail)
self.assertIn('compute_power_update', events_by_name)
self.assertEqual('Success', detail['events'][0]['result'])
# Test if notifications were emitted.
fake_notifier.wait_for_versioned_notifications(
'instance.power_off.start')
fake_notifier.wait_for_versioned_notifications(
'instance.power_off.end')
# Checking POWER_ON
self.api.create_server_external_events(events=[self.power_on])
expected_params = {'OS-EXT-STS:task_state': None,
'OS-EXT-STS:vm_state': vm_states.ACTIVE,
'OS-EXT-STS:power_state': power_state.RUNNING}
server = self._wait_for_server_parameter(self.api, self.server,
expected_params)
msg = ' with target power state POWER_ON.'
self.assertIn(msg, self.stdlog.logger.output)
# Test if this is logged in the instance action list.
actions = self.api.get_instance_actions(server['id'])
self.assertEqual(3, len(actions))
acts = {action['action']: action for action in actions}
self.assertEqual(['create', 'start', 'stop'], sorted(acts))
start_action = acts[instance_actions.START]
detail = self.api.api_get(
'/servers/%s/os-instance-actions/%s' % (
server['id'], start_action['request_id'])
).body['instanceAction']
events_by_name = {event['event']: event for event in detail['events']}
self.assertEqual(1, len(detail['events']), detail)
self.assertIn('compute_power_update', events_by_name)
self.assertEqual('Success', detail['events'][0]['result'])
# Test if notifications were emitted.
fake_notifier.wait_for_versioned_notifications(
'instance.power_on.start')
fake_notifier.wait_for_versioned_notifications(
'instance.power_on.end')

View File

@ -12,8 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import fixtures as fx
import mock
from oslo_utils.fixture import uuidsentinel as uuids
import six
import webob
from nova.api.openstack.compute import server_external_events \
@ -22,6 +24,7 @@ from nova import exception
from nova import objects
from nova.objects import instance as instance_obj
from nova import test
from nova.tests import fixtures
from nova.tests.unit.api.openstack import fakes
@ -207,3 +210,48 @@ class ServerExternalEventsTestV251(ServerExternalEventsTestV21):
self.assertEqual(400, result['events'][1]['code'])
self.assertEqual('failed', result['events'][1]['status'])
self.assertEqual(207, code)
@mock.patch('nova.objects.InstanceMappingList.get_by_instance_uuids',
fake_get_by_instance_uuids)
@mock.patch('nova.objects.InstanceList.get_by_filters',
fake_get_by_filters)
class ServerExternalEventsTestV276(ServerExternalEventsTestV21):
wsgi_api_version = '2.76'
def setUp(self):
super(ServerExternalEventsTestV276, self).setUp()
self.useFixture(fx.EnvironmentVariable('OS_DEBUG', '1'))
self.stdlog = self.useFixture(fixtures.StandardLogging())
def test_create_with_missing_tag(self):
body = self.default_body
body['events'][0]['name'] = 'power-update'
body['events'][1]['name'] = 'power-update'
result, code = self._assert_call(body,
[fake_instance_uuids[0]],
['power-update'])
msg = "Event tag is missing for instance"
self.assertIn(msg, self.stdlog.logger.output)
self.assertEqual(200, result['events'][0]['code'])
self.assertEqual('completed', result['events'][0]['status'])
self.assertEqual(400, result['events'][1]['code'])
self.assertEqual('failed', result['events'][1]['status'])
self.assertEqual(207, code)
def test_create_event_auth_pre_2_76_fails(self):
# Negative test to make sure you can't create 'power-update'
# before 2.76.
body = self.default_body
body['events'][0]['name'] = 'power-update'
body['events'][1]['name'] = 'power-update'
req = fakes.HTTPRequestV21.blank(
'/os-server-external-events', version='2.75')
exp = self.assertRaises(
exception.ValidationError,
self.api.create,
req,
body=body)
self.assertIn('Invalid input for field/attribute name.',
six.text_type(exp))

View File

@ -31,6 +31,7 @@ import six
from nova.compute import api as compute_api
from nova.compute import flavors
from nova.compute import instance_actions
from nova.compute import power_state
from nova.compute import rpcapi as compute_rpcapi
from nova.compute import task_states
from nova.compute import utils as compute_utils
@ -3963,6 +3964,7 @@ class _ComputeAPIUnitTestMixIn(object):
@mock.patch.object(objects.InstanceAction, 'action_start')
def test_external_instance_event(self, mock_action_start):
instances = [
objects.Instance(uuid=uuids.instance_1, host='host1',
migration_context=None),
@ -3972,6 +3974,14 @@ class _ComputeAPIUnitTestMixIn(object):
migration_context=None),
objects.Instance(uuid=uuids.instance_4, host='host2',
migration_context=None),
objects.Instance(uuid=uuids.instance_5, host='host2',
migration_context=None, task_state=None,
vm_state=vm_states.STOPPED,
power_state=power_state.SHUTDOWN),
objects.Instance(uuid=uuids.instance_6, host='host2',
migration_context=None, task_state=None,
vm_state=vm_states.ACTIVE,
power_state=power_state.RUNNING)
]
# Create a single cell context and associate it with all instances
mapping = objects.InstanceMapping.get_by_instance_uuid(
@ -3996,6 +4006,14 @@ class _ComputeAPIUnitTestMixIn(object):
instance_uuid=uuids.instance_4,
name='volume-extended',
tag=volume_id),
objects.InstanceExternalEvent(
instance_uuid=uuids.instance_5,
name='power-update',
tag="POWER_ON"),
objects.InstanceExternalEvent(
instance_uuid=uuids.instance_6,
name='power-update',
tag="POWER_OFF"),
]
self.compute_api.compute_rpcapi = mock.MagicMock()
self.compute_api.external_instance_event(self.context,
@ -4005,11 +4023,68 @@ class _ComputeAPIUnitTestMixIn(object):
host='host1')
method.assert_any_call(cell_context, instances[2:], events[2:],
host='host2')
mock_action_start.assert_called_once_with(
self.context, uuids.instance_4, instance_actions.EXTEND_VOLUME,
want_result=False)
calls = [mock.call(self.context, uuids.instance_4,
instance_actions.EXTEND_VOLUME, want_result=False),
mock.call(self.context, uuids.instance_5,
instance_actions.START, want_result=False),
mock.call(self.context, uuids.instance_6,
instance_actions.STOP, want_result=False)]
mock_action_start.assert_has_calls(calls)
self.assertEqual(2, method.call_count)
def test_external_instance_event_power_update_invalid_tag(self):
instance1 = objects.Instance(self.context)
instance1.uuid = uuids.instance1
instance1.id = 1
instance1.vm_state = vm_states.ACTIVE
instance1.task_state = None
instance1.power_state = power_state.RUNNING
instance1.host = 'host1'
instance1.migration_context = None
instance2 = objects.Instance(self.context)
instance2.uuid = uuids.instance2
instance2.id = 2
instance2.vm_state = vm_states.STOPPED
instance2.task_state = None
instance2.power_state = power_state.SHUTDOWN
instance2.host = 'host2'
instance2.migration_context = None
instances = [instance1, instance2]
events = [
objects.InstanceExternalEvent(
instance_uuid=instance1.uuid,
name='power-update',
tag="VACATION"),
objects.InstanceExternalEvent(
instance_uuid=instance2.uuid,
name='power-update',
tag="POWER_ON")
]
with test.nested(
mock.patch.object(self.compute_api.compute_rpcapi,
'external_instance_event'),
mock.patch.object(objects.InstanceAction, 'action_start'),
mock.patch.object(compute_api, 'LOG')
) as (
mock_ex, mock_action_start, mock_log
):
self.compute_api.external_instance_event(self.context,
instances, events)
self.assertEqual(2, mock_ex.call_count)
# event VACATION requested on instance1 is ignored because
# its an invalid event tag.
mock_ex.assert_has_calls(
[mock.call(self.context, [instance2],
[events[1]], host=u'host2'),
mock.call(self.context, [instance1], [], host=u'host1')],
any_order=True)
mock_action_start.assert_called_once_with(
self.context, instance2.uuid, instance_actions.START,
want_result=False)
self.assertEqual(1, mock_log.warning.call_count)
self.assertIn(
'Invalid power state', mock_log.warning.call_args[0][0])
def test_external_instance_event_evacuating_instance(self):
# Since we're patching the db's migration_get(), use a dict here so
# that we can validate the id is making its way correctly to the db api

View File

@ -2980,6 +2980,135 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase,
do_test()
def test_power_update(self):
instance = objects.Instance(self.context)
instance.uuid = uuids.instance
instance.id = 1
instance.vm_state = vm_states.STOPPED
instance.task_state = None
instance.power_state = power_state.SHUTDOWN
instance.host = self.compute.host
with test.nested(
mock.patch.object(nova.compute.utils,
'notify_about_instance_action'),
mock.patch.object(self.compute, '_notify_about_instance_usage'),
mock.patch.object(self.compute.driver, 'power_update_event'),
mock.patch.object(objects.Instance, 'save'),
mock.patch.object(manager, 'LOG')
) as (
mock_instance_notify, mock_instance_usage, mock_event, mock_save,
mock_log
):
self.compute.power_update(self.context, instance, "POWER_ON")
calls = [mock.call(self.context, instance, self.compute.host,
action=fields.NotificationAction.POWER_ON,
phase=fields.NotificationPhase.START),
mock.call(self.context, instance, self.compute.host,
action=fields.NotificationAction.POWER_ON,
phase=fields.NotificationPhase.END)]
mock_instance_notify.assert_has_calls(calls)
calls = [mock.call(self.context, instance, "power_on.start"),
mock.call(self.context, instance, "power_on.end")]
mock_instance_usage.assert_has_calls(calls)
mock_event.assert_called_once_with(instance, 'POWER_ON')
mock_save.assert_called_once_with(
expected_task_state=[None])
self.assertEqual(2, mock_log.debug.call_count)
self.assertIn('Trying to', mock_log.debug.call_args[0][0])
def test_power_update_not_implemented(self):
instance = objects.Instance(self.context)
instance.uuid = uuids.instance
instance.id = 1
instance.vm_state = vm_states.STOPPED
instance.task_state = None
instance.power_state = power_state.SHUTDOWN
instance.host = self.compute.host
with test.nested(
mock.patch.object(nova.compute.utils,
'notify_about_instance_action'),
mock.patch.object(self.compute, '_notify_about_instance_usage'),
mock.patch.object(self.compute.driver, 'power_update_event',
side_effect=NotImplementedError()),
mock.patch.object(instance, 'save'),
mock.patch.object(nova.compute.utils,
'add_instance_fault_from_exc'),
) as (
mock_instance_notify, mock_instance_usage, mock_event,
mock_save, mock_fault
):
self.assertRaises(NotImplementedError,
self.compute.power_update, self.context, instance, "POWER_ON")
self.assertIsNone(instance.task_state)
self.assertEqual(2, mock_save.call_count)
# second save is done by revert_task_state
mock_save.assert_has_calls(
[mock.call(expected_task_state=[None]), mock.call()])
mock_instance_notify.assert_called_once_with(
self.context, instance, self.compute.host,
action=fields.NotificationAction.POWER_ON,
phase=fields.NotificationPhase.START)
mock_instance_usage.assert_called_once_with(
self.context, instance, "power_on.start")
mock_fault.assert_called_once_with(
self.context, instance, mock.ANY, mock.ANY)
def test_external_instance_event_power_update_invalid_state(self):
instance = objects.Instance(self.context)
instance.uuid = uuids.instance1
instance.id = 1
instance.vm_state = vm_states.ACTIVE
instance.task_state = task_states.POWERING_OFF
instance.power_state = power_state.RUNNING
instance.host = 'host1'
instance.migration_context = None
with test.nested(
mock.patch.object(nova.compute.utils,
'notify_about_instance_action'),
mock.patch.object(self.compute, '_notify_about_instance_usage'),
mock.patch.object(self.compute.driver, 'power_update_event'),
mock.patch.object(objects.Instance, 'save'),
mock.patch.object(manager, 'LOG')
) as (
mock_instance_notify, mock_instance_usage, mock_event, mock_save,
mock_log
):
self.compute.power_update(self.context, instance, "POWER_ON")
mock_instance_notify.assert_not_called()
mock_instance_usage.assert_not_called()
mock_event.assert_not_called()
mock_save.assert_not_called()
self.assertEqual(1, mock_log.info.call_count)
self.assertIn('is a no-op', mock_log.info.call_args[0][0])
def test_external_instance_event_power_update_unexpected_task_state(self):
instance = objects.Instance(self.context)
instance.uuid = uuids.instance1
instance.id = 1
instance.vm_state = vm_states.ACTIVE
instance.task_state = None
instance.power_state = power_state.RUNNING
instance.host = 'host1'
instance.migration_context = None
with test.nested(
mock.patch.object(nova.compute.utils,
'notify_about_instance_action'),
mock.patch.object(self.compute, '_notify_about_instance_usage'),
mock.patch.object(self.compute.driver, 'power_update_event'),
mock.patch.object(objects.Instance, 'save',
side_effect=exception.UnexpectedTaskStateError("blah")),
mock.patch.object(manager, 'LOG')
) as (
mock_instance_notify, mock_instance_usage, mock_event, mock_save,
mock_log
):
self.compute.power_update(self.context, instance, "POWER_OFF")
mock_instance_notify.assert_not_called()
mock_instance_usage.assert_not_called()
mock_event.assert_not_called()
self.assertEqual(1, mock_log.info.call_count)
self.assertIn('possibly preempted', mock_log.info.call_args[0][0])
def test_extend_volume(self):
inst_obj = objects.Instance(id=3, uuid=uuids.instance)
connection_info = {'foo': 'bar'}
@ -3066,7 +3195,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase,
objects.Instance(id=1, uuid=uuids.instance_1),
objects.Instance(id=2, uuid=uuids.instance_2),
objects.Instance(id=3, uuid=uuids.instance_3),
objects.Instance(id=4, uuid=uuids.instance_4)]
objects.Instance(id=4, uuid=uuids.instance_4),
objects.Instance(id=4, uuid=uuids.instance_5)]
events = [
objects.InstanceExternalEvent(name='network-changed',
tag='tag1',
@ -3079,15 +3209,20 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase,
tag='tag3'),
objects.InstanceExternalEvent(name='volume-extended',
instance_uuid=uuids.instance_4,
tag='tag4')]
tag='tag4'),
objects.InstanceExternalEvent(name='power-update',
instance_uuid=uuids.instance_5,
tag='POWER_ON')]
@mock.patch.object(self.compute, 'power_update')
@mock.patch.object(self.compute,
'extend_volume')
@mock.patch.object(self.compute, '_process_instance_vif_deleted_event')
@mock.patch.object(self.compute.network_api, 'get_instance_nw_info')
@mock.patch.object(self.compute, '_process_instance_event')
def do_test(_process_instance_event, get_instance_nw_info,
_process_instance_vif_deleted_event, extend_volume):
_process_instance_vif_deleted_event, extend_volume,
power_update):
self.compute.external_instance_event(self.context,
instances, events)
get_instance_nw_info.assert_called_once_with(self.context,
@ -3099,6 +3234,9 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase,
self.context, instances[2], events[2].tag)
extend_volume.assert_called_once_with(
self.context, instances[3], events[3].tag)
power_update.assert_called_once_with(
self.context, instances[4], events[4].tag)
do_test()
def test_external_instance_event_with_exception(self):

View File

@ -1076,7 +1076,7 @@ object_data = {
'InstanceActionEventList': '1.1-13d92fb953030cdbfee56481756e02be',
'InstanceActionList': '1.1-a2b2fb6006b47c27076d3a1d48baa759',
'InstanceDeviceMetadata': '1.0-74d78dd36aa32d26d2769a1b57caf186',
'InstanceExternalEvent': '1.2-23eb6ba79cde5cd06d3445f845ba4589',
'InstanceExternalEvent': '1.3-e47782874cca95bb96e566286e9d1e23',
'InstanceFault': '1.2-7ef01f16f1084ad1304a513d6d410a38',
'InstanceFaultList': '1.2-6bb72de2872fe49ded5eb937a93f2451',
'InstanceGroup': '1.11-852ac511d30913ee88f3c3a869a8f30a',

View File

@ -26,6 +26,7 @@ from testtools import matchers
from tooz import hashring as hash_ring
from nova.api.metadata import base as instance_metadata
from nova.api.openstack import common
from nova import block_device
from nova.compute import power_state as nova_states
from nova.compute import provider_tree
@ -1864,6 +1865,20 @@ class IronicDriverTestCase(test.NoDBTestCase):
mock_sp.assert_has_calls([mock.call(node.uuid, 'reboot', soft=True),
mock.call(node.uuid, 'reboot')])
@mock.patch.object(objects.Instance, 'save')
def test_power_update_event(self, mock_save):
instance = fake_instance.fake_instance_obj(
self.ctx, node=self.instance_uuid,
power_state=nova_states.RUNNING,
vm_state=vm_states.ACTIVE,
task_state=task_states.POWERING_OFF)
self.driver.power_update_event(instance, common.POWER_OFF)
self.assertEqual(nova_states.SHUTDOWN, instance.power_state)
self.assertEqual(vm_states.STOPPED, instance.vm_state)
self.assertIsNone(instance.task_state)
mock_save.assert_called_once_with(
expected_task_state=task_states.POWERING_OFF)
@mock.patch.object(loopingcall, 'FixedIntervalLoopingCall')
@mock.patch.object(ironic_driver.IronicDriver,
'_validate_instance_and_node')

View File

@ -865,6 +865,19 @@ class ComputeDriver(object):
"""
raise NotImplementedError()
def power_update_event(self, instance, target_power_state):
"""Update power, vm and task states of the specified instance.
Note that the driver is expected to set the task_state of the
instance back to None.
:param instance: nova.objects.instance.Instance
:param target_power_state: The desired target power state for the
instance; possible values are "POWER_ON"
and "POWER_OFF".
"""
raise NotImplementedError()
def trigger_crash_dump(self, instance):
"""Trigger crash dump mechanism on the given instance.

View File

@ -44,6 +44,7 @@ from nova.objects import fields as obj_fields
from nova.objects import migrate_data
from nova.virt import driver
from nova.virt import hardware
from nova.virt.ironic import driver as ironic
from nova.virt import virtapi
CONF = nova.conf.CONF
@ -687,6 +688,18 @@ class MediumFakeDriver(FakeDriver):
local_gb = 1028
class PowerUpdateFakeDriver(SmallFakeDriver):
# A specific fake driver for the power-update external event testing.
def __init__(self, virtapi):
super(PowerUpdateFakeDriver, self).__init__(virtapi=None)
self.driver = ironic.IronicDriver(virtapi=virtapi)
def power_update_event(self, instance, target_power_state):
"""Update power state of the specified instance in the nova DB."""
self.driver.power_update_event(instance, target_power_state)
class MediumFakeDriverWithNestedCustomResources(MediumFakeDriver):
# A MediumFakeDriver variant that also reports CUSTOM_MAGIC resources on
# a nested resource provider

View File

@ -45,6 +45,7 @@ from nova import context as nova_context
from nova import exception
from nova.i18n import _
from nova import objects
from nova.objects import external_event as external_event_obj
from nova.objects import fields as obj_fields
from nova import servicegroup
from nova import utils
@ -1474,6 +1475,26 @@ class IronicDriver(virt_driver.ComputeDriver):
LOG.info('Successfully powered on Ironic node %s',
node.uuid, instance=instance)
def power_update_event(self, instance, target_power_state):
"""Update power, vm and task states of the specified instance in
the nova DB.
"""
LOG.info('Power update called for instance with '
'target power state %s.', target_power_state,
instance=instance)
if target_power_state == external_event_obj.POWER_ON:
instance.power_state = power_state.RUNNING
instance.vm_state = vm_states.ACTIVE
instance.task_state = None
expected_task_state = task_states.POWERING_ON
else:
# It's POWER_OFF
instance.power_state = power_state.SHUTDOWN
instance.vm_state = vm_states.STOPPED
instance.task_state = None
expected_task_state = task_states.POWERING_OFF
instance.save(expected_task_state=expected_task_state)
def trigger_crash_dump(self, instance):
"""Trigger crash dump mechanism on the given instance.

View File

@ -0,0 +1,20 @@
---
features:
- |
It is now possible to signal and perform an update of an instance's power
state as of the 2.76 microversion using the ``power-update`` external
event. Currently it is only supported in the ironic driver and through
this event Ironic will send all "power-on to power-off" and
"power-off to power-on" type power state changes on a physical instance
to nova which will update its database accordingly. This way nova will
not be able to enforce an incorrect power state on the physical instance
during the periodic ``_sync_power_states`` task. The changes to the power
state of an instance caused by this event can be viewed through
``GET /servers/{server_id}/os-instance-actions`` and
``GET /servers/{server_id}/os-instance-actions/{request_id}``.
upgrade:
- |
Until all the ``nova-compute`` services that run the ironic driver are
upgraded to the Train code that handles the ``power-update`` callbacks from
ironic, the ``[nova]/send_power_notifications`` config option can be kept
disabled in ironic.