ReST API: Add Events

Change-Id: I716dc2ad1c9294a7a9df27fbb77e32926b1ba307
Signed-off-by: Zane Bitter <zbitter@redhat.com>
This commit is contained in:
Zane Bitter 2012-11-22 11:06:57 +01:00
parent f454e5ac3a
commit cbef4bef02
4 changed files with 623 additions and 1 deletions

View File

@ -223,3 +223,66 @@ Parameters:
* `stack_name` The name of the stack to look up * `stack_name` The name of the stack to look up
* `stack_id` The unique identifier of the stack to look up * `stack_id` The unique identifier of the stack to look up
* `resource_name` The name of the resource in the template * `resource_name` The name of the resource in the template
List Stack Events
-----------------
```
GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/events
```
Parameters:
* `tenant_id` The unique identifier of the tenant or account
* `stack_name` The name of the stack to look up
* `stack_id` The unique identifier of the stack to look up
Find Stack Events by Name
-------------------------
```
GET /v1/{tenant_id}/stacks/{stack_name}/events
```
Parameters:
* `stack_name` The name of the stack to look up
Result:
```
HTTP/1.1 302 Found
Location: http://heat.example.com:8004/v1/{tenant_id}/stacks/{stack_name}/{stack_id}/events
```
This is a shortcut to go directly to the list of stack events when only the stack name is known.
List Resource Events
--------------------
```
GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events
```
Parameters:
* `tenant_id` The unique identifier of the tenant or account
* `stack_name` The name of the stack to look up
* `stack_id` The unique identifier of the stack to look up
* `resource_name` The name of the resource in the template
Get Event
---------
```
GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events/{event_id}
```
Parameters:
* `tenant_id` The unique identifier of the tenant or account
* `stack_name` The name of the stack to look up
* `stack_id` The unique identifier of the stack to look up
* `resource_name` The name of the resource in the template
* `event_id` The ID of the event

View File

@ -20,6 +20,7 @@ gettext.install('heat', unicode=1)
from heat.api.openstack.v1 import stacks from heat.api.openstack.v1 import stacks
from heat.api.openstack.v1 import resources from heat.api.openstack.v1 import resources
from heat.api.openstack.v1 import events
from heat.common import wsgi from heat.common import wsgi
from heat.openstack.common import log as logging from heat.openstack.common import log as logging
@ -61,8 +62,10 @@ class API(wsgi.Router):
stack_mapper.connect("stack_lookup", stack_mapper.connect("stack_lookup",
"/stacks/{stack_name}", "/stacks/{stack_name}",
action="lookup") action="lookup")
subpaths = ['resources', 'events']
path = "{path:%s}" % '|'.join(subpaths)
stack_mapper.connect("stack_lookup_subpath", stack_mapper.connect("stack_lookup_subpath",
"/stacks/{stack_name}/{path:resources}", "/stacks/{stack_name}/" + path,
action="lookup", action="lookup",
conditions={'method': 'GET'}) conditions={'method': 'GET'})
stack_mapper.connect("stack_show", stack_mapper.connect("stack_show",
@ -106,4 +109,26 @@ class API(wsgi.Router):
action="metadata", action="metadata",
conditions={'method': 'GET'}) conditions={'method': 'GET'})
# Events
events_resource = events.create_resource(conf)
with mapper.submapper(controller=events_resource,
path_prefix=stack_path) as ev_mapper:
# Stack event collection
ev_mapper.connect("event_index_stack",
"/events",
action="index",
conditions={'method': 'GET'})
# Resource event collection
ev_mapper.connect("event_index_resource",
"/resources/{resource_name}/events",
action="index",
conditions={'method': 'GET'})
# Event data
ev_mapper.connect("event_show",
"/resources/{resource_name}/events/{event_id}",
action="show",
conditions={'method': 'GET'})
super(API, self).__init__(mapper) super(API, self).__init__(mapper)

View File

