support list resources with details

Currently, users can list resources without details, or show
specific resource with details. However, they cannot list all
resources with details. It is desirable to support that.
With consistency concern, we should use {list URI}/detail as
pattern for resources detail list just like other detail list
API, but this pattern will conflict with
{resources list URI}/{resource name} when resoruce name
is 'detail', so implement this feature as a param in list API
seems to be this only option in this case.

DocImpact
APIImpact

Change-Id: I59f464d1803009d7ae2ec35d9fdf0671096967e4
Closes-Bug: #1467268
This commit is contained in:
Rico Lin 2015-07-08 17:22:33 +08:00
parent 347a0b55e4
commit 11efe222b3
8 changed files with 129 additions and 26 deletions

View File

@ -79,23 +79,34 @@ class ResourceController(object):
self.options = options
self.rpc_client = rpc_client.EngineClient()
def _extract_to_param(self, req, rpc_param, extractor, default):
key = rpc_param
if key in req.params:
try:
return extractor(key, req.params[key])
except ValueError as e:
raise exc.HTTPBadRequest(six.text_type(e))
else:
return default
@util.identified_stack
def index(self, req, identity):
"""
Lists summary information for all resources
Lists information for all resources
"""
nested_depth = 0
key = rpc_api.PARAM_NESTED_DEPTH
if key in req.params:
try:
nested_depth = param_utils.extract_int(key, req.params[key])
except ValueError as e:
raise exc.HTTPBadRequest(six.text_type(e))
nested_depth = self._extract_to_param(req,
rpc_api.PARAM_NESTED_DEPTH,
param_utils.extract_int,
default=0)
with_detail = self._extract_to_param(req,
rpc_api.PARAM_WITH_DETAIL,
param_utils.extract_bool,
default=False)
res_list = self.rpc_client.list_stack_resources(req.context,
identity,
nested_depth)
nested_depth,
with_detail)
return {'resources': [format_resource(req, res) for res in res_list]}

View File

