Make os-instance-action read deleted instances.

Add a microversion change to the os-instance-actions API so that we
mutate the context to set 'read_deleted="yes"' when looking up the
instance.

Blueprint: os-instance-actions-read-deleted-instances
Change-Id: I607a28bbe06e20e17ee47a283e06b1d42b5c0e84
This commit is contained in:
Mark Doffman 2015-12-01 15:45:11 -06:00 committed by Sean Dague
parent 07573aff23
commit 934a0e4ede
17 changed files with 228 additions and 28 deletions

View File

@ -0,0 +1,22 @@
{
"instanceActions": [
{
"action": "resize",
"instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
"message": "",
"project_id": "842",
"request_id": "req-25517360-b757-47d3-be45-0e8d2a01b36a",
"start_time": "2012-12-05T01:00:00.000000",
"user_id": "789"
},
{
"action": "reboot",
"instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
"message": "",
"project_id": "147",
"request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8",
"start_time": "2012-12-05T00:00:00.000000",
"user_id": "789"
}
]
}

View File

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

View File

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

View File

@ -63,6 +63,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.19 - Allow user to set and get the server description * 2.19 - Allow user to set and get the server description
* 2.20 - Add attach and detach volume operations for instances in shelved * 2.20 - Add attach and detach volume operations for instances in shelved
and shelved_offloaded state and shelved_offloaded state
* 2.21 - Make os-instance-actions read deleted instances
""" """
@ -72,7 +73,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.20" _MAX_API_VERSION = "2.21"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -20,6 +20,7 @@ from nova.api.openstack import extensions
from nova.api.openstack import wsgi from nova.api.openstack import wsgi
from nova import compute from nova import compute
from nova.i18n import _ from nova.i18n import _
from nova import utils
ALIAS = "os-instance-actions" ALIAS = "os-instance-actions"
authorize = extensions.os_compute_authorizer(ALIAS) authorize = extensions.os_compute_authorizer(ALIAS)
@ -49,11 +50,20 @@ class InstanceActionsController(wsgi.Controller):
event[key] = event_raw.get(key) event[key] = event_raw.get(key)
return event return event
@wsgi.Controller.api_version("2.1", "2.20")
def _get_instance(self, req, context, server_id):
return common.get_instance(self.compute_api, context, server_id)
@wsgi.Controller.api_version("2.21") # noqa
def _get_instance(self, req, context, server_id):
with utils.temporary_mutation(context, read_deleted='yes'):
return common.get_instance(self.compute_api, context, server_id)
@extensions.expected_errors(404) @extensions.expected_errors(404)
def index(self, req, server_id): def index(self, req, server_id):
"""Returns the list of actions recorded for a given instance.""" """Returns the list of actions recorded for a given instance."""
context = req.environ["nova.context"] context = req.environ["nova.context"]
instance = common.get_instance(self.compute_api, context, server_id) instance = self._get_instance(req, context, server_id)
authorize(context, target=instance) authorize(context, target=instance)
actions_raw = self.action_api.actions_get(context, instance) actions_raw = self.action_api.actions_get(context, instance)
actions = [self._format_action(action) for action in actions_raw] actions = [self._format_action(action) for action in actions_raw]
@ -63,7 +73,7 @@ class InstanceActionsController(wsgi.Controller):
def show(self, req, server_id, id): def show(self, req, server_id, id):
"""Return data about the given instance action.""" """Return data about the given instance action."""
context = req.environ['nova.context'] context = req.environ['nova.context']
instance = common.get_instance(self.compute_api, context, server_id) instance = self._get_instance(req, context, server_id)
authorize(context, target=instance) authorize(context, target=instance)
action = self.action_api.action_get_by_request_id(context, instance, action = self.action_api.action_get_by_request_id(context, instance,
id) id)

View File

@ -176,6 +176,10 @@ user documentation.
2.20 2.20
---- ----
From this version of the API user can call detach and attach volumes for From this version of the API user can call detach and attach volumes for
instances which are in shelved and shelved_offloaded state. instances which are in shelved and shelved_offloaded state.
2.21
----
The ``os-instance-actions`` API now returns information from deleted
instances.

View File

