diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index e83fc566ffbb..ba5e410eb880 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -312,6 +312,14 @@ "namespace": "http://docs.openstack.org/compute/ext/hypervisors/api/v1.1", "updated": "2012-06-21T00:00:00+00:00" }, + { + "alias": "os-instance-actions", + "description": "View a log of actions taken on an instance", + "links": [], + "name": "InstanceActions", + "namespace": "http://docs.openstack.org/compute/ext/instance-actions/api/v1.1", + "updated": "2013-02-08T00:00:00+00:00" + }, { "alias": "os-instance_usage_audit_log", "description": "Admin-only Task Log Monitoring.", diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index 0bd86e609a4c..a18e52437045 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -135,6 +135,9 @@ Admin-only hypervisor administration. + + View a log of actions taken on an instance + Admin-only Task Log Monitoring. diff --git a/doc/api_samples/os-instance-actions/instance-action-get-resp.json b/doc/api_samples/os-instance-actions/instance-action-get-resp.json new file mode 100644 index 000000000000..d5a2ff96c81e --- /dev/null +++ b/doc/api_samples/os-instance-actions/instance-action-get-resp.json @@ -0,0 +1,27 @@ +{ + "instanceAction": { + "action": "reboot", + "events": [ + { + "event": "schedule", + "finish_time": "2012-12-05 01:02:00.000000", + "result": "Success", + "start_time": "2012-12-05 01:00:02.000000", + "traceback": "" + }, + { + "event": "compute_create", + "finish_time": "2012-12-05 01:04:00.000000", + "result": "Success", + "start_time": "2012-12-05 01:03:00.000000", + "traceback": "" + } + ], + "instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13", + "message": "", + "project_id": "147", + "request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8", + "start_time": "2012-12-05 00:00:00.000000", + "user_id": "789" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-instance-actions/instance-action-get-resp.xml b/doc/api_samples/os-instance-actions/instance-action-get-resp.xml new file mode 100644 index 000000000000..720cdd39a082 --- /dev/null +++ b/doc/api_samples/os-instance-actions/instance-action-get-resp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/doc/api_samples/os-instance-actions/instance-actions-list-resp.json b/doc/api_samples/os-instance-actions/instance-actions-list-resp.json new file mode 100644 index 000000000000..22d29d076b0a --- /dev/null +++ b/doc/api_samples/os-instance-actions/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-05 01: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-05 00:00:00.000000", + "user_id": "789" + } + ] +} \ No newline at end of file diff --git a/doc/api_samples/os-instance-actions/instance-actions-list-resp.xml b/doc/api_samples/os-instance-actions/instance-actions-list-resp.xml new file mode 100644 index 000000000000..33896df919ad --- /dev/null +++ b/doc/api_samples/os-instance-actions/instance-actions-list-resp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 448013212ae8..2d3c4ed062ad 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -66,6 +66,8 @@ "compute_extension:hide_server_addresses": "is_admin:False", "compute_extension:hosts": "rule:admin_api", "compute_extension:hypervisors": "rule:admin_api", + "compute_extension:instance_actions": "", + "compute_extension:instance_actions:events": "rule:admin_api", "compute_extension:instance_usage_audit_log": "rule:admin_api", "compute_extension:keypairs": "", "compute_extension:multinic": "", diff --git a/nova/api/openstack/compute/contrib/instance_actions.py b/nova/api/openstack/compute/contrib/instance_actions.py new file mode 100644 index 000000000000..4ab32ad4cfd9 --- /dev/null +++ b/nova/api/openstack/compute/contrib/instance_actions.py @@ -0,0 +1,128 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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 webob import exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute +from nova import db + +authorize_actions = extensions.extension_authorizer('compute', + 'instance_actions') +authorize_events = extensions.soft_extension_authorizer('compute', + 'instance_actions:events') + +ACTION_KEYS = ['action', 'instance_uuid', 'request_id', 'user_id', + 'project_id', 'start_time', 'message'] +EVENT_KEYS = ['event', 'start_time', 'finish_time', 'result', 'traceback'] + + +def make_actions(elem): + for key in ACTION_KEYS: + elem.set(key) + + +def make_action(elem): + for key in ACTION_KEYS: + elem.set(key) + event = xmlutil.TemplateElement('events', selector='events') + for key in EVENT_KEYS: + event.set(key) + elem.append(event) + + +class InstanceActionsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('instanceActions') + elem = xmlutil.SubTemplateElement(root, 'instanceAction', + selector='instanceActions') + make_actions(elem) + return xmlutil.MasterTemplate(root, 1) + + +class InstanceActionTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('instanceAction', + selector='instanceAction') + make_action(root) + return xmlutil.MasterTemplate(root, 1) + + +class InstanceActionsController(wsgi.Controller): + + def __init__(self): + super(InstanceActionsController, self).__init__() + self.compute_api = compute.API() + + def _format_action(self, action_raw): + action = {} + for key in ACTION_KEYS: + if key in action_raw: + action[key] = action_raw[key] + return action + + def _format_event(self, event_raw): + event = {} + for key in EVENT_KEYS: + if key in event_raw: + event[key] = event_raw[key] + return event + + @wsgi.serializers(xml=InstanceActionsTemplate) + def index(self, req, server_id): + """Returns the list of actions recorded for a given instance.""" + context = req.environ["nova.context"] + instance = self.compute_api.get(context, server_id) + authorize_actions(context, target=instance) + actions_raw = db.actions_get(context, server_id) + actions = [self._format_action(action) for action in actions_raw] + return {'instanceActions': actions} + + @wsgi.serializers(xml=InstanceActionTemplate) + def show(self, req, server_id, id): + """Return data about the given instance action.""" + context = req.environ['nova.context'] + instance = self.compute_api.get(context, server_id) + authorize_actions(context, target=instance) + action = db.action_get_by_request_id(context, server_id, id) + if action is None: + raise exc.HTTPNotFound() + + action_id = action['id'] + action = self._format_action(action) + if authorize_events(context): + events_raw = db.action_events_get(context, action_id) + action['events'] = [self._format_event(evt) for evt in events_raw] + return {'instanceAction': action} + + +class Instance_actions(extensions.ExtensionDescriptor): + """View a log of actions and events taken on an instance.""" + + name = "InstanceActions" + alias = "os-instance-actions" + namespace = ("http://docs.openstack.org/compute/ext/" + "instance-actions/api/v1.1") + updated = "2013-02-08T00:00:00+00:00" + + def get_resources(self): + ext = extensions.ResourceExtension('os-instance-actions', + InstanceActionsController(), + parent=dict( + member_name='server', + collection_name='servers')) + return [ext] diff --git a/nova/db/api.py b/nova/db/api.py index ffd153a46c1b..b07cd6b8bd07 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -1630,9 +1630,9 @@ def actions_get(context, uuid): return IMPL.actions_get(context, uuid) -def action_get_by_id(context, uuid, action_id): - """Get the action by id and given instance.""" - return IMPL.action_get_by_id(context, uuid, action_id) +def action_get_by_request_id(context, uuid, request_id): + """Get the action by request_id and given instance.""" + return IMPL.action_get_by_request_id(context, uuid, request_id) def action_event_start(context, values): @@ -1646,6 +1646,7 @@ def action_event_finish(context, values): def action_events_get(context, action_id): + """Get the events by action id.""" return IMPL.action_events_get(context, action_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index d0a58e44f8a9..81d26d2d8346 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -4586,13 +4586,9 @@ def actions_get(context, instance_uuid): return actions -def action_get_by_id(context, instance_uuid, action_id): - """Get the action by id and given instance.""" - action = model_query(context, models.InstanceAction).\ - filter_by(instance_uuid=instance_uuid).\ - filter_by(id=action_id).\ - first() - +def action_get_by_request_id(context, instance_uuid, request_id): + """Get the action by request_id and given instance.""" + action = _action_get_by_request_id(context, instance_uuid, request_id) return action diff --git a/nova/tests/api/openstack/compute/contrib/test_instance_actions.py b/nova/tests/api/openstack/compute/contrib/test_instance_actions.py new file mode 100644 index 000000000000..b4db5daba1b8 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_instance_actions.py @@ -0,0 +1,231 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# 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. + +import copy +import uuid + +from lxml import etree +from webob import exc + +from nova.api.openstack.compute.contrib import instance_actions +from nova import db +from nova import exception +from nova.openstack.common import policy +from nova import test +from nova.tests.api.openstack import fakes +from nova.tests import fake_instance_actions + +FAKE_UUID = fake_instance_actions.FAKE_UUID +FAKE_REQUEST_ID = fake_instance_actions.FAKE_REQUEST_ID1 + + +def format_action(action): + '''Remove keys that aren't serialized.''' + if 'id' in action: + del(action['id']) + if 'finish_time' in action: + del(action['finish_time']) + return action + + +def format_event(event): + '''Remove keys that aren't serialized.''' + if 'id' in event: + del(event['id']) + return event + + +class InstanceActionsPolicyTest(test.TestCase): + def setUp(self): + super(InstanceActionsPolicyTest, self).setUp() + self.controller = instance_actions.InstanceActionsController() + + def test_list_actions_restricted_by_project(self): + rules = policy.Rules({'compute:get': policy.parse_rule(''), + 'compute_extension:instance_actions': + policy.parse_rule('project_id:%(project_id)s')}) + policy.set_rules(rules) + + def fake_instance_get_by_uuid(context, instance_id): + return {'name': 'fake', 'project_id': '%s_unequal' % + context.project_id} + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-instance-actions') + self.assertRaises(exception.NotAuthorized, self.controller.index, req, + str(uuid.uuid4())) + + def test_get_action_restricted_by_project(self): + rules = policy.Rules({'compute:get': policy.parse_rule(''), + 'compute_extension:instance_actions': + policy.parse_rule('project_id:%(project_id)s')}) + policy.set_rules(rules) + + def fake_instance_get_by_uuid(context, instance_id): + return {'name': 'fake', 'project_id': '%s_unequal' % + context.project_id} + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + req = fakes.HTTPRequest.blank( + '/v2/123/servers/12/os-instance-actions/1') + self.assertRaises(exception.NotAuthorized, self.controller.show, req, + str(uuid.uuid4()), '1') + + +class InstanceActionsTest(test.TestCase): + def setUp(self): + super(InstanceActionsTest, self).setUp() + self.controller = instance_actions.InstanceActionsController() + self.fake_actions = copy.deepcopy(fake_instance_actions.FAKE_ACTIONS) + self.fake_events = copy.deepcopy(fake_instance_actions.FAKE_EVENTS) + + def fake_instance_get_by_uuid(context, instance_id): + return {'name': 'fake', 'project_id': context.project_id} + + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + + def test_list_actions(self): + def fake_get_actions(context, uuid): + return self.fake_actions[uuid].values() + + self.stubs.Set(db, 'actions_get', fake_get_actions) + req = fakes.HTTPRequest.blank('/v2/123/servers/12/os-instance-actions') + res_dict = self.controller.index(req, FAKE_UUID) + for res in res_dict['instanceActions']: + fake_action = self.fake_actions[FAKE_UUID][res['request_id']] + fake_action = format_action(fake_action) + self.assertEqual(fake_action, res) + + def test_get_action_with_events_allowed(self): + def fake_get_action(context, uuid, request_id): + return self.fake_actions[uuid][request_id] + + def fake_get_events(context, action_id): + return self.fake_events[action_id] + + self.stubs.Set(db, 'action_get_by_request_id', fake_get_action) + self.stubs.Set(db, 'action_events_get', fake_get_events) + req = fakes.HTTPRequest.blank( + '/v2/123/servers/12/os-instance-actions/1', + use_admin_context=True) + res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID) + fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + fake_events = self.fake_events[fake_action['id']] + fake_events = [format_event(event) for event in fake_events] + fake_action = format_action(fake_action) + fake_action['events'] = fake_events + self.assertEqual(fake_action, res_dict['instanceAction']) + + def test_get_action_with_events_not_allowed(self): + def fake_get_action(context, uuid, request_id): + return self.fake_actions[uuid][request_id] + + def fake_get_events(context, action_id): + return self.fake_events[action_id] + + self.stubs.Set(db, 'action_get_by_request_id', fake_get_action) + self.stubs.Set(db, 'action_events_get', fake_get_events) + rules = policy.Rules({'compute:get': policy.parse_rule(''), + 'compute_extension:instance_actions': + policy.parse_rule(''), + 'compute_extension:instance_actions:events': + policy.parse_rule('is_admin:True')}) + policy.set_rules(rules) + req = fakes.HTTPRequest.blank( + '/v2/123/servers/12/os-instance-actions/1') + res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID) + fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + fake_action = format_action(fake_action) + self.assertEqual(fake_action, res_dict['instanceAction']) + + def test_action_not_found(self): + def fake_no_action(context, uuid, action_id): + return None + + self.stubs.Set(db, 'action_get_by_request_id', fake_no_action) + req = fakes.HTTPRequest.blank( + '/v2/123/servers/12/os-instance-actions/1') + self.assertRaises(exc.HTTPNotFound, self.controller.show, req, + FAKE_UUID, FAKE_REQUEST_ID) + + +class InstanceActionsSerializerTest(test.TestCase): + def setUp(self): + super(InstanceActionsSerializerTest, self).setUp() + self.fake_actions = copy.deepcopy(fake_instance_actions.FAKE_ACTIONS) + self.fake_events = copy.deepcopy(fake_instance_actions.FAKE_EVENTS) + + def _verify_instance_action_attachment(self, attach, tree): + for key in attach.keys(): + if key != 'events': + self.assertEqual(attach[key], tree.get(key), + '%s did not match' % key) + + def _verify_instance_action_event_attachment(self, attach, tree): + for key in attach.keys(): + self.assertEqual(attach[key], tree.get(key), + '%s did not match' % key) + + def test_instance_action_serializer(self): + serializer = instance_actions.InstanceActionTemplate() + action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + text = serializer.serialize({'instanceAction': action}) + tree = etree.fromstring(text) + + action = format_action(action) + self.assertEqual('instanceAction', tree.tag) + self._verify_instance_action_attachment(action, tree) + found_events = False + for child in tree: + if child.tag == 'events': + found_events = True + self.assertFalse(found_events) + + def test_instance_action_events_serializer(self): + serializer = instance_actions.InstanceActionTemplate() + action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] + event = self.fake_events[action['id']][0] + action['events'] = [event, event] + text = serializer.serialize({'instanceAction': action}) + tree = etree.fromstring(text) + + action = format_action(action) + self.assertEqual('instanceAction', tree.tag) + self._verify_instance_action_attachment(action, tree) + + event = format_event(event) + found_events = False + for child in tree: + if child.tag == 'events': + found_events = True + for key in event: + self.assertEqual(event[key], child.get(key)) + self.assertTrue(found_events) + + def test_instance_actions_serializer(self): + serializer = instance_actions.InstanceActionsTemplate() + action_list = self.fake_actions[FAKE_UUID].values() + text = serializer.serialize({'instanceActions': action_list}) + tree = etree.fromstring(text) + + action_list = [format_action(action) for action in action_list] + self.assertEqual('instanceActions', tree.tag) + self.assertEqual(len(action_list), len(tree)) + for idx, child in enumerate(tree): + self.assertEqual('instanceAction', child.tag) + request_id = child.get('request_id') + self._verify_instance_action_attachment( + self.fake_actions[FAKE_UUID][request_id], + child) diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py index 97be7376ea81..9c45edc08bd8 100644 --- a/nova/tests/api/openstack/compute/test_extensions.py +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -185,6 +185,7 @@ class ExtensionControllerTest(ExtensionTestCase): "FloatingIpsBulk", "Fox In Socks", "Hosts", + "InstanceActions", "Keypairs", "Multinic", "MultipleCreate", diff --git a/nova/tests/fake_instance_actions.py b/nova/tests/fake_instance_actions.py index 1667ac62d6e1..f34d9b213e20 100644 --- a/nova/tests/fake_instance_actions.py +++ b/nova/tests/fake_instance_actions.py @@ -17,6 +17,64 @@ from nova import db +FAKE_UUID = 'b48316c5-71e8-45e4-9884-6c78055b9b13' +FAKE_REQUEST_ID1 = 'req-3293a3f1-b44c-4609-b8d2-d81b105636b8' +FAKE_REQUEST_ID2 = 'req-25517360-b757-47d3-be45-0e8d2a01b36a' +FAKE_ACTION_ID1 = 'f811a359-0c98-4daa-87a4-2948d4c21b78' +FAKE_ACTION_ID2 = '4e9594b5-4ac5-421c-ac60-2d802b11c798' + +FAKE_ACTIONS = { + FAKE_UUID: { + FAKE_REQUEST_ID1: {'id': FAKE_ACTION_ID1, + 'action': 'reboot', + 'instance_uuid': FAKE_UUID, + 'request_id': FAKE_REQUEST_ID1, + 'project_id': '147', + 'user_id': '789', + 'start_time': '2012-12-05 00:00:00.000000', + 'finish_time': '', + 'message': '', + }, + FAKE_REQUEST_ID2: {'id': FAKE_ACTION_ID2, + 'action': 'resize', + 'instance_uuid': FAKE_UUID, + 'request_id': FAKE_REQUEST_ID2, + 'user_id': '789', + 'project_id': '842', + 'start_time': '2012-12-05 01:00:00.000000', + 'finish_time': '', + 'message': '', + } + } +} + +FAKE_EVENTS = { + FAKE_ACTION_ID1: [{'id': '1', + 'event': 'schedule', + 'start_time': '2012-12-05 01:00:02.000000', + 'finish_time': '2012-12-05 01:02:00.000000', + 'result': 'Success', + 'traceback': '', + }, + {'id': '2', + 'event': 'compute_create', + 'start_time': '2012-12-05 01:03:00.000000', + 'finish_time': '2012-12-05 01:04:00.000000', + 'result': 'Success', + 'traceback': '', + } + ], + FAKE_ACTION_ID2: [{'id': '3', + 'event': 'schedule', + 'start_time': '2012-12-05 03:00:00.000000', + 'finish_time': '2012-12-05 03:02:00.000000', + 'result': 'Error', + 'traceback': '' + } + ] +} + + def fake_action_event_start(*args): pass diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index a3718b877681..3878df531d99 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -143,6 +143,8 @@ policy_data = """ "compute_extension:hide_server_addresses": "", "compute_extension:hosts": "", "compute_extension:hypervisors": "", + "compute_extension:instance_actions": "", + "compute_extension:instance_actions:events": "is_admin:True", "compute_extension:instance_usage_audit_log": "", "compute_extension:keypairs": "", "compute_extension:multinic": "", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index 4b5160410c4b..17914de426d4 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -463,6 +463,14 @@ "name": "Volumes", "namespace": "http://docs.openstack.org/compute/ext/volumes/api/v1.1", "updated": "%(timestamp)s" + }, + { + "alias": "os-instance-actions", + "description": "%(text)s", + "links": [], + "name": "InstanceActions", + "namespace": "http://docs.openstack.org/compute/ext/instance-actions/api/v1.1", + "updated": "%(timestamp)s" } ] } diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index 4066858e7601..4492ed3aaaae 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -174,4 +174,7 @@ %(text)s + + %(text)s + diff --git a/nova/tests/integrated/api_samples/os-instance-actions/instance-action-get-resp.json.tpl b/nova/tests/integrated/api_samples/os-instance-actions/instance-action-get-resp.json.tpl new file mode 100644 index 000000000000..6ba99d264da6 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-instance-actions/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": "%(start_time)s", + "message": "", + "events": [ + { + "event": "%(event)s", + "start_time": "%(timestamp)s", + "finish_time": "%(timestamp)s", + "result": "%(result)s", + "traceback": "" + }, + { + "event": "%(event)s", + "start_time": "%(timestamp)s", + "finish_time": "%(timestamp)s", + "result": "%(result)s", + "traceback": "" + } + ] + } +} diff --git a/nova/tests/integrated/api_samples/os-instance-actions/instance-action-get-resp.xml.tpl b/nova/tests/integrated/api_samples/os-instance-actions/instance-action-get-resp.xml.tpl new file mode 100644 index 000000000000..ef4b7b0032ce --- /dev/null +++ b/nova/tests/integrated/api_samples/os-instance-actions/instance-action-get-resp.xml.tpl @@ -0,0 +1,5 @@ + + + + + diff --git a/nova/tests/integrated/api_samples/os-instance-actions/instance-actions-list-resp.json.tpl b/nova/tests/integrated/api_samples/os-instance-actions/instance-actions-list-resp.json.tpl new file mode 100644 index 000000000000..9f64a1b2982c --- /dev/null +++ b/nova/tests/integrated/api_samples/os-instance-actions/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": "%(timestamp)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": "%(timestamp)s", + "message": "" + } + ] +} diff --git a/nova/tests/integrated/api_samples/os-instance-actions/instance-actions-list-resp.xml.tpl b/nova/tests/integrated/api_samples/os-instance-actions/instance-actions-list-resp.xml.tpl new file mode 100644 index 000000000000..943b1ba7478b --- /dev/null +++ b/nova/tests/integrated/api_samples/os-instance-actions/instance-actions-list-resp.xml.tpl @@ -0,0 +1,5 @@ + + + + + diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 8ed8bd628ca9..e3022d48eced 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -14,6 +14,7 @@ # under the License. import base64 +import copy import datetime import inspect import json @@ -48,9 +49,11 @@ from nova.tests.api.openstack.compute.contrib import test_fping from nova.tests.api.openstack.compute.contrib import test_networks from nova.tests.api.openstack.compute.contrib import test_services from nova.tests.baremetal.db import base as bm_db_base +from nova.tests import fake_instance_actions from nova.tests import fake_network from nova.tests.image import fake from nova.tests.integrated import integrated_helpers +from nova.tests import utils as test_utils from nova import utils CONF = cfg.CONF @@ -3106,3 +3109,67 @@ class FloatingIpDNSJsonTest(ApiSampleTestBase): class FloatingIpDNSXmlTest(FloatingIpDNSJsonTest): ctype = 'xml' + + +class InstanceActionsSampleJsonTest(ApiSampleTestBase): + extension_name = ('nova.api.openstack.compute.contrib.instance_actions.' + 'Instance_actions') + + def setUp(self): + super(InstanceActionsSampleJsonTest, self).setUp() + self.actions = fake_instance_actions.FAKE_ACTIONS + self.events = fake_instance_actions.FAKE_EVENTS + self.instance = test_utils.get_test_instance() + + def fake_instance_action_get_by_request_id(context, uuid, request_id): + return copy.deepcopy(self.actions[uuid][request_id]) + + def fake_instance_actions_get(context, uuid): + return [copy.deepcopy(value) for value in + self.actions[uuid].itervalues()] + + def fake_instance_action_events_get(context, action_id): + return copy.deepcopy(self.events[action_id]) + + def fake_instance_get_by_uuid(context, instance_id): + return self.instance + + self.stubs.Set(db, 'action_get_by_request_id', + fake_instance_action_get_by_request_id) + self.stubs.Set(db, 'actions_get', fake_instance_actions_get) + self.stubs.Set(db, 'action_events_get', + fake_instance_action_events_get) + self.stubs.Set(db, 'instance_get_by_uuid', fake_instance_get_by_uuid) + + def test_instance_action_get(self): + fake_uuid = fake_instance_actions.FAKE_UUID + fake_request_id = fake_instance_actions.FAKE_REQUEST_ID1 + fake_action = self.actions[fake_uuid][fake_request_id] + + response = self._do_get('servers/%s/os-instance-actions/%s' % + (fake_uuid, fake_request_id)) + subs = self._get_regexes() + subs['action'] = '(reboot)|(resize)' + subs['instance_uuid'] = fake_uuid + subs['integer_id'] = '[0-9]+' + subs['request_id'] = fake_action['request_id'] + subs['start_time'] = fake_action['start_time'] + subs['result'] = '(Success)|(Error)' + subs['event'] = '(schedule)|(compute_create)' + return self._verify_response('instance-action-get-resp', subs, + response) + + def test_instance_actions_list(self): + fake_uuid = fake_instance_actions.FAKE_UUID + response = self._do_get('servers/%s/os-instance-actions' % (fake_uuid)) + subs = self._get_regexes() + subs['action'] = '(reboot)|(resize)' + subs['integer_id'] = '[0-9]+' + subs['request_id'] = ('req-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}' + '-[0-9a-f]{4}-[0-9a-f]{12}') + return self._verify_response('instance-actions-list-resp', subs, + response) + + +class InstanceActionsSampleXmlTest(InstanceActionsSampleJsonTest): + ctype = 'xml' diff --git a/nova/tests/test_db_api.py b/nova/tests/test_db_api.py index c6bf2941e888..22de9346f50a 100644 --- a/nova/tests/test_db_api.py +++ b/nova/tests/test_db_api.py @@ -685,9 +685,9 @@ class DbApiTestCase(test.TestCase): db.action_start(ctxt2, action_values) actions = db.actions_get(ctxt1, uuid1) - action_id = actions[0]['id'] - action = db.action_get_by_id(ctxt1, uuid1, action_id) - self.assertEqual('resize', action['action']) + request_id = actions[0]['request_id'] + action = db.action_get_by_request_id(ctxt1, uuid1, request_id) + self.assertEqual('run_instance', action['action']) self.assertEqual(ctxt1.request_id, action['request_id']) def test_instance_action_event_start(self):