@ -267,7 +267,7 @@ class EngineService(service.Service):
by the RPC caller.
"""
RPC_API_VERSION = '1.11'
RPC_API_VERSION = '1.12'
def __init__(self, host, topic, manager=None):
super(EngineService, self).__init__()
@ -1256,12 +1256,13 @@ class EngineService(service.Service):
if resource_name is None or name == resource_name]
@context.request_context
def list_stack_resources(self, cnxt, stack_identity, nested_depth=0):
def list_stack_resources(self, cnxt, stack_identity,
nested_depth=0, with_detail=False):
s = self._get_stack(cnxt, stack_identity, show_deleted=True)
stack = parser.Stack.load(cnxt, stack=s)
depth = min(nested_depth, cfg.CONF.max_nested_stack_depth)
return [api.format_stack_resource(resource, detail=False)
return [api.format_stack_resource(resource, detail=with_detail)
for resource in stack.iter_resources(depth)]
@context.request_context

View File

@ -19,13 +19,13 @@ PARAM_KEYS = (
PARAM_SHOW_DELETED, PARAM_SHOW_NESTED, PARAM_EXISTING,
PARAM_CLEAR_PARAMETERS, PARAM_GLOBAL_TENANT, PARAM_LIMIT,
PARAM_NESTED_DEPTH, PARAM_TAGS, PARAM_SHOW_HIDDEN, PARAM_TAGS_ANY,
PARAM_NOT_TAGS, PARAM_NOT_TAGS_ANY, TEMPLATE_TYPE,
PARAM_NOT_TAGS, PARAM_NOT_TAGS_ANY, TEMPLATE_TYPE, PARAM_WITH_DETAIL
) = (
'timeout_mins', 'disable_rollback', 'adopt_stack_data',
'show_deleted', 'show_nested', 'existing',
'clear_parameters', 'global_tenant', 'limit',
'nested_depth', 'tags', 'show_hidden', 'tags_any',
'not_tags', 'not_tags_any', 'template_type',
'not_tags', 'not_tags_any', 'template_type', 'with_detail',
)
STACK_KEYS = (

View File

@ -32,6 +32,7 @@ class EngineClient(object):
1.9 - Add template_type option to generate_template()
1.10 - Add support for software config list
1.11 - Add support for template versions list
1.12 - Add with_detail option for stack resources list
'''
BASE_RPC_API_VERSION = '1.0'
@ -421,16 +422,21 @@ class EngineClient(object):
stack_identity=stack_identity,
resource_name=resource_name))
def list_stack_resources(self, ctxt, stack_identity, nested_depth=0):
def list_stack_resources(self, ctxt, stack_identity,
nested_depth=0, with_detail=False):
"""
List the resources belonging to a stack.
:param ctxt: RPC context.
:param stack_identity: Name of the stack.
:param nested_depth: Levels of nested stacks of which list resources.
:param with_detail: show detail for resoruces in list.
"""
return self.call(ctxt, self.make_msg('list_stack_resources',
stack_identity=stack_identity,
nested_depth=nested_depth))
return self.call(ctxt,
self.make_msg('list_stack_resources',
stack_identity=stack_identity,
nested_depth=nested_depth,
with_detail=with_detail),
version='1.12')
def stack_suspend(self, ctxt, stack_identity):
return self.call(ctxt, self.make_msg('stack_suspend',

View File

@ -1598,7 +1598,9 @@ class CfnStackControllerTest(common.HeatTestCase):
rpc_client.EngineClient.call(
dummy_req.context,
('list_stack_resources', {'stack_identity': identity,
'nested_depth': 0})
'nested_depth': 0,
'with_detail': False}),
version='1.12'
).AndReturn(engine_resp)
self.m.ReplayAll()

View File

@ -2237,7 +2237,10 @@ class ResourceControllerTest(ControllerTest, common.HeatTestCase):
rpc_client.EngineClient.call(
req.context,
('list_stack_resources', {'stack_identity': stack_identity,
'nested_depth': 0})
'nested_depth': 0,
'with_detail': False,
}),
version='1.12'
).AndReturn(engine_resp)
self.m.ReplayAll()
@ -2274,13 +2277,16 @@ class ResourceControllerTest(ControllerTest, common.HeatTestCase):
rpc_client.EngineClient.call(
req.context,
('list_stack_resources', {'stack_identity': stack_identity,
'nested_depth': 0})
'nested_depth': 0,
'with_detail': False}),
version='1.12'
).AndRaise(to_remote_error(error))
self.m.ReplayAll()
resp = request_with_middleware(fault.FaultWrapper,
self.controller.index,
req, tenant_id=self.tenant,
req,
tenant_id=self.tenant,
stack_name=stack_identity.stack_name,
stack_id=stack_identity.stack_id)
@ -2300,7 +2306,9 @@ class ResourceControllerTest(ControllerTest, common.HeatTestCase):
rpc_client.EngineClient.call(
req.context,
('list_stack_resources', {'stack_identity': stack_identity,
'nested_depth': 99})
'nested_depth': 99,
'with_detail': False}),
version='1.12'
).AndReturn([])
self.m.ReplayAll()
@ -2348,6 +2356,80 @@ class ResourceControllerTest(ControllerTest, common.HeatTestCase):
self.assertEqual(403, resp.status_int)
self.assertIn('403 Forbidden', six.text_type(resp))
def test_index_detail(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'index', True)
res_name = 'WikiDatabase'
stack_identity = identifier.HeatIdentifier(self.tenant,
'wordpress', '1')
res_identity = identifier.ResourceIdentifier(resource_name=res_name,
**stack_identity)
req = self._get(stack_identity._tenant_path() + '/resources',
{'with_detail': 'true'})
resp_parameters = {
"OS::project_id": "3ab5b02fa01f4f95afa1e254afc4a435",
"network": "cf05086d-07c7-4ed6-95e5-e4af724677e6",
"OS::stack_name": "s1", "admin_pass": "******",
"key_name": "kk", "image": "fa5d387e-541f-4dfb-ae8a-83a614683f84",
"db_port": "50000",
"OS::stack_id": "723d7cee-46b3-4433-9c21-f3378eb0bfc4",
"flavor": "1"
},
engine_resp = [
{
u'resource_identity': dict(res_identity),
u'stack_name': stack_identity.stack_name,
u'resource_name': res_name,
u'resource_status_reason': None,
u'updated_time': u'2012-07-23T13:06:00Z',
u'stack_identity': stack_identity,
u'resource_action': u'CREATE',
u'resource_status': u'COMPLETE',
u'physical_resource_id':
u'a3455d8c-9f88-404d-a85b-5315293e67de',
u'resource_type': u'AWS::EC2::Instance',
u'parameters': resp_parameters,
u'description': u'Hello description',
u'stack_user_project_id': u'6f38bcfebbc4400b82d50c1a2ea3057d',
}
]
self.m.StubOutWithMock(rpc_client.EngineClient, 'call')
rpc_client.EngineClient.call(
req.context,
('list_stack_resources', {'stack_identity': stack_identity,
'nested_depth': 0,
'with_detail': True}),
version='1.12'
).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 = {
'resources': [{'links': [{'href': self._url(res_identity),
'rel': 'self'},
{'href': self._url(stack_identity),
'rel': 'stack'}],
u'resource_name': res_name,
u'logical_resource_id': res_name,
u'resource_status_reason': None,
u'updated_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'parameters': resp_parameters,
u'description': u'Hello description',
u'stack_user_project_id':
u'6f38bcfebbc4400b82d50c1a2ea3057d'}]}
self.assertEqual(expected, result)
self.m.VerifyAll()
def test_show(self, mock_enforce):
self._mock_enforce_setup(mock_enforce, 'show', True)
res_name = 'WikiDatabase'

View File

@ -39,7 +39,7 @@ class ServiceEngineTest(common.HeatTestCase):
def test_make_sure_rpc_version(self):
self.assertEqual(
'1.11',
'1.12',
service.EngineService.RPC_API_VERSION,
('RPC version is changed, please update this test to new version '
'and make sure additional test cases are added for RPC APIs '

View File

@ -230,7 +230,8 @@ class EngineRpcAPITestCase(common.HeatTestCase):
def test_list_stack_resources(self):
self._test_engine_api('list_stack_resources', 'call',
stack_identity=self.identity,
nested_depth=0)
nested_depth=0,
with_detail=False)
def test_stack_suspend(self):
self._test_engine_api('stack_suspend', 'call',