@ -193,8 +193,11 @@ class TestOpenStackClient(object):
resp.body = jsonutils.loads(response.content) resp.body = jsonutils.loads(response.content)
return resp return resp
def api_get(self, relative_uri, **kwargs): def api_get(self, relative_uri, api_version=None, **kwargs):
kwargs.setdefault('check_response_status', [200]) kwargs.setdefault('check_response_status', [200])
if api_version:
headers = kwargs.setdefault('headers', {})
headers['X-OpenStack-Nova-API-Version'] = api_version
return APIResponse(self.api_request(relative_uri, **kwargs)) return APIResponse(self.api_request(relative_uri, **kwargs))
def api_post(self, relative_uri, body, api_version=None, **kwargs): def api_post(self, relative_uri, body, api_version=None, **kwargs):
@ -360,6 +363,6 @@ class TestOpenStackClient(object):
def delete_server_group(self, group_id): def delete_server_group(self, group_id):
self.api_delete('/os-server-groups/%s' % group_id) self.api_delete('/os-server-groups/%s' % group_id)
def get_instance_actions(self, server_id): def get_instance_actions(self, server_id, api_version=None):
return self.api_get('/servers/%s/os-instance-actions' % return self.api_get('/servers/%s/os-instance-actions' %
(server_id)).body['instanceActions'] (server_id), api_version).body['instanceActions']

View File

@ -0,0 +1,27 @@
{
"instanceAction": {
"action": "%(action)s",
"instance_uuid": "%(instance_uuid)s",
"request_id": "%(request_id)s",
"user_id": "%(integer_id)s",
"project_id": "%(integer_id)s",
"start_time": "%(strtime)s",
"message": "",
"events": [
{
"event": "%(event)s",
"start_time": "%(strtime)s",
"finish_time": "%(strtime)s",
"result": "%(result)s",
"traceback": ""
},
{
"event": "%(event)s",
"start_time": "%(strtime)s",
"finish_time": "%(strtime)s",
"result": "%(result)s",
"traceback": ""
}
]
}
}

View File

@ -0,0 +1,22 @@
{
"instanceActions": [
{
"action": "%(action)s",
"instance_uuid": "%(uuid)s",
"request_id": "%(request_id)s",
"user_id": "%(integer_id)s",
"project_id": "%(integer_id)s",
"start_time": "%(strtime)s",
"message": ""
},
{
"action": "%(action)s",
"instance_uuid": "%(uuid)s",
"request_id": "%(request_id)s",
"user_id": "%(integer_id)s",
"project_id": "%(integer_id)s",
"start_time": "%(strtime)s",
"message": ""
}
]
}

View File

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

View File

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

View File

@ -29,6 +29,7 @@ CONF.import_opt('osapi_compute_extension',
class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21): class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
microversion = None
ADMIN_API = True ADMIN_API = True
extension_name = 'os-instance-actions' extension_name = 'os-instance-actions'
@ -39,6 +40,11 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
'contrib.instance_actions.Instance_actions') 'contrib.instance_actions.Instance_actions')
return f return f
def _fake_get(self, context, instance_uuid, expected_attrs=None,
want_objects=True):
return fake_instance.fake_instance_obj(
None, **{'uuid': instance_uuid})
def setUp(self): def setUp(self):
super(ServerActionsSampleJsonTest, self).setUp() super(ServerActionsSampleJsonTest, self).setUp()
self.actions = fake_server_actions.FAKE_ACTIONS self.actions = fake_server_actions.FAKE_ACTIONS
@ -58,11 +64,6 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
def fake_instance_get_by_uuid(context, instance_id): def fake_instance_get_by_uuid(context, instance_id):
return self.instance return self.instance
def fake_get(self, context, instance_uuid, expected_attrs=None,
want_objects=True):
return fake_instance.fake_instance_obj(
None, **{'uuid': instance_uuid})
self.stub_out('nova.db.action_get_by_request_id', self.stub_out('nova.db.action_get_by_request_id',
fake_instance_action_get_by_request_id) fake_instance_action_get_by_request_id)
self.stub_out('nova.db.actions_get', fake_server_actions_get) self.stub_out('nova.db.actions_get', fake_server_actions_get)
@ -70,7 +71,7 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
fake_instance_action_events_get) fake_instance_action_events_get)
self.stub_out('nova.db.instance_get_by_uuid', self.stub_out('nova.db.instance_get_by_uuid',
fake_instance_get_by_uuid) fake_instance_get_by_uuid)
self.stub_out('nova.compute.api.API.get', fake_get) self.stub_out('nova.compute.api.API.get', self._fake_get)
def test_instance_action_get(self): def test_instance_action_get(self):
fake_uuid = fake_server_actions.FAKE_UUID fake_uuid = fake_server_actions.FAKE_UUID
@ -78,7 +79,8 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
fake_action = self.actions[fake_uuid][fake_request_id] fake_action = self.actions[fake_uuid][fake_request_id]
response = self._do_get('servers/%s/os-instance-actions/%s' % response = self._do_get('servers/%s/os-instance-actions/%s' %
(fake_uuid, fake_request_id)) (fake_uuid, fake_request_id),
api_version=self.microversion)
subs = {} subs = {}
subs['action'] = '(reboot)|(resize)' subs['action'] = '(reboot)|(resize)'
subs['instance_uuid'] = fake_uuid subs['instance_uuid'] = fake_uuid
@ -91,7 +93,8 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
def test_instance_actions_list(self): def test_instance_actions_list(self):
fake_uuid = fake_server_actions.FAKE_UUID fake_uuid = fake_server_actions.FAKE_UUID
response = self._do_get('servers/%s/os-instance-actions' % (fake_uuid)) response = self._do_get('servers/%s/os-instance-actions' % (fake_uuid),
api_version=self.microversion)
subs = {} subs = {}
subs['action'] = '(reboot)|(resize)' subs['action'] = '(reboot)|(resize)'
subs['integer_id'] = '[0-9]+' subs['integer_id'] = '[0-9]+'
@ -99,3 +102,14 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
'-[0-9a-f]{4}-[0-9a-f]{12}') '-[0-9a-f]{4}-[0-9a-f]{12}')
self._verify_response('instance-actions-list-resp', subs, self._verify_response('instance-actions-list-resp', subs,
response, 200) response, 200)
class ServerActionsV221SampleJsonTest(ServerActionsSampleJsonTest):
microversion = '2.21'
scenarios = [('v2_21', {'api_major_version': 'v2.1'})]
def _fake_get(self, context, instance_uuid, expected_attrs=None,
want_objects=True):
self.assertEqual('yes', context.read_deleted)
return fake_instance.fake_instance_obj(
None, **{'uuid': instance_uuid})

