summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2019-08-15 23:39:50 +0000
committerGerrit Code Review <review@openstack.org>2019-08-15 23:39:50 +0000
commit35485eacfbd444ddbb7f2d1e8637bcecb295b485 (patch)
tree8ec348bb79d214d65c55daf72b989a1bad49b908
parent872a823d9a02f31e266882bfb499673c51fb4075 (diff)
parent62f6a0a1bc6c4b24621e1c2e927177f99501bef3 (diff)
downloadnova-35485eacfbd444ddbb7f2d1e8637bcecb295b485.tar.gz
Merge "API microversion 2.76: Add 'power-update' external event"
-rw-r--r--api-ref/source/os-server-external-events.inc6
-rw-r--r--api-ref/source/parameters.yaml20
-rw-r--r--doc/api_samples/versions/v21-version-get-resp.json2
-rw-r--r--doc/api_samples/versions/versions-get-resp.json2
-rw-r--r--nova/api/openstack/api_version_request.py7
-rw-r--r--nova/api/openstack/common.py3
-rw-r--r--nova/api/openstack/compute/rest_api_version_history.rst8
-rw-r--r--nova/api/openstack/compute/schemas/server_external_events.py4
-rw-r--r--nova/api/openstack/compute/server_external_events.py8
-rw-r--r--nova/compute/api.py17
-rw-r--r--nova/compute/manager.py103
-rw-r--r--nova/objects/external_event.py10
-rw-r--r--nova/tests/functional/api/client.py4
-rw-r--r--nova/tests/functional/test_server_external_events.py104
-rw-r--r--nova/tests/unit/api/openstack/compute/test_server_external_events.py48
-rw-r--r--nova/tests/unit/compute/test_compute_api.py81
-rw-r--r--nova/tests/unit/compute/test_compute_mgr.py144
-rw-r--r--nova/tests/unit/objects/test_objects.py2
-rw-r--r--nova/tests/unit/virt/ironic/test_driver.py15
-rw-r--r--nova/virt/driver.py13
-rw-r--r--nova/virt/fake.py13
-rw-r--r--nova/virt/ironic/driver.py21
-rw-r--r--releasenotes/notes/bp-nova-support-instance-power-update-8328355a0f3fb508.yaml20
23 files changed, 635 insertions, 20 deletions
diff --git a/api-ref/source/os-server-external-events.inc b/api-ref/source/os-server-external-events.inc
index b31c38116f..f04c98839f 100644
--- a/api-ref/source/os-server-external-events.inc
+++ b/api-ref/source/os-server-external-events.inc
@@ -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
diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml
index f2df3193ff..34fb3d1679 100644
--- a/api-ref/source/parameters.yaml
+++ b/api-ref/source/parameters.yaml
@@ -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
diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json
index ddcb8b65d8..b5c1ad05e1 100644
--- a/doc/api_samples/versions/v21-version-get-resp.json
+++ b/doc/api_samples/versions/v21-version-get-resp.json
@@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
- "version": "2.75",
+ "version": "2.76",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}
diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json
index 2df2d3c69a..f7b96be8f2 100644
--- a/doc/api_samples/versions/versions-get-resp.json
+++ b/doc/api_samples/versions/versions-get-resp.json
@@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
- "version": "2.75",
+ "version": "2.76",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}
diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py
index b3ddea6f5d..a41c029f49 100644
--- a/nova/api/openstack/api_version_request.py
+++ b/nova/api/openstack/api_version_request.py
@@ -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
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 36f6ed739a..c2bab9da0d 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -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',
diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst
index ace773fdbc..233abb0d16 100644
--- a/nova/api/openstack/compute/rest_api_version_history.rst
+++ b/nova/api/openstack/compute/rest_api_version_history.rst
@@ -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}``.
diff --git a/nova/api/openstack/compute/schemas/server_external_events.py b/nova/api/openstack/compute/schemas/server_external_events.py
index 38435e0dc5..ff02a08430 100644
--- a/nova/api/openstack/compute/schemas/server_external_events.py
+++ b/nova/api/openstack/compute/schemas/server_external_events.py
@@ -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')
diff --git a/nova/api/openstack/compute/server_external_events.py b/nova/api/openstack/compute/server_external_events.py
index 1b05556754..fdea25db8c 100644
--- a/nova/api/openstack/compute/server_external_events.py
+++ b/nova/api/openstack/compute/server_external_events.py
@@ -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']
diff --git a/nova/compute/api.py b/nova/compute/api.py
index ceff30e598..bac9546021 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -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
@@ -4711,6 +4712,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)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 2b05ab503c..f5a3908c41 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -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)
diff --git a/nova/objects/external_event.py b/nova/objects/external_event.py
index cfcc01c027..e2c7e95b11 100644
--- a/nova/objects/external_event.py
+++ b/nova/objects/external_event.py
@@ -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(),
diff --git a/nova/tests/functional/api/client.py b/nova/tests/functional/api/client.py
index 3609d09550..03221d9721 100644
--- a/nova/tests/functional/api/client.py
+++ b/nova/tests/functional/api/client.py
@@ -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']
diff --git a/nova/tests/functional/test_server_external_events.py b/nova/tests/functional/test_server_external_events.py
new file mode 100644
index 0000000000..0e25b6f10e
--- /dev/null
+++ b/nova/tests/functional/test_server_external_events.py
@@ -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')
diff --git a/nova/tests/unit/api/openstack/compute/test_server_external_events.py b/nova/tests/unit/api/openstack/compute/test_server_external_events.py
index 94f6b5c3a2..99695dd882 100644
--- a/nova/tests/unit/api/openstack/compute/test_server_external_events.py
+++ b/nova/tests/unit/api/openstack/compute/test_server_external_events.py
@@ -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))
diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py
index bc9fa98a5b..fecb74d30e 100644
--- a/nova/tests/unit/compute/test_compute_api.py
+++ b/nova/tests/unit/compute/test_compute_api.py
@@ -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
diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py
index 9dbe5a36fb..53e2715168 100644
--- a/nova/tests/unit/compute/test_compute_mgr.py
+++ b/nova/tests/unit/compute/test_compute_mgr.py
@@ -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):
diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py
index 1068032d47..b4e3c7c77f 100644
--- a/nova/tests/unit/objects/test_objects.py
+++ b/nova/tests/unit/objects/test_objects.py
@@ -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',
diff --git a/nova/tests/unit/virt/ironic/test_driver.py b/nova/tests/unit/virt/ironic/test_driver.py
index c3f4692c72..670a2e1323 100644
--- a/nova/tests/unit/virt/ironic/test_driver.py
+++ b/nova/tests/unit/virt/ironic/test_driver.py
@@ -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')
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
index d32ac27e63..4f05e8ffd8 100644
--- a/nova/virt/driver.py
+++ b/nova/virt/driver.py
@@ -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.
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 6f293de1b8..a404ab9c52 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -45,6 +45,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
@@ -688,6 +689,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
diff --git a/nova/virt/ironic/driver.py b/nova/virt/ironic/driver.py
index b985240bc5..f624ffad36 100644
--- a/nova/virt/ironic/driver.py
+++ b/nova/virt/ironic/driver.py
@@ -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.
diff --git a/releasenotes/notes/bp-nova-support-instance-power-update-8328355a0f3fb508.yaml b/releasenotes/notes/bp-nova-support-instance-power-update-8328355a0f3fb508.yaml
new file mode 100644
index 0000000000..51fc2949cf
--- /dev/null
+++ b/releasenotes/notes/bp-nova-support-instance-power-update-8328355a0f3fb508.yaml
@@ -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.