diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/instance-instance-get-resp.json.tpl b/doc/api_samples/os-instance-actions/v2.21/instance-action-get-resp.json similarity index 99% rename from nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/instance-instance-get-resp.json.tpl rename to doc/api_samples/os-instance-actions/v2.21/instance-action-get-resp.json index f259deefdb26..cbb6236c4eef 100644 --- a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/instance-instance-get-resp.json.tpl +++ b/doc/api_samples/os-instance-actions/v2.21/instance-action-get-resp.json @@ -24,4 +24,4 @@ "start_time": "2012-12-05T00:00:00.000000", "user_id": "789" } -} \ No newline at end of file +} diff --git a/doc/api_samples/os-instance-actions/v2.21/instance-actions-list-resp.json b/doc/api_samples/os-instance-actions/v2.21/instance-actions-list-resp.json new file mode 100644 index 000000000000..b9ae31ac53db --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.21/instance-actions-list-resp.json @@ -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" + } + ] +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 1366d699779e..f22db3db2b74 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.20", + "version": "2.21", "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 4a33f304cdb3..bb902524ae3d 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.20", + "version": "2.21", "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 9f3cda6a7421..967128501540 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -63,6 +63,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 2.19 - Allow user to set and get the server description * 2.20 - Add attach and detach volume operations for instances in shelved 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 # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.20" +_MAX_API_VERSION = "2.21" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/nova/api/openstack/compute/instance_actions.py b/nova/api/openstack/compute/instance_actions.py index b915ea8dd702..a84c5b1c0e6b 100644 --- a/nova/api/openstack/compute/instance_actions.py +++ b/nova/api/openstack/compute/instance_actions.py @@ -20,6 +20,7 @@ from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova import compute from nova.i18n import _ +from nova import utils ALIAS = "os-instance-actions" authorize = extensions.os_compute_authorizer(ALIAS) @@ -49,11 +50,20 @@ class InstanceActionsController(wsgi.Controller): event[key] = event_raw.get(key) 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) def index(self, req, server_id): """Returns the list of actions recorded for a given instance.""" 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) actions_raw = self.action_api.actions_get(context, instance) 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): """Return data about the given instance action.""" 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) action = self.action_api.action_get_by_request_id(context, instance, id) diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index 4c386a1034d3..ad35316bca24 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -176,6 +176,10 @@ user documentation. 2.20 ---- - From this version of the API user can call detach and attach volumes for instances which are in shelved and shelved_offloaded state. + +2.21 +---- + The ``os-instance-actions`` API now returns information from deleted + instances. diff --git a/nova/tests/functional/api/client.py b/nova/tests/functional/api/client.py index 9feae1272281..4a43c2abf898 100644 --- a/nova/tests/functional/api/client.py +++ b/nova/tests/functional/api/client.py @@ -193,8 +193,11 @@ class TestOpenStackClient(object): resp.body = jsonutils.loads(response.content) 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]) + if api_version: + headers = kwargs.setdefault('headers', {}) + headers['X-OpenStack-Nova-API-Version'] = api_version return APIResponse(self.api_request(relative_uri, **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): 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' % - (server_id)).body['instanceActions'] + (server_id), api_version).body['instanceActions'] diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.21/instance-action-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.21/instance-action-get-resp.json.tpl new file mode 100644 index 000000000000..7cd532523906 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.21/instance-action-get-resp.json.tpl @@ -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": "" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.21/instance-actions-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.21/instance-actions-list-resp.json.tpl new file mode 100644 index 000000000000..0fdc33916a80 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.21/instance-actions-list-resp.json.tpl @@ -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": "" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl index c73b4f873cda..9c7dd41228d7 100644 --- a/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl +++ b/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.20", + "version": "2.21", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl index 8a69527a551c..84f2620a6e4d 100644 --- a/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl +++ b/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.20", + "version": "2.21", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/tests/functional/api_sample_tests/test_instance_actions.py b/nova/tests/functional/api_sample_tests/test_instance_actions.py index 63f2fea4d5d0..25cbe241d989 100644 --- a/nova/tests/functional/api_sample_tests/test_instance_actions.py +++ b/nova/tests/functional/api_sample_tests/test_instance_actions.py @@ -29,6 +29,7 @@ CONF.import_opt('osapi_compute_extension', class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21): + microversion = None ADMIN_API = True extension_name = 'os-instance-actions' @@ -39,6 +40,11 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21): 'contrib.instance_actions.Instance_actions') 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): super(ServerActionsSampleJsonTest, self).setUp() 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): 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', fake_instance_action_get_by_request_id) 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) self.stub_out('nova.db.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): 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] 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['action'] = '(reboot)|(resize)' subs['instance_uuid'] = fake_uuid @@ -91,7 +93,8 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21): def test_instance_actions_list(self): 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['action'] = '(reboot)|(resize)' subs['integer_id'] = '[0-9]+' @@ -99,3 +102,14 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21): '-[0-9a-f]{4}-[0-9a-f]{12}') self._verify_response('instance-actions-list-resp', subs, 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}) diff --git a/nova/tests/functional/test_instance_actions.py b/nova/tests/functional/test_instance_actions.py new file mode 100644 index 000000000000..625f394cbd38 --- /dev/null +++ b/nova/tests/functional/test_instance_actions.py @@ -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']) diff --git a/nova/tests/unit/api/openstack/compute/test_instance_actions.py b/nova/tests/unit/api/openstack/compute/test_instance_actions.py index 313fa9433bb7..220de4cec61d 100644 --- a/nova/tests/unit/api/openstack/compute/test_instance_actions.py +++ b/nova/tests/unit/api/openstack/compute/test_instance_actions.py @@ -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.legacy_v2.contrib import instance_actions \ as instance_actions_v2 +from nova.api.openstack import wsgi as os_wsgi from nova.compute import api as compute_api from nova.db.sqlalchemy import models from nova import exception @@ -130,6 +131,11 @@ class InstanceActionsPolicyTestV2(InstanceActionsPolicyTestV21): class InstanceActionsTestV21(test.NoDBTestCase): 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): super(InstanceActionsTestV21, self).setUp() @@ -137,22 +143,19 @@ class InstanceActionsTestV21(test.NoDBTestCase): self.fake_actions = copy.deepcopy(fake_server_actions.FAKE_ACTIONS) 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): return fake_instance.fake_instance_obj(None, **{'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', fake_instance_get_by_uuid) def _get_http_req(self, action, use_admin_context=False): fake_url = '/123/servers/12/%s' % action 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): rules = {'compute:get': '', @@ -246,6 +249,15 @@ class InstanceActionsTestV21(test.NoDBTestCase): 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): instance_actions = instance_actions_v2 diff --git a/nova/tests/unit/api/openstack/compute/test_versions.py b/nova/tests/unit/api/openstack/compute/test_versions.py index f94e4e003fca..7953e8ddc02f 100644 --- a/nova/tests/unit/api/openstack/compute/test_versions.py +++ b/nova/tests/unit/api/openstack/compute/test_versions.py @@ -66,7 +66,7 @@ EXP_VERSIONS = { "v2.1": { "id": "v2.1", "status": "CURRENT", - "version": "2.20", + "version": "2.21", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z", "links": [ @@ -128,7 +128,7 @@ class VersionsTestV20(test.NoDBTestCase): { "id": "v2.1", "status": "CURRENT", - "version": "2.20", + "version": "2.21", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z", "links": [ diff --git a/releasenotes/notes/instance-actions-read-deleted-instances-18bbb327924b66c7.yaml b/releasenotes/notes/instance-actions-read-deleted-instances-18bbb327924b66c7.yaml new file mode 100644 index 000000000000..1600076a6fb3 --- /dev/null +++ b/releasenotes/notes/instance-actions-read-deleted-instances-18bbb327924b66c7.yaml @@ -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.