@ -0,0 +1,132 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# 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 itertools
from webob import exc
from heat.api.openstack.v1 import util
from heat.common import wsgi
from heat.engine import api as engine_api
from heat.engine import identifier
from heat.engine import rpcapi as engine_rpcapi
import heat.openstack.common.rpc.common as rpc_common
from heat.openstack.common.gettextutils import _
summary_keys = [
engine_api.EVENT_ID,
engine_api.EVENT_TIMESTAMP,
engine_api.EVENT_RES_NAME,
engine_api.EVENT_RES_STATUS,
engine_api.EVENT_RES_STATUS_DATA,
engine_api.EVENT_RES_PHYSICAL_ID,
]
def format_event(req, event, keys=None):
include_key = lambda k: k in keys if keys else True
def transform(key, value):
if not include_key(key):
return
if key == engine_api.EVENT_ID:
identity = identifier.EventIdentifier(**value)
yield ('id', identity.event_id)
yield ('links', [util.make_link(req, identity),
util.make_link(req, identity.resource(),
'resource'),
util.make_link(req, identity.stack(),
'stack')])
elif (key == engine_api.EVENT_STACK_ID or
key == engine_api.EVENT_STACK_NAME):
return
else:
yield (key, value)
return dict(itertools.chain.from_iterable(
transform(k, v) for k, v in event.items()))
class EventController(object):
"""
WSGI controller for Events in Heat v1 API
Implements the API actions
"""
def __init__(self, options):
self.options = options
self.engine = engine_rpcapi.EngineAPI()
def _event_list(self, req, identity,
filter_func=lambda e: True, detail=False):
try:
result = self.engine.list_events(req.context,
identity)
except rpc_common.RemoteError as ex:
return util.remote_error(ex)
if 'events' not in result:
raise exc.HTTPInternalServerError()
ev_list = result['events']
keys = None if detail else summary_keys
return [format_event(req, e, keys) for e in ev_list if filter_func(e)]
@util.identified_stack
def index(self, req, identity, resource_name=None):
"""
Lists summary information for all resources
"""
if resource_name is None:
events = self._event_list(req, identity)
else:
res_match = lambda e: e[engine_api.EVENT_RES_NAME] == resource_name
events = self._event_list(req, identity, res_match)
if not events:
msg = _('No events found for resource %s') % resource_name
raise exc.HTTPNotFound(msg)
return {'events': events}
@util.identified_stack
def show(self, req, identity, resource_name, event_id):
"""
Gets detailed information for a stack
"""
def event_match(ev):
identity = identifier.EventIdentifier(**ev[engine_api.EVENT_ID])
return (ev[engine_api.EVENT_RES_NAME] == resource_name and
identity.event_id == event_id)
events = self._event_list(req, identity, event_match, True)
if not events:
raise exc.HTTPNotFound(_('No event %s found') % event_id)
return {'event': events[0]}
def create_resource(options):
"""
Events resource factory method.
"""
# TODO(zaneb) handle XML based on Content-type/Accepts
deserializer = wsgi.JSONRequestDeserializer()
serializer = wsgi.JSONResponseSerializer()
return wsgi.Resource(EventController(options), deserializer, serializer)

View File

