diff --git a/docs/api/curl.rst b/docs/api/curl.rst index 84a5c1d2..1c3b57f1 100644 --- a/docs/api/curl.rst +++ b/docs/api/curl.rst @@ -10,6 +10,7 @@ Resources - `ResourceClass <#resource_class>`_ - `DataCenter <#data_center>`_ - `Node <#node>`_ +- `Overcloud <#overcloud>`_ Rack ---- @@ -418,4 +419,29 @@ response } } +Overcloud +---------- + +get Keystone URL for an overcloud +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + curl -X GET -H 'Content-Type:application/json' -H 'Accept: application/json' http://0.0.0.0:8585/v1/overclouds/cloudname + +response +^^^^^^^^ + +:: + + { + "stack_name": "cloudname", + "links": [ + { + "rel": "keystone", + "href": "http://192.0.2.5:5000/v2.0/" + } + ] + } + `back to top <#index>`_ diff --git a/tuskar/api/controllers/v1/__init__.py b/tuskar/api/controllers/v1/__init__.py index 1d602e0d..e2db9348 100644 --- a/tuskar/api/controllers/v1/__init__.py +++ b/tuskar/api/controllers/v1/__init__.py @@ -15,9 +15,10 @@ from tuskar.api.controllers.v1.controller import Controller from tuskar.api.controllers.v1.data_center import DataCenterController from tuskar.api.controllers.v1.flavor import FlavorsController from tuskar.api.controllers.v1.node import NodesController +from tuskar.api.controllers.v1.overcloud import OvercloudsController from tuskar.api.controllers.v1.rack import RacksController from tuskar.api.controllers.v1.resource_class import ResourceClassesController __all__ = (Controller, DataCenterController, FlavorsController, - RacksController, ResourceClassesController, + OvercloudsController, RacksController, ResourceClassesController, NodesController) diff --git a/tuskar/api/controllers/v1/controller.py b/tuskar/api/controllers/v1/controller.py index d0439d18..b672021c 100644 --- a/tuskar/api/controllers/v1/controller.py +++ b/tuskar/api/controllers/v1/controller.py @@ -12,6 +12,7 @@ import pecan from tuskar.api.controllers.v1.data_center import DataCenterController from tuskar.api.controllers.v1.node import NodesController +from tuskar.api.controllers.v1.overcloud import OvercloudsController from tuskar.api.controllers.v1.rack import RacksController from tuskar.api.controllers.v1.resource_class import ResourceClassesController @@ -22,6 +23,7 @@ class Controller(object): racks = RacksController() resource_classes = ResourceClassesController() data_centers = DataCenterController() + overclouds = OvercloudsController() nodes = NodesController() @pecan.expose('json') diff --git a/tuskar/api/controllers/v1/overcloud.py b/tuskar/api/controllers/v1/overcloud.py new file mode 100644 index 00000000..ca8329e2 --- /dev/null +++ b/tuskar/api/controllers/v1/overcloud.py @@ -0,0 +1,66 @@ +# 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 pecan +from wsme import api +from wsmeext import pecan as wsme_pecan + +import heatclient.exc + +from tuskar.api.controllers.v1.types import Error +from tuskar.api.controllers.v1.types import Link +from tuskar.api.controllers.v1.types import Overcloud +import tuskar.heat.client +from tuskar.openstack.common.gettextutils import _ + + +class OvercloudsController(pecan.rest.RestController): + """Controller for Overcloud.""" + + @wsme_pecan.wsexpose(Overcloud, unicode) + def get_one(self, stack_name): + heat = tuskar.heat.client.HeatClient() + + try: + stack = heat.get_stack(stack_name) + except heatclient.exc.HTTPNotFound as ex: + response = api.Response( + None, + error=Error(faultcode=ex.code, faultstring=str(ex)), + status_code=ex.code) + return response + + if not hasattr(stack, 'outputs'): + faultstring = _('Failed to find Keystone URL.') + response = api.Response( + None, + error=Error(faultcode=404, faultstring=faultstring), + status_code=404) + return response + + outputs = stack.outputs + keystone_param = filter(lambda x: x['output_key'] == 'KeystoneURL', + outputs) + if len(keystone_param) == 0: + faultstring = _('Failed to find Keystone URL.') + response = api.Response( + None, + error=Error(faultcode=404, faultstring=faultstring), + status_code=404) + return response + + keystone_link = Link(rel='keystone', + href=keystone_param[0]['output_value']) + overcloud = Overcloud(stack_name=stack_name, + links=[keystone_link]) + + return overcloud diff --git a/tuskar/api/controllers/v1/types/__init__.py b/tuskar/api/controllers/v1/types/__init__.py index c9d2827f..47a462dd 100644 --- a/tuskar/api/controllers/v1/types/__init__.py +++ b/tuskar/api/controllers/v1/types/__init__.py @@ -18,9 +18,10 @@ from tuskar.api.controllers.v1.types.error import Error from tuskar.api.controllers.v1.types.flavor import Flavor from tuskar.api.controllers.v1.types.link import Link from tuskar.api.controllers.v1.types.node import Node +from tuskar.api.controllers.v1.types.overcloud import Overcloud from tuskar.api.controllers.v1.types.rack import Rack from tuskar.api.controllers.v1.types.relation import Relation from tuskar.api.controllers.v1.types.resource_class import ResourceClass -__all__ = (Base, Capacity, Chassis, Error, Flavor, Link, Node, Rack, +__all__ = (Base, Capacity, Chassis, Error, Flavor, Link, Node, Overcloud, Rack, Relation, ResourceClass) diff --git a/tuskar/api/controllers/v1/types/overcloud.py b/tuskar/api/controllers/v1/types/overcloud.py new file mode 100644 index 00000000..732cc2b6 --- /dev/null +++ b/tuskar/api/controllers/v1/types/overcloud.py @@ -0,0 +1,23 @@ +# 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 wsme import types as wtypes + +from tuskar.api.controllers.v1.types.base import Base +from tuskar.api.controllers.v1.types.link import Link + + +class Overcloud(Base): + """An Overcloud representation.""" + + stack_name = wtypes.text + links = [Link] diff --git a/tuskar/heat/client.py b/tuskar/heat/client.py index 62659796..8129c690 100644 --- a/tuskar/heat/client.py +++ b/tuskar/heat/client.py @@ -102,10 +102,12 @@ class HeatClient(object): LOG.exception(e) return False - def get_stack(self): + def get_stack(self, name=None): """Get overcloud Heat template.""" + if name is None: + name = CONF.heat['stack_name'] if self.connection: - return self.connection.stacks.get(CONF.heat['stack_name']) + return self.connection.stacks.get(name) def get_template(self): """Get JSON representation of the Heat overcloud template.""" @@ -135,9 +137,11 @@ class HeatClient(object): LOG.exception(e) return False - def exists_stack(self): + def exists_stack(self, name=None): + if name is None: + name = CONF.heat['stack_name'] try: - self.get_stack() + self.get_stack(name) return True #return false if 404 except HeatStackNotFound: diff --git a/tuskar/tests/api/controllers/v1/test_overclouds.py b/tuskar/tests/api/controllers/v1/test_overclouds.py new file mode 100644 index 00000000..1562654b --- /dev/null +++ b/tuskar/tests/api/controllers/v1/test_overclouds.py @@ -0,0 +1,62 @@ +# 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 mox + +import heatclient.exc +import heatclient.v1.stacks + +import tuskar.heat.client +from tuskar.tests.api import api + + +class TestOverclouds(api.FunctionalTest): + + def test_it_returns_the_overcloud_endpoints(self): + heat_outputs = [ + {'output_value': 'http://192.0.2.5:5000/v2.0/', + 'description': 'URL for the Overcloud Keystone service', + 'output_key': 'KeystoneURL'}, + ] + + heat_stack = mox.MockAnything() + heat_stack.outputs = heat_outputs + + self.mox.StubOutWithMock(tuskar.heat.client.HeatClient, 'get_stack') + tuskar.heat.client.HeatClient.get_stack('stack_name').AndReturn( + heat_stack) + self.mox.ReplayAll() + + response = self.app.get('/v1/overclouds/stack_name') + self.assertEqual(response.status, '200 OK') + self.assertRegexpMatches(response.body, 'http://192.0.2.5:5000/v2.0/') + + def test_it_returns_404_for_nonexisting_overcloud(self): + self.mox.StubOutWithMock(tuskar.heat.client.HeatClient, 'get_stack') + tuskar.heat.client.HeatClient.get_stack( + 'stack_name').AndRaise(heatclient.exc.HTTPNotFound()) + self.mox.ReplayAll() + + response = self.app.get('/v1/overclouds/stack_name', + expect_errors=True) + self.assertEqual(response.status, '404 Not Found') + + def test_it_returns_404_during_provisioning(self): + heat_stack = self.mox.CreateMock(heatclient.v1.stacks.Stack) + self.mox.StubOutWithMock(tuskar.heat.client.HeatClient, 'get_stack') + tuskar.heat.client.HeatClient.get_stack('stack_name').AndReturn( + heat_stack) + self.mox.ReplayAll() + + response = self.app.get('/v1/overclouds/stack_name', + expect_errors=True) + self.assertEqual(response.status, '404 Not Found')