diff --git a/etc/heat/heat.conf.sample b/etc/heat/heat.conf.sample index bc490d1ff8..b3a7f0f7f6 100644 --- a/etc/heat/heat.conf.sample +++ b/etc/heat/heat.conf.sample @@ -983,3 +983,16 @@ #insecure=false +[revision] + +# +# Options defined in heat.common.config +# + +# Heat build revision. If you would prefer to manage your +# build revision separately you can move this section to a +# different file and add it as another config option (string +# value) +#heat_revision=unknown + + diff --git a/heat/api/openstack/v1/__init__.py b/heat/api/openstack/v1/__init__.py index caf36274f0..d590cac1db 100644 --- a/heat/api/openstack/v1/__init__.py +++ b/heat/api/openstack/v1/__init__.py @@ -19,6 +19,7 @@ from heat.api.openstack.v1 import stacks from heat.api.openstack.v1 import resources from heat.api.openstack.v1 import events from heat.api.openstack.v1 import actions +from heat.api.openstack.v1 import build_info from heat.common import wsgi from heat.openstack.common import log as logging @@ -36,8 +37,8 @@ class API(wsgi.Router): self.conf = conf mapper = routes.Mapper() + # Stacks stacks_resource = stacks.create_resource(conf) - with mapper.submapper(controller=stacks_resource, path_prefix="/{tenant_id}") as stack_mapper: # Template handling @@ -164,4 +165,14 @@ class API(wsgi.Router): action="action", conditions={'method': 'POST'}) + # Info + info_resource = build_info.create_resource(conf) + with mapper.submapper(controller=info_resource, + path_prefix="/{tenant_id}") as info_mapper: + + info_mapper.connect('build_info', + '/build_info', + action='build_info', + conditions={'method': 'GET'}) + super(API, self).__init__(mapper) diff --git a/heat/api/openstack/v1/build_info.py b/heat/api/openstack/v1/build_info.py new file mode 100644 index 0000000000..f1cbf20dbe --- /dev/null +++ b/heat/api/openstack/v1/build_info.py @@ -0,0 +1,51 @@ +# 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. + +from oslo.config import cfg + +from heat.api.openstack.v1 import util +from heat.common import wsgi +from heat.rpc import client as rpc_client + + +class BuildInfoController(object): + """ + WSGI controller for BuildInfo in Heat v1 API + Returns build information for current app + """ + + def __init__(self, options): + self.options = options + self.engine = rpc_client.EngineClient() + + @util.tenant_local + def build_info(self, req): + engine_revision = self.engine.get_revision(req.context) + build_info = { + 'api': {'revision': cfg.CONF.revision['heat_revision']}, + 'engine': {'revision': engine_revision} + } + + return build_info + + +def create_resource(options): + """ + BuildInfo factory method. + """ + deserializer = wsgi.JSONRequestDeserializer() + serializer = wsgi.JSONResponseSerializer() + return wsgi.Resource(BuildInfoController(options), deserializer, + serializer) diff --git a/heat/common/config.py b/heat/common/config.py index 38240f837a..8b27c4c4e6 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -150,6 +150,15 @@ def register_clients_opts(): cfg.CONF.register_opts(opts_copy, group=client_specific_group) +revision_group = cfg.OptGroup('revision') +revision_opts = [ + cfg.StrOpt('heat_revision', + default='unknown', + help=_('Heat build revision. ' + 'If you would prefer to manage your build revision ' + 'separately you can move this section to a different ' + 'file and add it as another config option'))] + cfg.CONF.register_opts(engine_opts) cfg.CONF.register_opts(service_opts) cfg.CONF.register_opts(rpc_opts) @@ -157,6 +166,8 @@ cfg.CONF.register_group(paste_deploy_group) cfg.CONF.register_opts(paste_deploy_opts, group=paste_deploy_group) cfg.CONF.register_group(auth_password_group) cfg.CONF.register_opts(auth_password_opts, group=auth_password_group) +cfg.CONF.register_group(revision_group) +cfg.CONF.register_opts(revision_opts, group=revision_group) register_clients_opts() diff --git a/heat/engine/service.py b/heat/engine/service.py index 3dcd6d6225..05f2651afa 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -199,6 +199,9 @@ class EngineService(service.Service): return [format_stack_detail(s) for s in stacks] + def get_revision(self, cnxt): + return cfg.CONF.revision['heat_revision'] + @request_context def list_stacks(self, cnxt, limit=None, marker=None, sort_keys=None, sort_dir=None, filters=None): diff --git a/heat/rpc/client.py b/heat/rpc/client.py index 83bd150f19..8909f3e235 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -322,3 +322,6 @@ class EngineClient(heat.openstack.common.rpc.proxy.RpcProxy): return self.call(ctxt, self.make_msg('set_watch_state', watch_name=watch_name, state=state)) + + def get_revision(self, ctxt): + return self.call(ctxt, self.make_msg('get_revision')) diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index 34be82245e..3ce889751e 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -33,6 +33,7 @@ import heat.api.openstack.v1.stacks as stacks import heat.api.openstack.v1.resources as resources import heat.api.openstack.v1.events as events import heat.api.openstack.v1.actions as actions +import heat.api.openstack.v1.build_info as build_info from heat.tests import utils import heat.api.middleware.fault as fault @@ -2432,6 +2433,16 @@ class RoutesTest(HeatTestCase): 'event_id': 'dddd' }) + def test_build_info(self): + self.assertRoute( + self.m, + '/fake_tenant/build_info', + 'GET', + 'build_info', + 'BuildInfoController', + {'tenant_id': 'fake_tenant'} + ) + class ActionControllerTest(ControllerTest, HeatTestCase): ''' @@ -2595,3 +2606,39 @@ class ActionControllerTest(ControllerTest, HeatTestCase): stack_id=stack_identity.stack_id, body=body) self.m.VerifyAll() + + +class BuildInfoControllerTest(HeatTestCase): + def test_theres_a_default_api_build_revision(self): + req = mock.Mock() + controller = build_info.BuildInfoController({}) + controller.engine = mock.Mock() + + response = controller.build_info(req, tenant_id='tenant_id') + self.assertIn('api', response) + self.assertIn('revision', response['api']) + self.assertEqual('unknown', response['api']['revision']) + + @mock.patch.object(build_info.cfg, 'CONF') + def test_response_api_build_revision_from_config_file(self, mock_conf): + req = mock.Mock() + controller = build_info.BuildInfoController({}) + mock_engine = mock.Mock() + mock_engine.get_revision.return_value = 'engine_revision' + controller.engine = mock_engine + mock_conf.revision = {'heat_revision': 'test'} + + response = controller.build_info(req, tenant_id='tenant_id') + self.assertEqual('test', response['api']['revision']) + + def test_retrieves_build_revision_from_the_engine(self): + req = mock.Mock() + controller = build_info.BuildInfoController({}) + mock_engine = mock.Mock() + mock_engine.get_revision.return_value = 'engine_revision' + controller.engine = mock_engine + + response = controller.build_info(req, tenant_id='tenant_id') + self.assertIn('engine', response) + self.assertIn('revision', response['engine']) + self.assertEqual('engine_revision', response['engine']['revision'])