View File

@ -0,0 +1,76 @@
# Copyright 2016 IBM Corp.
#
# 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.tests.functional.api import client
from nova.tests.functional import test_servers
from nova.tests.unit import fake_network
class InstanceActionsTestV2(test_servers.ServersTestBase):
"""Tests Instance Actions API"""
def _create_server(self):
"""Creates a minimal test server via the compute API
Ensures the server is created and can be retrieved from the compute API
and waits for it to be ACTIVE.
:returns: created server (dict)
"""
# TODO(mriedem): We should pull this up into the parent class so we
# don't have so much copy/paste in these functional tests.
fake_network.set_stub_network_methods(self)
# Create a server
server = self._build_minimal_create_server_request()
created_server = self.api.post_server({'server': server})
self.assertTrue(created_server['id'])
created_server_id = created_server['id']
# Check it's there
found_server = self.api.get_server(created_server_id)
self.assertEqual(created_server_id, found_server['id'])
found_server = self._wait_for_state_change(found_server, 'BUILD')
# It should be available...
self.assertEqual('ACTIVE', found_server['status'])
return found_server
def test_get_instance_actions(self):
server = self._create_server()
actions = self.api.get_instance_actions(server['id'])
self.assertEqual('create', actions[0]['action'])
def test_get_instance_actions_deleted(self):
server = self._create_server()
self._delete_server(server['id'])
self.assertRaises(client.OpenStackApiNotFoundException,
self.api.get_instance_actions,
server['id'])
class InstanceActionsTestV21(InstanceActionsTestV2):
api_major_version = 'v2.1'
class InstanceActionsTestV221(InstanceActionsTestV21):
microversion = '2.21'
def test_get_instance_actions_deleted(self):
server = self._create_server()
self._delete_server(server['id'])
actions = self.api.get_instance_actions(server['id'],
api_version=self.microversion)
self.assertEqual('delete', actions[0]['action'])
self.assertEqual('create', actions[1]['action'])

View File

