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):