diff --git a/etc/heat/policy.json b/etc/heat/policy.json index 40c54d9c2b..dba2c8705f 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -54,6 +54,7 @@ "stacks:resource_schema": "rule:deny_stack_user", "stacks:show": "rule:deny_stack_user", "stacks:template": "rule:deny_stack_user", + "stacks:environment": "rule:deny_stack_user", "stacks:update": "rule:deny_stack_user", "stacks:update_patch": "rule:deny_stack_user", "stacks:preview_update": "rule:deny_stack_user", diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index efd658b6b8..8162a6725d 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -170,7 +170,8 @@ class API(wsgi.Router): { 'name': 'stack_lookup_subpath', 'url': '/stacks/{stack_name}/' - '{path:resources|events|template|actions}', + '{path:resources|events|template|actions' + '|environment}', 'action': 'lookup', 'method': 'GET' }, @@ -193,6 +194,12 @@ class API(wsgi.Router): 'action': 'template', 'method': 'GET' }, + { + 'name': 'stack_lookup', + 'url': '/stacks/{stack_name}/{stack_id}/environment', + 'action': 'environment', + 'method': 'GET' + }, # Stack update/delete { diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 4daedc7402..0859e0d6a6 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -453,6 +453,16 @@ class StackController(object): # TODO(zaneb): always set Content-type to application/json return templ + @util.identified_stack + def environment(self, req, identity): + """Get the environment for an existing stack.""" + env = self.rpc_client.get_environment(req.context, identity) + + if env is None: + raise exc.HTTPNotFound() + + return env + @util.identified_stack def update(self, req, identity, body): """Update an existing stack with a new template and/or parameters.""" diff --git a/heat/engine/service.py b/heat/engine/service.py index 7f773dd075..4bdb8ff2e7 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -294,7 +294,7 @@ class EngineService(service.Service): by the RPC caller. """ - RPC_API_VERSION = '1.27' + RPC_API_VERSION = '1.28' def __init__(self, host, topic): super(EngineService, self).__init__() @@ -1249,6 +1249,19 @@ class EngineService(service.Service): return s.raw_template.template return None + @context.request_context + def get_environment(self, cnxt, stack_identity): + """Returns the environment for an existing stack. + + :param cnxt: RPC context + :param stack_identity: identifies the stack + :rtype: dict + """ + s = self._get_stack(cnxt, stack_identity, show_deleted=True) + if s: + return s.raw_template.environment + return None + @context.request_context def list_outputs(self, cntx, stack_identity): """Get a list of stack outputs. diff --git a/heat/rpc/client.py b/heat/rpc/client.py index e611e98f12..b2ad856fb2 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -48,6 +48,7 @@ class EngineClient(object): 1.25 - list_stack_resource filter update 1.26 - Add mark_unhealthy 1.27 - Add check_software_deployment + 1.28 - Add environment_show call """ BASE_RPC_API_VERSION = '1.0' @@ -372,6 +373,19 @@ class EngineClient(object): return self.call(ctxt, self.make_msg('get_template', stack_identity=stack_identity)) + def get_environment(self, context, stack_identity): + """Returns the environment for an existing stack. + + :param context: RPC context + :param stack_identity: identifies the stack + :rtype: dict + """ + + return self.call(context, + self.make_msg('get_environment', + stack_identity=stack_identity), + version='1.28') + def delete_stack(self, ctxt, stack_identity, cast=True): """Deletes a given stack. diff --git a/heat/tests/api/openstack_v1/test_stacks.py b/heat/tests/api/openstack_v1/test_stacks.py index 726c1926c3..732c06cf52 100644 --- a/heat/tests/api/openstack_v1/test_stacks.py +++ b/heat/tests/api/openstack_v1/test_stacks.py @@ -1680,6 +1680,49 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase): self.assertEqual(template, response) self.m.VerifyAll() + def test_get_environment(self, mock_enforce): + self._mock_enforce_setup(mock_enforce, 'environment', True) + identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6') + req = self._get('/stacks/%(stack_name)s/%(stack_id)s' % identity) + env = {'parameters': {'Foo': 'bar'}} + + self.m.StubOutWithMock(rpc_client.EngineClient, 'call') + rpc_client.EngineClient.call( + req.context, + ('get_environment', {'stack_identity': dict(identity)},), + version='1.28', + ).AndReturn(env) + self.m.ReplayAll() + + response = self.controller.environment(req, tenant_id=identity.tenant, + stack_name=identity.stack_name, + stack_id=identity.stack_id) + + self.assertEqual(env, response) + self.m.VerifyAll() + + def test_get_environment_no_env(self, mock_enforce): + self._mock_enforce_setup(mock_enforce, 'environment', True) + identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6') + req = self._get('/stacks/%(stack_name)s/%(stack_id)s' % identity) + + self.m.StubOutWithMock(rpc_client.EngineClient, 'call') + rpc_client.EngineClient.call( + req.context, + ('get_environment', {'stack_identity': dict(identity)},), + version='1.28', + ).AndReturn(None) + self.m.ReplayAll() + + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.environment, + req, + tenant_id=identity.tenant, + stack_name=identity.stack_name, + stack_id=identity.stack_id) + + self.m.VerifyAll() + def test_get_template_err_denied_policy(self, mock_enforce): self._mock_enforce_setup(mock_enforce, 'template', False) identity = identifier.HeatIdentifier(self.tenant, 'wordpress', '6') diff --git a/heat/tests/engine/service/test_service_engine.py b/heat/tests/engine/service/test_service_engine.py index 403581fcdd..360b2435ef 100644 --- a/heat/tests/engine/service/test_service_engine.py +++ b/heat/tests/engine/service/test_service_engine.py @@ -40,7 +40,7 @@ class ServiceEngineTest(common.HeatTestCase): def test_make_sure_rpc_version(self): self.assertEqual( - '1.27', + '1.28', 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 ' diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index a7cada7dbd..e358b38af2 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -1068,6 +1068,35 @@ class StackServiceTest(common.HeatTestCase): outputs = self.eng.list_outputs(self.ctx, mock.ANY) self.assertEqual([], outputs) + def test_get_environment(self): + # Setup + t = template_format.parse(tools.wp_template) + env = {'parameters': {'KeyName': 'EnvKey'}} + tmpl = templatem.Template(t) + stack = parser.Stack(self.ctx, 'get_env_stack', tmpl) + + mock_get_stack = self.patchobject(self.eng, '_get_stack') + mock_get_stack.return_value = mock.MagicMock() + mock_get_stack.return_value.raw_template.environment = env + self.patchobject(parser.Stack, 'load', return_value=stack) + + # Test + found = self.eng.get_environment(self.ctx, stack.identifier()) + + # Verify + self.assertEqual(env, found) + + def test_get_environment_no_env(self): + # Setup + exc = exception.EntityNotFound(entity='stack', name='missing') + self.patchobject(self.eng, '_get_stack', side_effect=exc) + + # Test + self.assertRaises(dispatcher.ExpectedException, + self.eng.get_environment, + self.ctx, + 'irrelevant') + def test_stack_show_output(self): t = template_format.parse(tools.wp_template) t['outputs'] = {'test': {'value': 'first', 'description': 'sec'}, diff --git a/heat/tests/test_rpc_client.py b/heat/tests/test_rpc_client.py index ae52ccba5d..c506db9e9b 100644 --- a/heat/tests/test_rpc_client.py +++ b/heat/tests/test_rpc_client.py @@ -406,3 +406,8 @@ class EngineRpcAPITestCase(common.HeatTestCase): mark_unhealthy=True, resource_status_reason="Any reason", version='1.26') + + def test_get_environment(self): + self._test_engine_api( + 'get_environment', 'call', stack_identity=self.identity, + version='1.28')