@ -23,6 +23,7 @@ from webob import exc
from nova.api.openstack.compute import instance_actions as instance_actions_v21 from nova.api.openstack.compute import instance_actions as instance_actions_v21
from nova.api.openstack.compute.legacy_v2.contrib import instance_actions \ from nova.api.openstack.compute.legacy_v2.contrib import instance_actions \
as instance_actions_v2 as instance_actions_v2
from nova.api.openstack import wsgi as os_wsgi
from nova.compute import api as compute_api from nova.compute import api as compute_api
from nova.db.sqlalchemy import models from nova.db.sqlalchemy import models
from nova import exception from nova import exception
@ -130,6 +131,11 @@ class InstanceActionsPolicyTestV2(InstanceActionsPolicyTestV21):
class InstanceActionsTestV21(test.NoDBTestCase): class InstanceActionsTestV21(test.NoDBTestCase):
instance_actions = instance_actions_v21 instance_actions = instance_actions_v21
wsgi_api_version = os_wsgi.DEFAULT_API_VERSION
def fake_get(self, context, instance_uuid, expected_attrs=None,
want_objects=False):
return objects.Instance(uuid=instance_uuid)
def setUp(self): def setUp(self):
super(InstanceActionsTestV21, self).setUp() super(InstanceActionsTestV21, self).setUp()
@ -137,22 +143,19 @@ class InstanceActionsTestV21(test.NoDBTestCase):
self.fake_actions = copy.deepcopy(fake_server_actions.FAKE_ACTIONS) self.fake_actions = copy.deepcopy(fake_server_actions.FAKE_ACTIONS)
self.fake_events = copy.deepcopy(fake_server_actions.FAKE_EVENTS) self.fake_events = copy.deepcopy(fake_server_actions.FAKE_EVENTS)
def fake_get(self, context, instance_uuid, expected_attrs=None,
want_objects=False):
return objects.Instance(uuid=instance_uuid)
def fake_instance_get_by_uuid(context, instance_id, use_slave=False): def fake_instance_get_by_uuid(context, instance_id, use_slave=False):
return fake_instance.fake_instance_obj(None, return fake_instance.fake_instance_obj(None,
**{'name': 'fake', 'project_id': context.project_id}) **{'name': 'fake', 'project_id': context.project_id})
self.stubs.Set(compute_api.API, 'get', fake_get) self.stubs.Set(compute_api.API, 'get', self.fake_get)
self.stub_out('nova.db.instance_get_by_uuid', self.stub_out('nova.db.instance_get_by_uuid',
fake_instance_get_by_uuid) fake_instance_get_by_uuid)
def _get_http_req(self, action, use_admin_context=False): def _get_http_req(self, action, use_admin_context=False):
fake_url = '/123/servers/12/%s' % action fake_url = '/123/servers/12/%s' % action
return fakes.HTTPRequest.blank(fake_url, return fakes.HTTPRequest.blank(fake_url,
use_admin_context=use_admin_context) use_admin_context=use_admin_context,
version=self.wsgi_api_version)
def _set_policy_rules(self): def _set_policy_rules(self):
rules = {'compute:get': '', rules = {'compute:get': '',
@ -246,6 +249,15 @@ class InstanceActionsTestV21(test.NoDBTestCase):
FAKE_UUID, 'fake') FAKE_UUID, 'fake')
class InstanceActionsTestV221(InstanceActionsTestV21):
wsgi_api_version = "2.21"
def fake_get(self, context, instance_uuid, expected_attrs=None,
want_objects=False):
self.assertEqual('yes', context.read_deleted)
return objects.Instance(uuid=instance_uuid)
class InstanceActionsTestV2(InstanceActionsTestV21): class InstanceActionsTestV2(InstanceActionsTestV21):
instance_actions = instance_actions_v2 instance_actions = instance_actions_v2

View File

@ -66,7 +66,7 @@ EXP_VERSIONS = {
"v2.1": { "v2.1": {
"id": "v2.1", "id": "v2.1",
"status": "CURRENT", "status": "CURRENT",
"version": "2.20", "version": "2.21",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z", "updated": "2013-07-23T11:33:21Z",
"links": [ "links": [
@ -128,7 +128,7 @@ class VersionsTestV20(test.NoDBTestCase):
{ {
"id": "v2.1", "id": "v2.1",
"status": "CURRENT", "status": "CURRENT",
"version": "2.20", "version": "2.21",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z", "updated": "2013-07-23T11:33:21Z",
"links": [ "links": [

View File

@ -0,0 +1,9 @@
---
features:
- The os-instance-actions methods now read actions from deleted instances.
This means that
'GET /v2.1/{tenant-id}/servers/{server-id}/os-instance-actions'
and
'GET /v2.1/{tenant-id}/servers/{server-id}/os-instance-actions/{req-id}'
will return instance-action items even if the instance corresponding to
'{server-id}' has been deleted.