@ -35,6 +35,7 @@ from heat.common.wsgi import Request
import heat.api.openstack.v1.stacks as stacks import heat.api.openstack.v1.stacks as stacks
import heat.api.openstack.v1.resources as resources import heat.api.openstack.v1.resources as resources
import heat.api.openstack.v1.events as events
@attr(tag=['unit', 'api-openstack-v1']) @attr(tag=['unit', 'api-openstack-v1'])
@ -1021,6 +1022,407 @@ class ResourceControllerTest(ControllerTest, unittest.TestCase):
self.m.VerifyAll() self.m.VerifyAll()
@attr(tag=['unit', 'api-openstack-v1', 'EventController'])
@attr(speed='fast')
class EventControllerTest(ControllerTest, unittest.TestCase):
'''
Tests the API class which acts as the WSGI controller,
the endpoint processing API requests after they are routed
'''
def setUp(self):
# Create WSGI controller instance
class DummyConfig():
bind_port = 8004
cfgopts = DummyConfig()
self.controller = events.EventController(options=cfgopts)
def test_resource_index(self):
event_id = '42'
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '6')
res_identity = identifier.ResourceIdentifier(resource_name=res_name,
**stack_identity)
ev_identity = identifier.EventIdentifier(event_id=event_id,
**res_identity)
req = self._get(stack_identity._tenant_path() +
'/resources/' + res_name + '/events')
engine_resp = {u'events': [
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:05:39Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev_identity),
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
},
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:05:39Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': 'SomeOtherResource',
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev_identity),
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
}
]}
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndReturn(engine_resp)
self.m.ReplayAll()
result = self.controller.index(req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id,
resource_name=res_name)
expected = {
'events': [
{
'id': event_id,
'links': [
{'href': self._url(ev_identity), 'rel': 'self'},
{'href': self._url(res_identity), 'rel': 'resource'},
{'href': self._url(stack_identity), 'rel': 'stack'},
],
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_time': u'2012-07-23T13:05:39Z',
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
}
]
}
self.assertEqual(result, expected)
self.m.VerifyAll()
def test_stack_index(self):
event_id = '42'
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '6')
res_identity = identifier.ResourceIdentifier(resource_name=res_name,
**stack_identity)
ev_identity = identifier.EventIdentifier(event_id=event_id,
**res_identity)
req = self._get(stack_identity._tenant_path() + '/events')
engine_resp = {u'events': [
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:05:39Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev_identity),
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
}
]}
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndReturn(engine_resp)
self.m.ReplayAll()
result = self.controller.index(req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id)
expected = {
'events': [
{
'id': event_id,
'links': [
{'href': self._url(ev_identity), 'rel': 'self'},
{'href': self._url(res_identity), 'rel': 'resource'},
{'href': self._url(stack_identity), 'rel': 'stack'},
],
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_time': u'2012-07-23T13:05:39Z',
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
}
]
}
self.assertEqual(result, expected)
self.m.VerifyAll()
def test_index_stack_nonexist(self):
stack_identity = identifier.HeatIdentifier(self.tenant,
'wibble', '6')
req = self._get(stack_identity._tenant_path() + '/events')
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndRaise(rpc_common.RemoteError("AttributeError"))
self.m.ReplayAll()
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.index,
req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id)
self.m.VerifyAll()
def test_index_resource_nonexist(self):
event_id = '42'
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '6')
res_identity = identifier.ResourceIdentifier(resource_name=res_name,
**stack_identity)
ev_identity = identifier.EventIdentifier(event_id=event_id,
**res_identity)
req = self._get(stack_identity._tenant_path() +
'/resources/' + res_name + '/events')
engine_resp = {u'events': [
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:05:39Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': 'SomeOtherResource',
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev_identity),
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
}
]}
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndReturn(engine_resp)
self.m.ReplayAll()
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.index,
req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id,
resource_name=res_name)
self.m.VerifyAll()
def test_show(self):
event_id = '42'
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '6')
res_identity = identifier.ResourceIdentifier(resource_name=res_name,
**stack_identity)
ev1_identity = identifier.EventIdentifier(event_id='41',
**res_identity)
ev_identity = identifier.EventIdentifier(event_id=event_id,
**res_identity)
req = self._get(stack_identity._tenant_path() +
'/resources/' + res_name + '/events/' + event_id)
engine_resp = {u'events': [
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:05:39Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev1_identity),
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
},
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:06:00Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev_identity),
u'resource_status': u'CREATE_COMPLETE',
u'physical_resource_id':
u'a3455d8c-9f88-404d-a85b-5315293e67de',
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
}
]}
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndReturn(engine_resp)
self.m.ReplayAll()
result = self.controller.show(req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id,
resource_name=res_name,
event_id=event_id)
expected = {
'event': {
'id': event_id,
'links': [
{'href': self._url(ev_identity), 'rel': 'self'},
{'href': self._url(res_identity), 'rel': 'resource'},
{'href': self._url(stack_identity), 'rel': 'stack'},
],
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_time': u'2012-07-23T13:06:00Z',
u'resource_status': u'CREATE_COMPLETE',
u'physical_resource_id':
u'a3455d8c-9f88-404d-a85b-5315293e67de',
u'resource_type': u'AWS::EC2::Instance',
u'resource_properties': {u'UserData': u'blah'},
}
}
self.assertEqual(result, expected)
self.m.VerifyAll()
def test_show_nonexist(self):
event_id = '42'
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '6')
res_identity = identifier.ResourceIdentifier(resource_name=res_name,
**stack_identity)
ev_identity = identifier.EventIdentifier(event_id='41',
**res_identity)
req = self._get(stack_identity._tenant_path() +
'/resources/' + res_name + '/events/' + event_id)
engine_resp = {u'events': [
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:05:39Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': res_name,
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev_identity),
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
}
]}
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndReturn(engine_resp)
self.m.ReplayAll()
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id,
resource_name=res_name, event_id=event_id)
self.m.VerifyAll()
def test_show_bad_resource(self):
event_id = '42'
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '6')
res_identity = identifier.ResourceIdentifier(resource_name=res_name,
**stack_identity)
ev_identity = identifier.EventIdentifier(event_id='41',
**res_identity)
req = self._get(stack_identity._tenant_path() +
'/resources/' + res_name + '/events/' + event_id)
engine_resp = {u'events': [
{
u'stack_name': u'wordpress',
u'event_time': u'2012-07-23T13:05:39Z',
u'stack_identity': dict(stack_identity),
u'logical_resource_id': 'SomeOtherResourceName',
u'resource_status_reason': u'state changed',
u'event_identity': dict(ev_identity),
u'resource_status': u'IN_PROGRESS',
u'physical_resource_id': None,
u'resource_properties': {u'UserData': u'blah'},
u'resource_type': u'AWS::EC2::Instance',
}
]}
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndReturn(engine_resp)
self.m.ReplayAll()
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id,
resource_name=res_name, event_id=event_id)
self.m.VerifyAll()
def test_show_stack_nonexist(self):
event_id = '42'
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wibble', '6')
req = self._get(stack_identity._tenant_path() +
'/resources/' + res_name + '/events/' + event_id)
self.m.StubOutWithMock(rpc, 'call')
rpc.call(req.context, self.topic,
{'method': 'list_events',
'args': {'stack_identity': stack_identity},
'version': self.api_version},
None).AndRaise(rpc_common.RemoteError("AttributeError"))
self.m.ReplayAll()
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
req, tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id,
resource_name=res_name, event_id=event_id)
self.m.VerifyAll()
if __name__ == '__main__': if __name__ == '__main__':
sys.argv.append(__file__) sys.argv.append(__file__)
nose.main() nose.main()