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:
parent
b26bc7fd7a
commit
62f6a0a1bc
@ -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
|
||||
|
@ -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
|
||||
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.75",
|
||||
"version": "2.76",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.75",
|
||||
"version": "2.76",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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}``.
|
||||
|
@ -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')
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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']
|
||||
|
104
nova/tests/functional/test_server_external_events.py
Normal file
104
nova/tests/functional/test_server_external_events.py
Normal 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')
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user