From 276c5a3707b675a771f640a3bdbdebe090f293fc Mon Sep 17 00:00:00 2001 From: Jay Dobies Date: Thu, 23 Jan 2014 13:13:37 -0500 Subject: [PATCH] API controllers for icehouse domain model Change-Id: I2cf5c5fe67cc4c5befb53f8323ee4b32edc3d520 --- etc/tuskar/tuskar.conf.sample | 10 +- test-requirements.txt | 1 + tuskar/api/controllers/__init__.py | 13 - tuskar/api/controllers/root.py | 10 +- tuskar/api/controllers/v1/__init__.py | 24 -- tuskar/api/controllers/v1/controller.py | 35 ++- tuskar/api/controllers/v1/data_center.py | 76 ----- tuskar/api/controllers/v1/flavor.py | 80 ----- tuskar/api/controllers/v1/models.py | 142 +++++++++ tuskar/api/controllers/v1/node.py | 42 --- tuskar/api/controllers/v1/overcloud.py | 182 ++++++++---- tuskar/api/controllers/v1/rack.py | 122 -------- .../api/controllers/v1/resource_category.py | 144 +++++++++ tuskar/api/controllers/v1/resource_class.py | 96 ------ tuskar/api/controllers/v1/types/__init__.py | 27 -- tuskar/api/controllers/v1/types/base.py | 49 ---- tuskar/api/controllers/v1/types/capacity.py | 26 -- tuskar/api/controllers/v1/types/chassis.py | 26 -- tuskar/api/controllers/v1/types/error.py | 25 -- tuskar/api/controllers/v1/types/flavor.py | 44 --- tuskar/api/controllers/v1/types/link.py | 48 --- tuskar/api/controllers/v1/types/node.py | 43 --- tuskar/api/controllers/v1/types/overcloud.py | 23 -- tuskar/api/controllers/v1/types/rack.py | 79 ----- tuskar/api/controllers/v1/types/relation.py | 25 -- .../controllers/v1/types/resource_class.py | 57 ---- tuskar/common/exception.py | 125 +------- tuskar/db/__init__.py | 16 - tuskar/db/sqlalchemy/api.py | 275 ++++++++++-------- tuskar/db/sqlalchemy/models.py | 21 ++ tuskar/drivers/__init__.py | 16 - .../tests/api/controllers/v1/test_models.py | 155 ++++++++++ .../api/controllers/v1/test_overcloud.py | 130 +++++++++ .../controllers/v1/test_resource_category.py | 128 ++++++++ tuskar/tests/db/test_api.py | 269 +++++++++-------- tuskar/tests/drivers/__init__.py | 0 36 files changed, 1167 insertions(+), 1417 deletions(-) delete mode 100644 tuskar/api/controllers/v1/data_center.py delete mode 100644 tuskar/api/controllers/v1/flavor.py create mode 100644 tuskar/api/controllers/v1/models.py delete mode 100644 tuskar/api/controllers/v1/node.py delete mode 100644 tuskar/api/controllers/v1/rack.py create mode 100644 tuskar/api/controllers/v1/resource_category.py delete mode 100644 tuskar/api/controllers/v1/resource_class.py delete mode 100644 tuskar/api/controllers/v1/types/__init__.py delete mode 100644 tuskar/api/controllers/v1/types/base.py delete mode 100644 tuskar/api/controllers/v1/types/capacity.py delete mode 100644 tuskar/api/controllers/v1/types/chassis.py delete mode 100644 tuskar/api/controllers/v1/types/error.py delete mode 100644 tuskar/api/controllers/v1/types/flavor.py delete mode 100644 tuskar/api/controllers/v1/types/link.py delete mode 100644 tuskar/api/controllers/v1/types/node.py delete mode 100644 tuskar/api/controllers/v1/types/overcloud.py delete mode 100644 tuskar/api/controllers/v1/types/rack.py delete mode 100644 tuskar/api/controllers/v1/types/relation.py delete mode 100644 tuskar/api/controllers/v1/types/resource_class.py delete mode 100644 tuskar/drivers/__init__.py create mode 100644 tuskar/tests/api/controllers/v1/test_models.py create mode 100644 tuskar/tests/api/controllers/v1/test_overcloud.py create mode 100644 tuskar/tests/api/controllers/v1/test_resource_category.py delete mode 100644 tuskar/tests/drivers/__init__.py diff --git a/etc/tuskar/tuskar.conf.sample b/etc/tuskar/tuskar.conf.sample index 379da7be..f6c4464d 100644 --- a/etc/tuskar/tuskar.conf.sample +++ b/etc/tuskar/tuskar.conf.sample @@ -33,14 +33,6 @@ #auth_strategy=noauth -# -# Options defined in tuskar.api.controllers.v1.types.link -# - -# Ironic API entrypoint URL (string value) -#ironic_url=http://ironic.local:6543/v1 - - # # Options defined in tuskar.common.exception # @@ -551,4 +543,4 @@ #ringfile=/etc/oslo/matchmaker_ring.json -# Total option count: 111 +# Total option count: 110 diff --git a/test-requirements.txt b/test-requirements.txt index da531c45..f4fa5516 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,6 +4,7 @@ hacking>=0.8.0,<0.9 coverage>=3.6 discover fixtures>=0.3.14 +mock>=1.0 mox>=0.5.3 MySQL-python python-subunit diff --git a/tuskar/api/controllers/__init__.py b/tuskar/api/controllers/__init__.py index 13cd5bb1..e69de29b 100644 --- a/tuskar/api/controllers/__init__.py +++ b/tuskar/api/controllers/__init__.py @@ -1,13 +0,0 @@ -# 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. diff --git a/tuskar/api/controllers/root.py b/tuskar/api/controllers/root.py index ed038795..1d07ba28 100644 --- a/tuskar/api/controllers/root.py +++ b/tuskar/api/controllers/root.py @@ -1,14 +1,10 @@ # -*- encoding: utf-8 -*- # -# Copyright © 2012 New Dream Network, LLC (DreamHost) -# -# Author: Doug Hellmann -# # 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 +# 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 @@ -18,12 +14,12 @@ import pecan -from tuskar.api.controllers import v1 +from tuskar.api.controllers.v1 import controller class RootController(object): - v1 = v1.Controller() + v1 = controller.Controller() @pecan.expose('json') def index(self): diff --git a/tuskar/api/controllers/v1/__init__.py b/tuskar/api/controllers/v1/__init__.py index e2db9348..e69de29b 100644 --- a/tuskar/api/controllers/v1/__init__.py +++ b/tuskar/api/controllers/v1/__init__.py @@ -1,24 +0,0 @@ -# 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 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, - OvercloudsController, RacksController, ResourceClassesController, - NodesController) diff --git a/tuskar/api/controllers/v1/controller.py b/tuskar/api/controllers/v1/controller.py index b672021c..40f7b166 100644 --- a/tuskar/api/controllers/v1/controller.py +++ b/tuskar/api/controllers/v1/controller.py @@ -1,30 +1,29 @@ -#from oslo.config import cfg +# -*- encoding: utf-8 -*- +# +# 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 pecan.core import render -#from pecan import rest -#import wsme -#from wsme import api -#from wsme import types as wtypes -#import wsmeext.pecan as wsme_pecan -#from tuskar.common import exception -#from tuskar.openstack.common import log - -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 +from tuskar.api.controllers.v1.resource_category \ + import ResourceCategoriesController class Controller(object): """Version 1 API controller root.""" - racks = RacksController() - resource_classes = ResourceClassesController() - data_centers = DataCenterController() + resource_categories = ResourceCategoriesController() overclouds = OvercloudsController() - nodes = NodesController() @pecan.expose('json') def index(self): diff --git a/tuskar/api/controllers/v1/data_center.py b/tuskar/api/controllers/v1/data_center.py deleted file mode 100644 index ff97bb80..00000000 --- a/tuskar/api/controllers/v1/data_center.py +++ /dev/null @@ -1,76 +0,0 @@ -#from oslo.config import cfg -import pecan -from pecan.core import render -from pecan import rest -import wsme -#from wsme import api -#from wsme import types as wtypes -#import wsmeext.pecan as wsme_pecan - -from tuskar.compute.nova import NovaClient -from tuskar.heat.client import HeatClient as heat_client - -#from tuskar.common import exception -#from tuskar.openstack.common import log - - -class DataCenterController(rest.RestController): - """Controller for provisioning the Tuskar data centre description as an - overcloud on Triple O - """ - _custom_actions = {'template': ['GET']} - - @pecan.expose('json') - def get_all(self): - heat = heat_client() - return heat.get_stack().to_dict() - - @pecan.expose() - def template(self): - rcs = pecan.request.dbapi.get_heat_data() - nova_utils = NovaClient() - return render('overcloud.yaml', dict(resource_classes=rcs, - nova_util=nova_utils)) - - @pecan.expose('json') - def post(self): - # TODO(): Currently all Heat parameters are hardcoded in - # template. - params = {} - rcs = pecan.request.dbapi.get_heat_data() - heat = heat_client() - nova_utils = NovaClient() - - for resource in rcs: - service_type = resource.service_type - image_id = getattr(resource, "image_id", None) - - if image_id: - if service_type == 'compute': - params['NovaImage'] = image_id - elif service_type in ('not_compute', 'controller'): - params['notcomputeImage'] = image_id - - template_body = render('overcloud.yaml', dict(resource_classes=rcs, - nova_util=nova_utils)) - if heat.validate_template(template_body): - - if heat.exists_stack(): - res = heat.update_stack(template_body, params) - else: - res = heat.create_stack(template_body, params) - - if res: - for rc in rcs: - [pecan.request.dbapi.update_rack_state( - r, 'CREATE_IN_PROGRESS') for r in rc.racks] - - pecan.response.status_code = 202 - return {} - else: - raise wsme.exc.ClientSideError(_( - "Cannot update the Heat overcloud template" - )) - else: - raise wsme.exc.ClientSideError(_("The overcloud Heat template" - "could not be validated")) diff --git a/tuskar/api/controllers/v1/flavor.py b/tuskar/api/controllers/v1/flavor.py deleted file mode 100644 index a295e512..00000000 --- a/tuskar/api/controllers/v1/flavor.py +++ /dev/null @@ -1,80 +0,0 @@ -#from oslo.config import cfg -import pecan -#from pecan.core import render -from pecan import rest -import wsme -#from wsme import api -from wsme import types as wtypes -import wsmeext.pecan as wsme_pecan - -#from tuskar.common import exception -from tuskar.openstack.common import log - -from tuskar.api.controllers.v1.types import Flavor - -LOG = log.getLogger(__name__) - - -class FlavorsController(rest.RestController): - """REST controller for Flavor.""" - - #nova=NovaClient() - - #POST /api/resource_classes/1/flavors - @wsme.validate(Flavor) - @wsme_pecan.wsexpose(Flavor, wtypes.text, body=Flavor, status_code=201) - def post(self, resource_class_id, flavor): - """Create a new Flavor for a ResourceClass.""" - try: - flavor = pecan.request.dbapi.create_resource_class_flavor( - resource_class_id, flavor) - #nova_flavor_uuid = self.nova.create_flavor( - # flavor, - # pecan.request.dbapi.get_resource_class(resource_class_id) - # .name - #) - #pecan.request.dbapi.update_flavor_nova_uuid(flavor.id, - # nova_flavor_uuid) - except Exception as e: - LOG.exception(e) - raise wsme.exc.ClientSideError(_("Invalid data")) - pecan.response.status_code = 201 - return Flavor.add_capacities(resource_class_id, flavor) - - #Do we need this, i.e. GET /api/resource_classes/1/flavors - #i.e. return just the flavors for a given resource_class? - @wsme_pecan.wsexpose([Flavor], wtypes.text) - def get_all(self, resource_class_id): - """Retrieve a list of all flavors.""" - flavors = [] - for flavor in pecan.request.dbapi.get_flavors(resource_class_id): - flavors.append(Flavor.add_capacities(resource_class_id, flavor)) - - return flavors - #return [Flavor.from_db_model(flavor) for flavor in result] - - @wsme_pecan.wsexpose(Flavor, wtypes.text, wtypes.text) - def get_one(self, resource_id, flavor_id): - """Retrieve a specific flavor.""" - flavor = pecan.request.dbapi.get_flavor(flavor_id) - return Flavor.add_capacities(resource_id, flavor) - - @wsme.validate(Flavor) - @wsme_pecan.wsexpose(Flavor, wtypes.text, wtypes.text, body=Flavor) - def put(self, resource_class_id, flavor_id, flavor): - """Update an existing ResourceClass Flavor""" - try: - flavor = pecan.request.dbapi.update_resource_class_flavor( - resource_class_id, flavor_id, flavor) - except Exception as e: - LOG.exception(e) - raise wsme.exc.ClientSideError(_("Invalid data")) - return Flavor.add_capacities(resource_class_id, flavor) - - @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204) - def delete(self, resource_class_id, flavor_id): - """Delete a Flavor.""" - #pecan.response.status_code = 204 - #nova_flavor_uuid = pecan.request.dbapi.delete_flavor(flavor_id) - pecan.request.dbapi.delete_flavor(flavor_id) - #self.nova.delete_flavor(nova_flavor_uuid) diff --git a/tuskar/api/controllers/v1/models.py b/tuskar/api/controllers/v1/models.py new file mode 100644 index 00000000..0f655fd5 --- /dev/null +++ b/tuskar/api/controllers/v1/models.py @@ -0,0 +1,142 @@ +# -*- encoding: utf-8 -*- +# +# 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. + +""" +Contains transfer objects for use with WSME REST APIs. The objects in this +module also contain the translations between the REST transfer objects and +the internal Tuskar domain model. +""" + +import logging +from wsme import types as wtypes + +from tuskar.db.sqlalchemy import models as db_models + + +LOG = logging.getLogger(__name__) + + +class Base(wtypes.Base): + """Base functionality for all API models. + + This class should never be directly instantiated. Subclasses must be sure + to define an attribute named _db_class for the to_db_model to use + when instantiating DB models. + """ + + @classmethod + def from_db_model(cls, db_model, skip_fields=None): + """Returns the database representation of the given transfer object.""" + skip_fields = skip_fields or [] + data = dict((k, v) for k, v in db_model.as_dict().items() + if k not in skip_fields) + return cls(**data) + + def to_db_model(self, omit_unset=False, skip_fields=None): + """Converts this object into its database representation.""" + skip_fields = skip_fields or [] + attribute_names = [a.name for a in self._wsme_attributes + if a.name not in skip_fields] + + if omit_unset: + attribute_names = [n for n in attribute_names + if getattr(self, n) != wtypes.Unset] + + values = dict((name, self._lookup(name)) for name in attribute_names) + db_object = self._db_class(**values) + return db_object + + def _lookup(self, key): + """Looks up a key, translating WSME's Unset into Python's None. + + :return: value of the given attribute; None if it is not set + """ + value = getattr(self, key) + if value == wtypes.Unset: + value = None + return value + + +class ResourceCategory(Base): + """Transfer object for resource categories.""" + + _db_class = db_models.ResourceCategory + + id = int + name = wtypes.text + description = wtypes.text + image_id = wtypes.text + + +class OvercloudCategoryCount(Base): + """Transfer object for overcloud category counts.""" + + _db_class = db_models.OvercloudCategoryCount + + id = int + resource_category_id = int + overcloud_id = int + num_nodes = int + + +class Overcloud(Base): + """Transfer object for overclouds.""" + + _db_class = db_models.Overcloud + + id = int + stack_id = wtypes.text + name = wtypes.text + description = wtypes.text + attributes = {wtypes.text: wtypes.text} + counts = [OvercloudCategoryCount] + + @classmethod + def from_db_model(cls, db_overcloud, skip_fields=None): + # General Data + transfer_overcloud = super(Overcloud, cls)\ + .from_db_model(db_overcloud, skip_fields=['attributes', 'counts']) + + # Attributes + translated = {} + for db_attribute in db_overcloud.attributes: + translated[db_attribute.key] = db_attribute.value + transfer_overcloud.attributes = translated + + # Counts + transfer_overcloud.counts = [OvercloudCategoryCount.from_db_model(c) + for c in db_overcloud.counts] + return transfer_overcloud + + def to_db_model(self, omit_unset=False, skip_fields=None): + # General Data + db_model = super(Overcloud, self).to_db_model( + omit_unset=omit_unset, + skip_fields=['attributes', 'counts']) + + # Attributes + if self.attributes != wtypes.Unset: + + translated = [] + for key, value in self.attributes.items(): + translated.append(db_models.OvercloudAttribute( + key=key, value=value, overcloud_id=self.id + )) + db_model.attributes = translated + + # Counts + if self.counts != wtypes.Unset: + db_model.counts = [c.to_db_model() for c in self.counts] + + return db_model diff --git a/tuskar/api/controllers/v1/node.py b/tuskar/api/controllers/v1/node.py deleted file mode 100644 index cdae56e8..00000000 --- a/tuskar/api/controllers/v1/node.py +++ /dev/null @@ -1,42 +0,0 @@ -import pecan -from pecan import rest - -import wsmeext.pecan as wsme_pecan - -from tuskar.api.controllers.v1.types import Error -from tuskar.api.controllers.v1.types import Node -from tuskar.common import exception -from tuskar.openstack.common import log -from wsme import api - -LOG = log.getLogger(__name__) - - -class NodesController(rest.RestController): - """REST controller for Node.""" - - @wsme_pecan.wsexpose([Node]) - def get_all(self): - """Retrieve a list of all nodes.""" - result = [] - db_api = pecan.request.dbapi - - for node in db_api.get_nodes(None): - result.append(Node.convert(node)) - - return result - - @wsme_pecan.wsexpose(Node, unicode) - def get_one(self, node_id): - """Retrieve an instance of a Node.""" - db_api = pecan.request.dbapi - - try: - node = db_api.get_node(node_id) - except exception.TuskarException as e: - response = api.Response(None, - error=Error(faultcode=e.code, - faultstring=str(e)), - status_code=e.code) - return response - return Node.convert(node) diff --git a/tuskar/api/controllers/v1/overcloud.py b/tuskar/api/controllers/v1/overcloud.py index ca8329e2..2f233de0 100644 --- a/tuskar/api/controllers/v1/overcloud.py +++ b/tuskar/api/controllers/v1/overcloud.py @@ -1,66 +1,144 @@ -# 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 +# -*- encoding: utf-8 -*- # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 # -# 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. +# 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 logging import pecan -from wsme import api +from pecan import rest +import wsme 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 _ +from tuskar.api.controllers.v1 import models -class OvercloudsController(pecan.rest.RestController): - """Controller for Overcloud.""" +LOG = logging.getLogger(__name__) - @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 +class OvercloudsController(rest.RestController): + """REST controller for the Overcloud class.""" - 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 + @wsme.validate(models.Overcloud) + @wsme_pecan.wsexpose(models.Overcloud, + body=models.Overcloud, + status_code=201) + def post(self, transfer_overcloud): + """Creates a new overcloud. - 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 + :param transfer_overcloud: data submitted by the user + :type transfer_overcloud: + tuskar.api.controllers.v1.models.Overcloud - keystone_link = Link(rel='keystone', - href=keystone_param[0]['output_value']) - overcloud = Overcloud(stack_name=stack_name, - links=[keystone_link]) + :return: created overcloud + :rtype: tuskar.api.controllers.v1.models.Overcloud - return overcloud + :raises: tuskar.common.exception.OvercloudExists: if an overcloud + with the given name exists + """ + + LOG.debug('Creating overcloud: %s' % transfer_overcloud) + + # Persist to the database + db_overcloud = transfer_overcloud.to_db_model() + result = pecan.request.dbapi.create_overcloud(db_overcloud) + + # Package for transfer back to the user + saved_overcloud =\ + models.Overcloud.from_db_model(result) + + return saved_overcloud + + @wsme.validate(models.Overcloud) + @wsme_pecan.wsexpose(models.Overcloud, + int, + body=models.Overcloud) + def put(self, overcloud_id, overcloud_delta): + """Updates an existing overcloud, including its attributes and counts. + + :param overcloud_id: identifies the overcloud being deleted + :type overcloud_id: int + + :param overcloud_delta: contains only values that are to be affected + by the update + :type overcloud_delta: + tuskar.api.controllers.v1.models.Overcloud + + :return: created overcloud + :rtype: tuskar.api.controllers.v1.models.Overcloud + + :raises: tuskar.common.exception.OvercloudNotFound if there + is no overcloud with the given ID + """ + LOG.debug('Updating overcloud: %s' % overcloud_id) + + # ID is in the URL so make sure it's in the transfer object + # before translation + overcloud_delta.id = overcloud_id + db_delta = overcloud_delta.to_db_model(omit_unset=True) + + # Will raise a not found if there is no overcloud with the ID + result = pecan.request.dbapi.update_overcloud(db_delta) + + updated = models.Overcloud.from_db_model(result) + + return updated + + @wsme_pecan.wsexpose(None, int, status_code=204) + def delete(self, overcloud_id): + """Deletes the given overcloud. + + :param overcloud_id: identifies the overcloud being deleted + :type overcloud_id: int + + :raises: tuskar.common.exception.OvercloudNotFound if there + is no overcloud with the given ID + """ + + LOG.debug('Deleting overcloud with ID: %s' % overcloud_id) + pecan.request.dbapi.delete_overcloud_by_id(overcloud_id) + + @wsme_pecan.wsexpose(models.Overcloud, int) + def get_one(self, overcloud_id): + """Returns a specific overcloud. + + An exception is raised if no overcloud is found with the + given ID. + + :param overcloud_id: identifies the overcloud being deleted + :type overcloud_id: int + + :return: matching overcloud + :rtype: tuskar.api.controllers.v1.models.Overcloud + + :raises: tuskar.common.exception.OvercloudNotFound if there + is no overcloud with the given ID + """ + + LOG.debug('Retrieving overcloud with ID: %s' % overcloud_id) + overcloud = pecan.request.dbapi.get_overcloud_by_id(overcloud_id) + transfer_overcloud = models.Overcloud.from_db_model(overcloud) + return transfer_overcloud + + @wsme_pecan.wsexpose([models.Overcloud]) + def get_all(self): + """Returns all overclouds. + + An empty list is returned if no overclouds are present. + + :return: list of overclouds; empty list if none are found + :rtype: list of tuskar.api.controllers.v1.models.Overcloud + """ + LOG.debug('Retrieving all overclouds') + overclouds = pecan.request.dbapi.get_overclouds() + transfer_overclouds = [models.Overcloud.from_db_model(o) + for o in overclouds] + return transfer_overclouds diff --git a/tuskar/api/controllers/v1/rack.py b/tuskar/api/controllers/v1/rack.py deleted file mode 100644 index 1601b29a..00000000 --- a/tuskar/api/controllers/v1/rack.py +++ /dev/null @@ -1,122 +0,0 @@ -#from oslo.config import cfg - -import pecan -#from pecan.core import render -from pecan import rest - -import wsme -from wsme import api -from wsme import types as wtypes -import wsmeext.pecan as wsme_pecan - -from tuskar.api.controllers.v1.types import Error -from tuskar.api.controllers.v1.types import Link -from tuskar.api.controllers.v1.types import Rack -from tuskar.common import exception -#from tuskar.compute.nova import NovaClient -from tuskar.heat.client import HeatClient as heat_client -from tuskar.openstack.common import log - -LOG = log.getLogger(__name__) - - -class RacksController(rest.RestController): - """REST controller for Rack.""" - - @wsme.validate(Rack) - @wsme_pecan.wsexpose(Rack, body=Rack, status_code=201) - def post(self, rack): - """Create a new Rack.""" - try: - result = pecan.request.dbapi.create_rack(rack) - links = [Link.build('self', - pecan.request.host_url, - 'racks', - result.id)] - except Exception as e: - LOG.exception(e) - raise wsme.exc.ClientSideError(_("Invalid data")) - - # 201 Created require Location header pointing to newly created - # resource - # - # FIXME(mfojtik): For some reason, Pecan does not return 201 here - # as configured above - # - pecan.response.headers['Location'] = str(links[0].href) - pecan.response.status_code = 201 - return Rack.convert_with_links(result, links) - - @wsme.validate(Rack) - @wsme_pecan.wsexpose(Rack, wtypes.text, body=Rack, status_code=200) - def put(self, rack_id, rack): - """Update the Rack.""" - - try: - result = pecan.request.dbapi.update_rack(rack_id, rack) - links = [Link.build('self', pecan.request.host_url, 'racks', - result.id)] - # - # TODO(mfojtik): Update the HEAT template at this point - # - except Exception as e: - LOG.exception(e) - raise wsme.exc.ClientSideError(_("Invalid data")) - return Rack.convert_with_links(result, links) - - @wsme_pecan.wsexpose([Rack]) - def get_all(self): - """Retrieve a list of all racks.""" - result = [] - links = [] - db_api = pecan.request.dbapi - heat_stack = False - if heat_client().exists_stack(): - heat_stack = heat_client().get_stack() - - for rack in db_api.get_racks(None): - if heat_stack: - db_api.update_rack_state(rack, heat_stack.stack_status) - links = [Link.build('self', pecan.request.host_url, 'racks', - rack.id)] - result.append(Rack.convert_with_links(rack, links)) - - return result - - @wsme_pecan.wsexpose(Rack, unicode) - def get_one(self, rack_id): - """Retrieve information about the given Rack.""" - db_api = pecan.request.dbapi - try: - rack = db_api.get_rack(rack_id) - except exception.TuskarException as e: - response = api.Response( - None, - error=Error(faultcode=e.code, faultstring=str(e)), - status_code=e.code) - return response - - heat_stack = False - if heat_client().exists_stack(): - heat_stack = heat_client().get_stack() - - if heat_stack: - db_api.update_rack_state(rack, heat_stack.stack_status) - links = [Link.build('self', pecan.request.host_url, 'racks', - rack.id)] - return Rack.convert_with_links(rack, links) - - @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) - def delete(self, rack_id): - """Remove the Rack.""" - try: - pecan.request.dbapi.delete_rack(rack_id) - pecan.response.status_code = 204 - # - # TODO(mfojtik): Update the HEAT template at this point - # - except exception.TuskarException as e: - response = api.Response( - Error(faultcode=e.code, faultstring=str(e)), - status_code=e.code) - return response diff --git a/tuskar/api/controllers/v1/resource_category.py b/tuskar/api/controllers/v1/resource_category.py new file mode 100644 index 00000000..72495ed5 --- /dev/null +++ b/tuskar/api/controllers/v1/resource_category.py @@ -0,0 +1,144 @@ +# -*- encoding: utf-8 -*- +# +# 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 logging +import pecan +from pecan import rest +import wsme +from wsmeext import pecan as wsme_pecan + +from tuskar.api.controllers.v1 import models + + +LOG = logging.getLogger(__name__) + + +class ResourceCategoriesController(rest.RestController): + """REST controller for the ResourceCategory class.""" + + @wsme.validate(models.ResourceCategory) + @wsme_pecan.wsexpose(models.ResourceCategory, + body=models.ResourceCategory, + status_code=201) + def post(self, transfer_category): + """Creates a new resource category. + + :param transfer_category: data submitted by the user + :type transfer_category: + tuskar.api.controllers.v1.models.ResourceCategory + + :return: created category + :rtype: tuskar.api.controllers.v1.models.ResourceCategory + + :raises: tuskar.common.exception.ResourceCategoryExists: if a resource + category with the given name exists + """ + + LOG.debug('Creating resource category: %s' % transfer_category) + + # Persist to the database + db_category = transfer_category.to_db_model() + result = pecan.request.dbapi.create_resource_category(db_category) + + # Package for transfer back to the user + saved_category =\ + models.ResourceCategory.from_db_model(result) + + return saved_category + + @wsme.validate(models.ResourceCategory) + @wsme_pecan.wsexpose(models.ResourceCategory, + int, + body=models.ResourceCategory) + def put(self, category_id, category_delta): + """Updates an existing resource category. + + :param category_id: identifies the category being deleted + :type category_id: int + + :param category_delta: contains only values that are to be affected + by the update operation + :type category_delta: + tuskar.api.controllers.v1.models.ResourceCategory + + :return: category with updated values + :rtype: tuskar.api.controllers.v1.models.ResourceCategory + + :raises: tuskar.common.exception.ResourceCategoryNotFound if there + is no category with the given ID + """ + + LOG.debug('Updating resource category: %s' % category_id) + + # ID is in the URL so make sure it's in the transfer object + # before translation + category_delta.id = category_id + + db_delta = category_delta.to_db_model(omit_unset=True) + + # Will raise a not found if there is no category with the ID + updated = pecan.request.dbapi.update_resource_category(db_delta) + + return updated + + @wsme_pecan.wsexpose(None, int, status_code=204) + def delete(self, category_id): + """Deletes the given resource category. + + :param category_id: identifies the category being deleted + :type category_id: int + + :raises: tuskar.common.exception.ResourceCategoryNotFound if there + is no category with the given ID + """ + + LOG.debug('Deleting resource category with ID: %s' % category_id) + pecan.request.dbapi.delete_resource_category_by_id(category_id) + + @wsme_pecan.wsexpose(models.ResourceCategory, int) + def get_one(self, category_id): + """Returns a specific resource category. + + An exception is raised if no resource category is found with the + given ID. + + :param category_id: identifies the category being deleted + :type category_id: int + + :return: matching resource category + :rtype: tuskar.api.controllers.v1.models.ResourceCategory + + :raises: tuskar.common.exception.ResourceCategoryNotFound if there + is no category with the given ID + """ + + LOG.debug('Retrieving resource category with ID: %s' % category_id) + category = pecan.request.dbapi.get_resource_category_by_id(category_id) + transfer_category = models.ResourceCategory.from_db_model(category) + return transfer_category + + @wsme_pecan.wsexpose([models.ResourceCategory]) + def get_all(self): + """Returns all resource categories. + + An empty list is returned if no resource categories are present. + + :return: list of categories; empty list if none are found + :rtype: list of tuskar.api.controllers.v1.models.ResourceCategory + """ + LOG.debug('Retrieving all resource categories') + categories = pecan.request.dbapi.get_resource_categories() + transfer_categories = [models.ResourceCategory.from_db_model(c) + for c in categories] + return transfer_categories diff --git a/tuskar/api/controllers/v1/resource_class.py b/tuskar/api/controllers/v1/resource_class.py deleted file mode 100644 index e6459cde..00000000 --- a/tuskar/api/controllers/v1/resource_class.py +++ /dev/null @@ -1,96 +0,0 @@ -#from oslo.config import cfg -import pecan -#from pecan.core import render -from pecan import rest -import wsme -#from wsme import api -from wsme import types as wtypes -import wsmeext.pecan as wsme_pecan - -#from tuskar.common import exception -from tuskar.openstack.common import log - -from tuskar.api.controllers.v1.flavor import FlavorsController -#from tuskar.api.controllers.v1.types import Base -#from tuskar.api.controllers.v1.types import Flavor -#from tuskar.api.controllers.v1.types import Relation -from tuskar.api.controllers.v1.types import ResourceClass - -LOG = log.getLogger(__name__) - - -class ResourceClassesController(rest.RestController): - """REST controller for Resource Class.""" - - flavors = FlavorsController() - - @wsme.validate(ResourceClass) - @wsme_pecan.wsexpose(ResourceClass, body=ResourceClass, status_code=201) - def post(self, resource_class): - """Create a new Resource Class.""" - try: - result = pecan.request.dbapi.create_resource_class(resource_class) - #create in nova any flavors included in this resource_class - #creation for flav in result.flavors: - #nova_flavor_uuid = self.flavors.nova.create_flavor(flav, - # result.name) - #pecan.request.dbapi.update_flavor_nova_uuid(flav.id, - # nova_flavor_uuid) - except Exception as e: - LOG.exception(e) - raise wsme.exc.ClientSideError(_("Invalid data")) - - # 201 Created require Location header pointing to newly created - # resource - # - # FIXME(mfojtik): For some reason, Pecan does not return 201 here - # as configured above - # - rc = ResourceClass.convert(result, pecan.request.host_url) - pecan.response.headers['Location'] = str(rc.links[0].href) - pecan.response.status_code = 201 - return rc - - @wsme.validate(ResourceClass) - @wsme_pecan.wsexpose(ResourceClass, wtypes.text, body=ResourceClass, - status_code=200) - def put(self, resource_class_id, resource_class): - try: - result = pecan.request.dbapi.update_resource_class( - resource_class_id, - resource_class) - # - # TODO(mfojtik): Update the HEAT template at this point - # - except Exception as e: - LOG.exception(e) - raise wsme.exc.ClientSideError(_("Invalid data")) - return ResourceClass.convert(result, pecan.request.host_url) - - @wsme_pecan.wsexpose([ResourceClass]) - def get_all(self): - """Retrieve a list of all Resource Classes.""" - result = [] - for rc in pecan.request.dbapi.get_resource_classes(None): - result.append(ResourceClass.convert(rc, pecan.request.host_url)) - - return result - - @wsme_pecan.wsexpose(ResourceClass, unicode) - def get_one(self, resource_class_id): - """Retrieve information about the given Resource Class.""" - dbapi = pecan.request.dbapi - resource_class = dbapi.get_resource_class(resource_class_id) - return ResourceClass.convert(resource_class, pecan.request.host_url) - - @wsme_pecan.wsexpose(None, wtypes.text, status_code=204) - def delete(self, resource_class_id): - """Remove the Resource Class.""" - # - # TODO(mfojtik): Update the HEAT template at this point - # - #DELETE any resource class flavors from nova too - #for flav in pecan.request.dbapi.get_flavors(resource_class_id): - # nova_flavor_uuid = pecan.request.dbapi.delete_flavor(flav.id) - # self.flavors.nova.delete_flavor(nova_flavor_uuid) - pecan.request.dbapi.delete_resource_class(resource_class_id) diff --git a/tuskar/api/controllers/v1/types/__init__.py b/tuskar/api/controllers/v1/types/__init__.py deleted file mode 100644 index 47a462dd..00000000 --- a/tuskar/api/controllers/v1/types/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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 tuskar.api.controllers.v1.types.base import Base -from tuskar.api.controllers.v1.types.capacity import Capacity -from tuskar.api.controllers.v1.types.chassis import Chassis -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, Overcloud, Rack, - Relation, ResourceClass) diff --git a/tuskar/api/controllers/v1/types/base.py b/tuskar/api/controllers/v1/types/base.py deleted file mode 100644 index e093c236..00000000 --- a/tuskar/api/controllers/v1/types/base.py +++ /dev/null @@ -1,49 +0,0 @@ -# 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 wsme -from wsme import types as wtypes - - -class Base(wsme.types.Base): - - def __init__(self, **kwargs): - self.fields = list(kwargs) - for k, v in kwargs.iteritems(): - setattr(self, k, v) - - @classmethod - def from_db_model(cls, m): - return cls(**m.as_dict()) - - @classmethod - def from_db_and_links(cls, m, links): - return cls(links=links, **(m.as_dict())) - - def as_dict(self): - return dict((k, getattr(self, k)) - for k in self.fields - if hasattr(self, k) and - getattr(self, k) != wsme.Unset) - - def get_id(self): - """Returns the ID of this resource as specified in the self link.""" - - # FIXME(mtaylor) We should use a more robust method for parsing the URL - if not isinstance(self.id, wtypes.UnsetType): - return self.id - elif not isinstance(self.links, wtypes.UnsetType): - return self.links[0].href.split("/")[-1] - else: - raise wsme.exc.ClientSideError(_("No ID or URL Set for Resource")) diff --git a/tuskar/api/controllers/v1/types/capacity.py b/tuskar/api/controllers/v1/types/capacity.py deleted file mode 100644 index a589ab2a..00000000 --- a/tuskar/api/controllers/v1/types/capacity.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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 wsme -from wsme import types as wtypes - -from tuskar.api.controllers.v1.types.base import Base - - -class Capacity(Base): - """A capacity representation.""" - - name = wtypes.text - value = wtypes.text - unit = wtypes.text diff --git a/tuskar/api/controllers/v1/types/chassis.py b/tuskar/api/controllers/v1/types/chassis.py deleted file mode 100644 index 076d97cb..00000000 --- a/tuskar/api/controllers/v1/types/chassis.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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 wsme -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 Chassis(Base): - """A chassis representation.""" - - id = wtypes.text - links = [Link] diff --git a/tuskar/api/controllers/v1/types/error.py b/tuskar/api/controllers/v1/types/error.py deleted file mode 100644 index 05d8fbf7..00000000 --- a/tuskar/api/controllers/v1/types/error.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 wsme -from wsme import types as wtypes - -from tuskar.api.controllers.v1.types import base - - -class Error(base.Base): - """An error representation.""" - - faultcode = int - faultstring = wtypes.text diff --git a/tuskar/api/controllers/v1/types/flavor.py b/tuskar/api/controllers/v1/types/flavor.py deleted file mode 100644 index 84271a19..00000000 --- a/tuskar/api/controllers/v1/types/flavor.py +++ /dev/null @@ -1,44 +0,0 @@ -# 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 pecan -import wsme -from wsme import types as wtypes - -from tuskar.api.controllers.v1.types.base import Base -from tuskar.api.controllers.v1.types.capacity import Capacity -from tuskar.api.controllers.v1.types.link import Link - - -class Flavor(Base): - """A representation of Flavor in HTTP body.""" - #FIXME - I want id to be UUID - String - id = wsme.wsattr(int, mandatory=False) - name = wsme.wsattr(wtypes.text, mandatory=False) - max_vms = wsme.wsattr(int, mandatory=False) - capacities = [Capacity] - links = [Link] - - @classmethod - def add_capacities(self, rc_id, flavor): - capacities = [] - for c in flavor.capacities: - capacities.append(Capacity(name=c.name, - value=c.value, - unit=c.unit)) - - links = [Link.build('self', pecan.request.host_url, - "resource_classes/%s/flavors" % rc_id, flavor.id - )] - - return Flavor(capacities=capacities, links=links, **(flavor.as_dict())) diff --git a/tuskar/api/controllers/v1/types/link.py b/tuskar/api/controllers/v1/types/link.py deleted file mode 100644 index 3fc3697d..00000000 --- a/tuskar/api/controllers/v1/types/link.py +++ /dev/null @@ -1,48 +0,0 @@ -# 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 - -#import wsme -from wsme import types as wtypes - -from tuskar.api.controllers.v1.types.base import Base - -CONF = cfg.CONF - -ironic_opts = [ - cfg.StrOpt('ironic_url', - default='http://ironic.local:6543/v1', - help='Ironic API entrypoint URL'), -] - -CONF.register_opts(ironic_opts) - - -class Link(Base): - """A link representation.""" - - href = wtypes.text - "The url of a link." - - rel = wtypes.text - "The name of a link." - - @classmethod - def build(self, rel_name, url, type, type_arg): - return Link(href=('%s/v1/%s/%s') % (url, type, type_arg), rel=rel_name) - - @classmethod - def build_ironic_link(self, rel_name, resource_id): - return Link(href=('%s/%s') % (CONF.ironic_url, resource_id), - rel=rel_name) diff --git a/tuskar/api/controllers/v1/types/node.py b/tuskar/api/controllers/v1/types/node.py deleted file mode 100644 index dea625ce..00000000 --- a/tuskar/api/controllers/v1/types/node.py +++ /dev/null @@ -1,43 +0,0 @@ -# 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 pecan -from wsme import types as wtypes - -from tuskar.api.controllers.v1.types.base import Base -from tuskar.api.controllers.v1.types.link import Link -from tuskar.api.controllers.v1.types.relation import Relation - - -class Node(Base): - """A Node representation.""" - - id = wtypes.text - # FIXME: We expose this as nova_baremetal_node_id, but are not yet changing - # the column name in the database, because this is a more involved change. - nova_baremetal_node_id = wtypes.text - rack = Relation - links = [Link] - - @classmethod - def convert(self, node): - kwargs = node.as_dict() - links = [Link.build('self', pecan.request.host_url, 'nodes', - node.id)] - rack_link = [Link.build('self', pecan.request.host_url, - 'racks', node.rack_id)] - kwargs['rack'] = Relation(id=node.rack_id, links=rack_link) - kwargs['id'] = str(node.id) - kwargs['nova_baremetal_node_id'] = str(node.node_id) - return Node(links=links, **kwargs) diff --git a/tuskar/api/controllers/v1/types/overcloud.py b/tuskar/api/controllers/v1/types/overcloud.py deleted file mode 100644 index 732cc2b6..00000000 --- a/tuskar/api/controllers/v1/types/overcloud.py +++ /dev/null @@ -1,23 +0,0 @@ -# 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/api/controllers/v1/types/rack.py b/tuskar/api/controllers/v1/types/rack.py deleted file mode 100644 index 5a84693c..00000000 --- a/tuskar/api/controllers/v1/types/rack.py +++ /dev/null @@ -1,79 +0,0 @@ -# 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 pecan -#import wsme -from wsme import types as wtypes - -from tuskar.api.controllers.v1.types.base import Base -from tuskar.api.controllers.v1.types.capacity import Capacity -from tuskar.api.controllers.v1.types.chassis import Chassis -from tuskar.api.controllers.v1.types.link import Link -from tuskar.api.controllers.v1.types.node import Node -from tuskar.api.controllers.v1.types.relation import Relation - - -class Rack(Base): - """A representation of Rack in HTTP body.""" - - id = int - name = wtypes.text - slots = int - subnet = wtypes.text - location = wtypes.text - state = wtypes.text - chassis = Chassis - capacities = [Capacity] - nodes = [Node] - links = [Link] - resource_class = Relation - - @classmethod - def convert_with_links(self, rack, links): - - kwargs = rack.as_dict() # returns a new dict, overwriting keys is safe - - if rack.chassis_id: - kwargs['chassis'] = Chassis(id=rack.chassis_id, - links=[Link.build_ironic_link( - 'chassis', rack.chassis_id)]) - else: - kwargs['chassis'] = Chassis() - - if rack.resource_class_id: - l = [Link.build('self', pecan.request.host_url, 'resource_classes', - rack.resource_class_id)] - kwargs['resource_class'] = Relation(id=rack.resource_class_id, - links=l) - - kwargs['capacities'] = [Capacity(name=c.name, value=c.value, - unit=c.unit) - for c in rack.capacities] - - kwargs['nodes'] = [Node(id=str(n.id), - node_id=n.node_id, - links=[ - Link.build('self', - pecan.request.host_url, - 'nodes', n.id) - ]) - for n in rack.nodes] - - return Rack(links=links, **kwargs) - - @classmethod - def convert(self, rack, base_url, minimal=False): - links = [Link.build('self', pecan.request.host_url, 'rack', - rack.id)] - if minimal: - return Rack(links=links, id=str(rack.id)) diff --git a/tuskar/api/controllers/v1/types/relation.py b/tuskar/api/controllers/v1/types/relation.py deleted file mode 100644 index c7e539f3..00000000 --- a/tuskar/api/controllers/v1/types/relation.py +++ /dev/null @@ -1,25 +0,0 @@ -# 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 wsme -#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 Relation(Base): - """A representation of a 1 to 1 or 1 to many relation in the database.""" - - id = int - links = [Link] diff --git a/tuskar/api/controllers/v1/types/resource_class.py b/tuskar/api/controllers/v1/types/resource_class.py deleted file mode 100644 index b1bd46b6..00000000 --- a/tuskar/api/controllers/v1/types/resource_class.py +++ /dev/null @@ -1,57 +0,0 @@ -# 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 pecan -#import wsme -from wsme import types as wtypes - -from tuskar.api.controllers.v1.types.base import Base -from tuskar.api.controllers.v1.types.flavor import Flavor -from tuskar.api.controllers.v1.types.link import Link -from tuskar.api.controllers.v1.types.relation import Relation - - -class ResourceClass(Base): - """A representation of Resource Class in HTTP body.""" - - id = int - name = wtypes.text - service_type = wtypes.text - image_id = wtypes.wsattr(wtypes.text, mandatory=False, default=None) - racks = [Relation] - flavors = [Flavor] - links = [Link] - - @classmethod - def convert(self, resource_class, base_url, minimal=False): - links = [Link.build('self', pecan.request.host_url, 'resource_classes', - resource_class.id)] - if minimal: - return ResourceClass(links=links, id=str(resource_class.id)) - else: - racks = [] - if resource_class.racks: - for r in resource_class.racks: - l = [Link.build('self', pecan.request.host_url, - 'racks', r.id)] - rack = Relation(id=r.id, links=l) - racks.append(rack) - - flavors = [] - if resource_class.flavors: - for flav in resource_class.flavors: - - flavor = Flavor.add_capacities(resource_class.id, flav) - flavors.append(flavor) - return ResourceClass(links=links, racks=racks, flavors=flavors, - **(resource_class.as_dict())) diff --git a/tuskar/common/exception.py b/tuskar/common/exception.py index d1f17f5b..01416899 100644 --- a/tuskar/common/exception.py +++ b/tuskar/common/exception.py @@ -19,12 +19,8 @@ Includes decorator for re-raising Tuskar-type exceptions. SHOULD include dedicated exception logging. """ -import functools - from oslo.config import cfg -from tuskar.common import safe_utils -from tuskar.openstack.common import excutils from tuskar.openstack.common.gettextutils import _ # noqa from tuskar.openstack.common import log as logging @@ -68,46 +64,6 @@ def _cleanse_dict(original): return dict((k, v) for k, v in original.iteritems() if not "_pass" in k) -def wrap_exception(notifier=None, publisher_id=None, event_type=None, - level=None): - """This decorator wraps a method to catch any exceptions that may - get thrown. It logs the exception as well as optionally sending - it to the notification system. - """ - def inner(f): - def wrapped(self, context, *args, **kw): - # Don't store self or context in the payload, it now seems to - # contain confidential information. - try: - return f(self, context, *args, **kw) - except Exception as e: - with excutils.save_and_reraise_exception(): - if notifier: - payload = dict(exception=e) - call_dict = safe_utils.getcallargs(f, *args, **kw) - cleansed = _cleanse_dict(call_dict) - payload.update({'args': cleansed}) - - # Use a temp vars so we don't shadow - # our outer definitions. - temp_level = level - if not temp_level: - temp_level = notifier.ERROR - - temp_type = event_type - if not temp_type: - # If f has multiple decorators, they must use - # functools.wraps to ensure the name is - # propagated. - temp_type = f.__name__ - - notifier.notify(context, publisher_id, temp_type, - temp_level, payload) - - return functools.wraps(f)(wrapped) - return inner - - class TuskarException(Exception): """Base Tuskar Exception @@ -169,70 +125,11 @@ class PolicyNotAuthorized(NotAuthorized): message = _("Policy doesn't allow %(action)s to be performed.") -class Invalid(TuskarException): - message = _("Unacceptable parameters.") - code = 400 - - -class InvalidCPUInfo(Invalid): - message = _("Unacceptable CPU info") + ": %(reason)s" - - -class InvalidIpAddressError(Invalid): - message = _("%(address)s is not a valid IP v4/6 address.") - - -class InvalidDiskFormat(Invalid): - message = _("Disk format %(disk_format)s is not acceptable") - - -class InvalidUUID(Invalid): - message = _("Expected a uuid but received %(uuid)s.") - - -class InvalidMAC(Invalid): - message = _("Expected a MAC address but received %(mac)s.") - - -# Cannot be templated as the error syntax varies. -# msg needs to be constructed when raised. -class InvalidParameterValue(Invalid): - message = _("%(err)s") - - class NotFound(TuskarException): message = _("Resource could not be found.") code = 404 -class DiskNotFound(NotFound): - message = _("No disk at %(location)s") - - -class ImageNotFound(NotFound): - message = _("Image %(image_id)s could not be found.") - - -class HostNotFound(NotFound): - message = _("Host %(host)s could not be found.") - - -class ConsoleNotFound(NotFound): - message = _("Console %(console_id)s could not be found.") - - -class FileNotFound(NotFound): - message = _("File %(file_path)s could not be found.") - - -class NoValidHost(NotFound): - message = _("No valid host was found. %(reason)s") - - -class InstanceNotFound(NotFound): - message = _("Instance %(instance)s could not be found.") - - class ResourceCategoryNotFound(NotFound): message = _('Resource category could not be found.') @@ -245,29 +142,9 @@ class OvercloudNotFound(NotFound): message = _('Overcloud could not be found.') -class NodeLocked(NotFound): - message = _("Node %(node)s is locked by another process.") - - -class PortNotFound(NotFound): - message = _("Port %(port)s could not be found.") - - -class PowerStateFailure(TuskarException): - message = _("Failed to set node power state to %(pstate)s.") - - -class ExclusiveLockRequired(NotAuthorized): - message = _("An exclusive lock is required, " - "but the current context has a shared lock.") - - -class IPMIFailure(TuskarException): - message = _("IPMI command failed: %(cmd)s.") - - class DuplicateEntry(TuskarException): message = _("Duplicate entry found.") + code = 409 class ResourceCategoryExists(DuplicateEntry): diff --git a/tuskar/db/__init__.py b/tuskar/db/__init__.py index 56425d0f..e69de29b 100644 --- a/tuskar/db/__init__.py +++ b/tuskar/db/__init__.py @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# All Rights Reserved. -# -# 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. diff --git a/tuskar/db/sqlalchemy/api.py b/tuskar/db/sqlalchemy/api.py index 9340d1be..5d81d271 100644 --- a/tuskar/db/sqlalchemy/api.py +++ b/tuskar/db/sqlalchemy/api.py @@ -103,20 +103,15 @@ class Connection(api.Connection): return result @staticmethod - def save_resource_category(resource_category): - """Saves a resource category to the database. - - If the ResourceCategory instance does not contain a value for - its id, it will be added as a new resource category. If the id is - present, it is treated as an update to an existing resource category. - Therefore, updates should be made to the instance retrieved from - one of the get_* calls and passed back to this call. + def create_resource_category(resource_category): + """Creates a new resource category in the database. :param resource_category: category instance to save :type resource_category: tuskar.db.sqlalchemy.models.ResourceCategory :return: the resource category instance that was saved with its ID populated + :rtype: tuskar.db.sqlalchemy.models.ResourceCategory :raises: tuskar.common.exception.ResourceCategoryExists: if a resource category with the given name exists @@ -135,6 +130,26 @@ class Connection(api.Connection): finally: session.close() + def update_resource_category(self, updated): + """Updates the given resource category. + + :param updated: category instance containing changed values + :type updated: tuskar.db.sqlalchemy.models.ResourceCategory + + :return: the resource category instance that was saved + :rtype: tuskar.db.sqlalchemy.models.ResourceCategory + + :raises: tuskar.common.exception.ResourceCategoryNotFound if there + is no category with the given ID + """ + existing = self.get_resource_category_by_id(updated.id) + + for a in ('name', 'description', 'image_id'): + if getattr(updated, a) is not None: + setattr(existing, a, getattr(updated, a)) + + return self.create_resource_category(existing) + def delete_resource_category_by_id(self, category_id): """Deletes a resource category from the database. @@ -156,111 +171,6 @@ class Connection(api.Connection): finally: session.close() - @staticmethod - def get_overcloud_category_counts_by_overcloud(overcloud_id): - """Returns all overcloud category counts for a given overcloud. - - :param overcloud_id: database ID for the overcloud whose counts - are being retrieved - :type overcloud_id: int - - :return: list of category counts; empty list if none are found - :rtype: list of tuskar.db.sqlalchemy.models.OvercloudCategoryCount - """ - - session = db_session.get_session() - deployments = \ - session.query(models.OvercloudCategoryCount) \ - .filter_by(overcloud_id=overcloud_id) \ - .all() - session.close() - return deployments - - @staticmethod - def get_overcloud_category_count_by_id(count_id): - """Single overcloud category count query. - - :param count_id: database ID of a saved count instance - :type count_id: int - - :return: count if one exists with the given ID - :rtype: tuskar.db.sqlalchemy.models.OvercloudCategoryCount - - :raises: tuskar.common.exception.OvercloudCategoryCountNotFound: - if no deployment with the given ID exists - """ - - session = db_session.get_session() - try: - query = session.query(models.OvercloudCategoryCount).filter_by( - id=count_id) - result = query.one() - - except NoResultFound: - raise exception.OvercloudCategoryCountNotFound() - - finally: - session.close() - - return result - - @staticmethod - def save_overcloud_category_count(count): - """Saves a deployment to the database. - - If the ResourceCategoryDeployment instance does not contain a value for - its id, it will be added as a new deployment. If the id is - present, it is treated as an update to an existing deployment. - Therefore, updates should be made to the instance retrieved from - one of the get_* calls and passed back to this call. - - :param count: count mapping instance to save - :type count: tuskar.db.sqlalchemy.models.OvercloudCategoryCount - - :return: the deployment instance that was saved with its - ID populated - - :raises: tuskar.common.exception.OvercloudCategoryCountExists: - if a count mapping already exists for the given overcloud and - resource category - """ - session = db_session.get_session() - session.begin() - - try: - session.add(count) - session.commit() - return count - - except db_exception.DBDuplicateEntry: - raise exception.OvercloudCategoryCountExists( - cloud=count.overcloud_id, - cat=count.resource_category_id) - - finally: - session.close() - - def delete_overcloud_category_count(self, count_id): - """Deletes an overcloud category count from the database. - - :param count_id: database ID of the count - :type count_id: int - - :raises: tuskar.common.exception.OvercloudCategoryCountNotFound - if there is none with the given ID - """ - count = self.get_overcloud_category_count_by_id(count_id) - - session = db_session.get_session() - session.begin() - - try: - session.delete(count) - session.commit() - - finally: - session.close() - @staticmethod def get_overclouds(): """Returns all overcloud instances from the database. @@ -272,6 +182,7 @@ class Connection(api.Connection): session = db_session.get_session() overclouds = session.query(models.Overcloud).\ options(subqueryload(models.Overcloud.attributes)).\ + options(subqueryload(models.Overcloud.counts)).\ all() session.close() return overclouds @@ -291,6 +202,7 @@ class Connection(api.Connection): try: query = session.query(models.Overcloud).\ options(subqueryload(models.Overcloud.attributes)).\ + options(subqueryload(models.Overcloud.counts)).\ filter_by(id=overcloud_id) result = query.one() @@ -303,20 +215,15 @@ class Connection(api.Connection): return result @staticmethod - def save_overcloud(overcloud): - """Saves the given overcloud instance to the database. - - If the Overcloud instance does not contain a value for - its id, it will be added as a new overcloud. If the id is present, - it is treated as an update to an existing overcloud. - Therefore, updates should be made to the instance retrieved from - one of the get_* calls and passed back to this call. + def create_overcloud(overcloud): + """Creates a new overcloud instance to the database. :param overcloud: overcloud instance to save :type overcloud: tuskar.db.sqlalchemy.models.Overcloud :return: the overcloud instance that was saved with its ID populated + :rtype: tuskar.db.sqlalchemy.models.Overcloud :raises: tuskar.common.exception.OvercloudExists: if a resource category with the given name exists @@ -338,6 +245,132 @@ class Connection(api.Connection): finally: session.close() + def update_overcloud(self, updated): + """Updates the configuration of an existing overcloud. + + The specified parameter is an instance of the domain model with + the changes to be made. Updating follows the given rules: + - The updated overcloud must include the ID of the overcloud + being updated. + - Any direct attributes on the overcloud that are *not* being changed + should have their values set to None. + - For attributes and counts, only differences are specified according + to the following rules: + - New items are specified in the updated object's lists + - Updated items are specified in the updated object's lists with + the new value and existing key + - Removed items are specified in the updated object's lists with + a value of None (zero in the case of a count). + - Unchanged items are *not* specified. + + :param updated: overcloud instance containing changed values + :type updated: tuskar.db.sqlalchemy.models.Overcloud + + :return: the overcloud instance that was saved + :rtype: tuskar.db.sqlalchemy.models.Overcloud + + :raises: tuskar.common.exception.OvercloudNotFound if there + is no overcloud with the given ID + """ + + existing = self.get_overcloud_by_id(updated.id) + + session = db_session.get_session() + session.begin() + + try: + # First class attributes on the overcloud + for name in ('stack_id', 'name', 'description'): + new_value = getattr(updated, name) + if new_value is not None: + setattr(existing, name, new_value) + + self._update_overcloud_attributes(existing, session, updated) + self._update_overcloud_counts(existing, session, updated) + + # Save the modified object + session.add(existing) + session.commit() + + return existing + + finally: + session.close() + + @staticmethod + def _update_overcloud_attributes(existing, session, updated): + if updated.attributes is not None: + existing_keys = [a.key for a in existing.attributes] + existing_attributes_by_key = \ + dict((a.key, a) for a in existing.attributes) + + delete_keys = [] + for a in updated.attributes: + + # Deleted + if a.value is None: + delete_keys.append(a.key) + continue + + # Updated + if a.key in existing_keys: + updating = existing_attributes_by_key[a.key] + updating.value = a.value + session.add(updating) + continue + + # Added + if a.key not in existing_keys: + existing_attributes_by_key[a.key] = a + a.overcloud_id = updated.id + existing.attributes.append(a) + session.add(a) + continue + + # Purge deleted attributes + for a in existing.attributes: + if a.key in delete_keys: + existing.attributes.remove(a) + session.delete(a) + + @staticmethod + def _update_overcloud_counts(existing, session, updated): + if updated.counts is not None: + existing_count_cat_ids = [c.resource_category_id + for c in existing.counts] + existing_counts_by_cat_id = \ + dict((c.resource_category_id, c) for c in existing.counts) + + delete_category_ids = [] + for c in updated.counts: + + # Deleted + if c.num_nodes == 0: + delete_category_ids.append(c.resource_category_id) + continue + + # Updated + if c.resource_category_id in existing_count_cat_ids: + updating = \ + existing_counts_by_cat_id[c.resource_category_id] + updating.num_nodes = c.num_nodes + session.add(updating) + continue + + # New + if c.resource_category_id not in existing_count_cat_ids: + existing_counts_by_cat_id[c.resource_category_id] = c + c.overcloud_id = updated.id + existing.counts.append(c) + session.add(c) + continue + + # Purge deleted counts + for c in existing.counts: + if c.resource_category_id in delete_category_ids: + existing.counts.remove(c) + session.delete(c) + def delete_overcloud_by_id(self, overcloud_id): """Deletes a overcloud from the database. diff --git a/tuskar/db/sqlalchemy/models.py b/tuskar/db/sqlalchemy/models.py index 35f41c58..c02facb0 100644 --- a/tuskar/db/sqlalchemy/models.py +++ b/tuskar/db/sqlalchemy/models.py @@ -52,6 +52,11 @@ class TuskarBase(models.TimestampMixin, models.ModelBase): """Base class for all Tuskar domain models.""" metadata = None + def as_dict(self): + d = dict([(c.name, self[c.name]) for c in self.__table__.columns]) + return d + + Base = declarative_base(cls=TuskarBase) @@ -205,5 +210,21 @@ class Overcloud(Base): # List of configuration attributes for the overcloud attributes = relationship(OvercloudAttribute.__name__) + # List of counts of resource categories to deploy + counts = relationship(OvercloudCategoryCount.__name__) + def __eq__(self, other): return self.name == other.name + + def as_dict(self): + d = dict([(c.name, self[c.name]) for c in self.__table__.columns]) + + # Foreign keys aren't picked up by the base as_dict, so add them in + # here + attribute_dicts = [a.as_dict() for a in self.attributes] + d['attributes'] = attribute_dicts + + count_dicts = [c.as_dict() for c in self.counts] + d['counts'] = count_dicts + + return d diff --git a/tuskar/drivers/__init__.py b/tuskar/drivers/__init__.py deleted file mode 100644 index 56425d0f..00000000 --- a/tuskar/drivers/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# All Rights Reserved. -# -# 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. diff --git a/tuskar/tests/api/controllers/v1/test_models.py b/tuskar/tests/api/controllers/v1/test_models.py new file mode 100644 index 00000000..c12e8497 --- /dev/null +++ b/tuskar/tests/api/controllers/v1/test_models.py @@ -0,0 +1,155 @@ +# -*- encoding: utf-8 -*- +# +# 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 unittest + +from wsme import types as wtypes + +from tuskar.api.controllers.v1 import models as api_models +from tuskar.db.sqlalchemy import models as db_models + + +class BaseTests(unittest.TestCase): + + def test_lookup(self): + # Setup + class Stub(api_models.Base): + a1 = int + a2 = int + + stub = Stub(a1=1, a2=wtypes.Unset) + + # Test + self.assertEqual(1, stub._lookup('a1')) + self.assertEqual(None, stub._lookup('a2')) + + +class OvercloudModelTests(unittest.TestCase): + + def test_from_db_model(self): + # Setup + db_attrs = [ + db_models.OvercloudAttribute( + id=10, + overcloud_id=1, + key='key-1', + value='value-1', + ), + db_models.OvercloudAttribute( + id=20, + overcloud_id=1, + key='key-2', + value='value-2', + ), + ] + + db_counts = [ + db_models.OvercloudCategoryCount( + id=100, + overcloud_id=1, + resource_category_id=5, + num_nodes=5, + ) + ] + + db_model = db_models.Overcloud( + id=1, + stack_id='stack-1', + name='name-1', + description='desc-1', + attributes=db_attrs, + counts=db_counts, + ) + + # Test + api_model = api_models.Overcloud.from_db_model(db_model) + + # Verify + self.assertTrue(api_model is not None) + self.assertTrue(isinstance(api_model, api_models.Overcloud)) + + self.assertEqual(api_model.id, db_model.id) + self.assertEqual(api_model.stack_id, db_model.stack_id) + self.assertEqual(api_model.name, db_model.name) + self.assertEqual(api_model.description, db_model.description) + + self.assertEqual(len(api_model.attributes), len(db_model.attributes)) + self.assertTrue(isinstance(api_model.attributes, dict)) + for d_attr in db_model.attributes: + self.assertEqual(api_model.attributes[d_attr.key], d_attr.value) + + self.assertEqual(len(api_model.counts), len(db_model.counts)) + for a_count, d_count in zip(api_model.counts, db_model.counts): + self.assertTrue(isinstance(a_count, + api_models.OvercloudCategoryCount)) + self.assertEqual(a_count.id, d_count.id) + self.assertEqual(a_count.resource_category_id, + d_count.resource_category_id) + self.assertEqual(a_count.overcloud_id, d_count.overcloud_id) + self.assertEqual(a_count.num_nodes, d_count.num_nodes) + + def test_to_db_model(self): + # Setup + api_attrs = {'key-1': 'value-1'} + + api_counts = [ + api_models.OvercloudCategoryCount( + id=10, + resource_category_id=2, + overcloud_id=1, + num_nodes=50, + ), + api_models.OvercloudCategoryCount( + id=11, + resource_category_id=3, + overcloud_id=1, + num_nodes=15, + ), + ] + + api_model = api_models.Overcloud( + id=1, + stack_id='stack-1', + name='name-1', + description='desc-1', + attributes=api_attrs, + counts=api_counts, + ) + + # Test + db_model = api_model.to_db_model() + + # Verify + self.assertTrue(db_model is not None) + self.assertTrue(isinstance(db_model, db_models.Overcloud)) + self.assertEqual(db_model.id, api_model.id) + self.assertEqual(db_model.stack_id, api_model.stack_id) + self.assertEqual(db_model.name, api_model.name) + self.assertEqual(db_model.description, api_model.description) + + self.assertEqual(len(db_model.attributes), len(api_model.attributes)) + for d_attr in db_model.attributes: + self.assertTrue(isinstance(d_attr, db_models.OvercloudAttribute)) + self.assertEqual(d_attr.overcloud_id, api_model.id) + self.assertEqual(d_attr.value, api_attrs[d_attr.key]) + + self.assertEqual(len(db_model.counts), len(api_model.counts)) + for d_count, a_count in zip(db_model.counts, api_model.counts): + self.assertTrue(isinstance(d_count, + db_models.OvercloudCategoryCount)) + self.assertEqual(d_count.id, a_count.id) + self.assertEqual(d_count.resource_category_id, + a_count.resource_category_id) + self.assertEqual(d_count.overcloud_id, a_count.overcloud_id) + self.assertEqual(d_count.num_nodes, a_count.num_nodes) diff --git a/tuskar/tests/api/controllers/v1/test_overcloud.py b/tuskar/tests/api/controllers/v1/test_overcloud.py new file mode 100644 index 00000000..c8e6c6d0 --- /dev/null +++ b/tuskar/tests/api/controllers/v1/test_overcloud.py @@ -0,0 +1,130 @@ +# -*- encoding: utf-8 -*- +# +# 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 os + +import mock +from pecan.testing import load_test_app + +from tuskar.db.sqlalchemy import models as db_models +from tuskar.tests import base + + +URL_OVERCLOUDS = '/v1/overclouds' + + +class OvercloudTests(base.TestCase): + + def setUp(self): + super(OvercloudTests, self).setUp() + + config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), + '..', '..', '..', '..', 'api', 'config.py') + self.app = load_test_app(config_file) + + @mock.patch('tuskar.db.sqlalchemy.api.Connection.get_overclouds') + def test_get_all(self, mock_db_get): + # Setup + fake_results = [db_models.Overcloud(name='foo')] + mock_db_get.return_value = fake_results + + # Test + response = self.app.get(URL_OVERCLOUDS) + result = response.json + + # Verify + self.assertEqual(response.status_int, 200) + self.assertTrue(isinstance(result, list)) + self.assertEqual(1, len(result)) + self.assertEqual(result[0]['name'], 'foo') + + mock_db_get.assert_called_once() + + @mock.patch('tuskar.db.sqlalchemy.api.' + 'Connection.get_overcloud_by_id') + def test_get_one(self, mock_db_get): + # Setup + fake_result = db_models.Overcloud(name='foo') + mock_db_get.return_value = fake_result + + # Test + url = URL_OVERCLOUDS + '/' + '12345' + response = self.app.get(url) + result = response.json + + # Verify + self.assertEqual(response.status_int, 200) + self.assertEqual(result['name'], 'foo') + + mock_db_get.assert_called_once_with(12345) + + @mock.patch('tuskar.db.sqlalchemy.api.Connection.create_overcloud') + def test_post(self, mock_db_create): + # Setup + create_me = {'name': 'new'} + + fake_created = db_models.Overcloud(name='created') + mock_db_create.return_value = fake_created + + # Test + response = self.app.post_json(URL_OVERCLOUDS, params=create_me) + result = response.json + + # Verify + self.assertEqual(response.status_int, 201) + self.assertEqual(result['name'], fake_created.name) + + self.assertEqual(1, mock_db_create.call_count) + db_create_model = mock_db_create.call_args[0][0] + self.assertTrue(isinstance(db_create_model, + db_models.Overcloud)) + self.assertEqual(db_create_model.name, create_me['name']) + + @mock.patch('tuskar.db.sqlalchemy.api.Connection.update_overcloud') + def test_put(self, mock_db_update): + # Setup + changes = {'name': 'updated'} + + fake_updated = db_models.Overcloud(name='after-update', + attributes=[], + counts=[]) + mock_db_update.return_value = fake_updated + + # Test + url = URL_OVERCLOUDS + '/' + '12345' + response = self.app.put_json(url, params=changes) + result = response.json + + # Verify + self.assertEqual(response.status_int, 200) + self.assertEqual(result['name'], fake_updated.name) + + self.assertEqual(1, mock_db_update.call_count) + db_update_model = mock_db_update.call_args[0][0] + self.assertTrue(isinstance(db_update_model, + db_models.Overcloud)) + self.assertEqual(db_update_model.id, 12345) + self.assertEqual(db_update_model.name, changes['name']) + + @mock.patch('tuskar.db.sqlalchemy.api.' + 'Connection.delete_overcloud_by_id') + def test_delete(self, mock_db_delete): + # Test + url = URL_OVERCLOUDS + '/' + '12345' + response = self.app.delete(url) + + # Verify + self.assertEqual(response.status_int, 204) + + mock_db_delete.assert_called_once_with(12345) diff --git a/tuskar/tests/api/controllers/v1/test_resource_category.py b/tuskar/tests/api/controllers/v1/test_resource_category.py new file mode 100644 index 00000000..1dbda9ce --- /dev/null +++ b/tuskar/tests/api/controllers/v1/test_resource_category.py @@ -0,0 +1,128 @@ +# -*- encoding: utf-8 -*- +# +# 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 os + +import mock +from pecan.testing import load_test_app + +from tuskar.db.sqlalchemy import models as db_models +from tuskar.tests import base + + +URL_CATEGORIES = '/v1/resource_categories' + + +class ResourceCategoryTests(base.TestCase): + + def setUp(self): + super(ResourceCategoryTests, self).setUp() + + config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), + '..', '..', '..', '..', 'api', 'config.py') + self.app = load_test_app(config_file) + + @mock.patch('tuskar.db.sqlalchemy.api.Connection.get_resource_categories') + def test_get_all(self, mock_db_get): + # Setup + fake_results = [db_models.ResourceCategory(name='foo')] + mock_db_get.return_value = fake_results + + # Test + response = self.app.get(URL_CATEGORIES) + result = response.json + + # Verify + self.assertEqual(response.status_int, 200) + self.assertTrue(isinstance(result, list)) + self.assertEqual(1, len(result)) + self.assertEqual(result[0]['name'], 'foo') + + mock_db_get.assert_called_once() + + @mock.patch('tuskar.db.sqlalchemy.api.' + 'Connection.get_resource_category_by_id') + def test_get_one(self, mock_db_get): + # Setup + fake_result = db_models.ResourceCategory(name='foo') + mock_db_get.return_value = fake_result + + # Test + url = URL_CATEGORIES + '/' + '12345' + response = self.app.get(url) + result = response.json + + # Verify + self.assertEqual(response.status_int, 200) + self.assertEqual(result['name'], 'foo') + + mock_db_get.assert_called_once_with(12345) + + @mock.patch('tuskar.db.sqlalchemy.api.Connection.create_resource_category') + def test_post(self, mock_db_create): + # Setup + create_me = {'name': 'new'} + + fake_created = db_models.ResourceCategory(name='created') + mock_db_create.return_value = fake_created + + # Test + response = self.app.post_json(URL_CATEGORIES, params=create_me) + result = response.json + + # Verify + self.assertEqual(response.status_int, 201) + self.assertEqual(result['name'], fake_created.name) + + self.assertEqual(1, mock_db_create.call_count) + db_create_model = mock_db_create.call_args[0][0] + self.assertTrue(isinstance(db_create_model, + db_models.ResourceCategory)) + self.assertEqual(db_create_model.name, create_me['name']) + + @mock.patch('tuskar.db.sqlalchemy.api.Connection.update_resource_category') + def test_put(self, mock_db_update): + # Setup + changes = {'name': 'updated'} + + fake_updated = db_models.ResourceCategory(name='after-update') + mock_db_update.return_value = fake_updated + + # Test + url = URL_CATEGORIES + '/' + '12345' + response = self.app.put_json(url, params=changes) + result = response.json + + # Verify + self.assertEqual(response.status_int, 200) + self.assertEqual(result['name'], fake_updated.name) + + self.assertEqual(1, mock_db_update.call_count) + db_update_model = mock_db_update.call_args[0][0] + self.assertTrue(isinstance(db_update_model, + db_models.ResourceCategory)) + self.assertEqual(db_update_model.id, 12345) + self.assertEqual(db_update_model.name, changes['name']) + + @mock.patch('tuskar.db.sqlalchemy.api.' + 'Connection.delete_resource_category_by_id') + def test_delete(self, mock_db_delete): + # Test + url = URL_CATEGORIES + '/' + '12345' + response = self.app.delete(url) + + # Verify + self.assertEqual(response.status_int, 204) + + mock_db_delete.assert_called_once_with(12345) diff --git a/tuskar/tests/db/test_api.py b/tuskar/tests/db/test_api.py index e74e9702..acc849ba 100644 --- a/tuskar/tests/db/test_api.py +++ b/tuskar/tests/db/test_api.py @@ -39,7 +39,7 @@ class ResourceCategoryTests(db_base.DbTestCase): def test_save_resource_category(self): # Test - saved = self.connection.save_resource_category(self.save_me_1) + saved = self.connection.create_resource_category(self.save_me_1) # Verify self.assertTrue(saved is not None) @@ -55,7 +55,7 @@ class ResourceCategoryTests(db_base.DbTestCase): def test_save_resource_category_duplicate_name(self): # Setup - self.connection.save_resource_category(self.save_me_1) + self.connection.create_resource_category(self.save_me_1) duplicate = models.ResourceCategory( name=self.save_me_1.name, description='irrelevant', @@ -64,16 +64,19 @@ class ResourceCategoryTests(db_base.DbTestCase): # Test self.assertRaises(exception.ResourceCategoryExists, - self.connection.save_resource_category, + self.connection.create_resource_category, duplicate) def test_update(self): # Setup - saved = self.connection.save_overcloud(self.save_me_1) + saved = self.connection.create_resource_category(self.save_me_1) # Test - saved.image_id = 'abcdef' - self.connection.save_overcloud(saved) + delta = models.ResourceCategory( + id=saved.id, + image_id='abcdef' + ) + self.connection.update_resource_category(delta) # Verify found = self.connection.get_resource_category_by_id(saved.id) @@ -81,7 +84,7 @@ class ResourceCategoryTests(db_base.DbTestCase): def test_delete_category(self): # Setup - saved = self.connection.save_resource_category(self.save_me_1) + saved = self.connection.create_resource_category(self.save_me_1) # Test self.connection.delete_resource_category_by_id(saved.id) @@ -97,8 +100,8 @@ class ResourceCategoryTests(db_base.DbTestCase): def test_get_resource_categories(self): # Setup - self.connection.save_resource_category(self.save_me_1) - self.connection.save_resource_category(self.save_me_2) + self.connection.create_resource_category(self.save_me_1) + self.connection.create_resource_category(self.save_me_2) # Test all_categories = self.connection.get_resource_categories() @@ -126,8 +129,8 @@ class ResourceCategoryTests(db_base.DbTestCase): def test_get_resource_category_by_id(self): # Setup - self.connection.save_resource_category(self.save_me_1) - saved_2 = self.connection.save_resource_category(self.save_me_2) + self.connection.create_resource_category(self.save_me_1) + saved_2 = self.connection.create_resource_category(self.save_me_2) # Test found = self.connection.get_resource_category_by_id(saved_2.id) @@ -143,111 +146,6 @@ class ResourceCategoryTests(db_base.DbTestCase): 'fake-id') -class OvercloudCategoryCountTests(db_base.DbTestCase): - - def setUp(self): - super(OvercloudCategoryCountTests, self).setUp() - - self.connection = dbapi.Connection() - - # Foreign Key Relationship Data - self.overcloud_1 = models.Overcloud( - name='overcloud-1' - ) - self.connection.save_overcloud(self.overcloud_1) - - self.resource_category_1 = models.ResourceCategory( - name='resource-category-1' - ) - self.connection.save_resource_category(self.resource_category_1) - - # Sample data for tests - self.count_1 = models.OvercloudCategoryCount( - overcloud_id=self.overcloud_1.id, - resource_category_id=self.resource_category_1.id, - num_nodes=2 - ) - - def test_save_overcould_count(self): - # Test - saved = self.connection.save_overcloud_category_count(self.count_1) - - # Verify - self.assertTrue(saved is not None) - - self.assertTrue(saved.id is not None) - self.assertEqual(saved.overcloud_id, self.count_1.overcloud_id) - self.assertEqual(saved.resource_category_id, - self.count_1.resource_category_id) - self.assertEqual(saved.num_nodes, self.count_1.num_nodes) - - def test_delete_overcloud_count(self): - # Setup - saved = self.connection.save_overcloud_category_count(self.count_1) - - # Test - self.connection.delete_overcloud_category_count(saved.id) - - # Verify - found = self.connection.get_overcloud_category_counts_by_overcloud( - self.overcloud_1.id) - self.assertEqual(0, len(found)) - - def test_delete_nonexistent_overcloud_count(self): - self.assertRaises(exception.OvercloudCategoryCountNotFound, - self.connection.delete_overcloud_category_count, - 'fake-overcloud-id') - - def test_get_overcloud_counts(self): - # Setup - resource_category_2 = models.ResourceCategory( - name='resource-category-2' - ) - category_2 = \ - self.connection.save_resource_category(resource_category_2) - - self.connection.save_overcloud_category_count(self.count_1) - count_2 = models.OvercloudCategoryCount( - overcloud_id=self.count_1.overcloud_id, - resource_category_id=category_2.id, - num_nodes=5 - ) - self.connection.save_overcloud_category_count(count_2) - - # Test - all_counts =\ - self.connection.get_overcloud_category_counts_by_overcloud( - self.overcloud_1.id) - self.assertEqual(2, len(all_counts)) - - def test_get_overcloud_counts_no_results(self): - # Test - all_counts =\ - self.connection.get_overcloud_category_counts_by_overcloud( - self.overcloud_1.id) - - # Verify - self.assertTrue(isinstance(all_counts, list)) - self.assertEqual(0, len(all_counts)) - - def test_get_overcloud_count_by_id(self): - # Setup - saved = self.connection.save_overcloud_category_count(self.count_1) - - # Test - found = self.connection.get_overcloud_category_count_by_id(saved.id) - - # Verify - self.assertTrue(found is not None) - self.assertEqual(found.id, saved.id) - self.assertEqual(found.overcloud_id, saved.overcloud_id) - - def test_get_overcloud_count_by_id_no_result(self): - self.assertRaises(exception.OvercloudCategoryCountNotFound, - self.connection.get_overcloud_category_count_by_id, - 'fake-id') - - class OvercloudTests(db_base.DbTestCase): def setUp(self): @@ -265,10 +163,16 @@ class OvercloudTests(db_base.DbTestCase): value='value-2', ) + self.count_1 = models.OvercloudCategoryCount( + resource_category_id='cat-1', + num_nodes=4, + ) + self.overcloud_1 = models.Overcloud( name='overcloud-1', description='desc-1', - attributes=[self.attributes_1, self.attributes_2] + attributes=[self.attributes_1, self.attributes_2], + counts=[self.count_1] ) self.overcloud_2 = models.Overcloud( @@ -277,9 +181,9 @@ class OvercloudTests(db_base.DbTestCase): attributes=[] ) - def test_save_overcloud(self): + def test_create_overcloud(self): # Test - saved = self.connection.save_overcloud(self.overcloud_1) + saved = self.connection.create_overcloud(self.overcloud_1) # Verify self.assertTrue(saved is not None) @@ -297,9 +201,15 @@ class OvercloudTests(db_base.DbTestCase): self.assertEqual(saved.attributes[index].key, attribute.key) self.assertEqual(saved.attributes[index].value, attribute.value) - def test_save_overcloud_duplicate_name(self): + for index, count in enumerate(self.overcloud_1.counts): + self.assertEqual(saved.counts[index].resource_category_id, + count.resource_category_id) + self.assertEqual(saved.counts[index].num_nodes, + count.num_nodes) + + def test_create_overcloud_duplicate_name(self): # Setup - self.connection.save_overcloud(self.overcloud_1) + self.connection.create_overcloud(self.overcloud_1) duplicate = models.Overcloud( name=self.overcloud_1.name, description='irrelevant', @@ -307,10 +217,10 @@ class OvercloudTests(db_base.DbTestCase): # Test self.assertRaises(exception.OvercloudExists, - self.connection.save_overcloud, + self.connection.create_overcloud, duplicate) - def test_save_overcloud_duplicate_attribute(self): + def test_create_overcloud_duplicate_attribute(self): # Setup duplicate_attribute = models.OvercloudAttribute( key=self.attributes_1.key, @@ -320,12 +230,113 @@ class OvercloudTests(db_base.DbTestCase): # Test self.assertRaises(exception.DuplicateAttribute, - self.connection.save_overcloud, + self.connection.create_overcloud, self.overcloud_1) + def test_update_overcloud(self): + # Setup + saved = self.connection.create_overcloud(self.overcloud_1) + + # Test + saved.stack_id = 'new_id' + self.connection.update_overcloud(saved) + + # Verify + found = self.connection.get_overcloud_by_id(saved.id) + self.assertEqual(found.stack_id, saved.stack_id) + self.assertEqual(found.name, self.overcloud_1.name) + + def test_update_overcloud_attributes(self): + # Setup + + # Add a third attribute for enough data + self.overcloud_1.attributes.append(models.OvercloudAttribute( + key='key-3', + value='value-3', + )) + saved = self.connection.create_overcloud(self.overcloud_1) + + # Test + # - Ignore the first + saved.attributes.pop(0) + + # - Change the second + saved.attributes[0].value = 'updated-2' + + # - Delete the third + saved.attributes[1].value = None + + # - Add a fourth + saved.attributes.append(models.OvercloudAttribute( + key='key-4', + value='value-4', + )) + + self.connection.update_overcloud(saved) + + # Verify + found = self.connection.get_overcloud_by_id(saved.id) + + self.assertEqual(3, len(found.attributes)) + self.assertEqual(found.attributes[0].key, 'key-1') + self.assertEqual(found.attributes[0].value, 'value-1') + self.assertEqual(found.attributes[1].key, 'key-2') + self.assertEqual(found.attributes[1].value, 'updated-2') + self.assertEqual(found.attributes[2].key, 'key-4') + self.assertEqual(found.attributes[2].value, 'value-4') + + def test_update_overcloud_counts(self): + # Setup + + # Add extra counts for enough data + self.overcloud_1.counts.append(models.OvercloudCategoryCount( + resource_category_id='cat-2', + num_nodes=2, + )) + self.overcloud_1.counts.append(models.OvercloudCategoryCount( + resource_category_id='cat-3', + num_nodes=3, + )) + saved = self.connection.create_overcloud(self.overcloud_1) + + # Test + # - Ignore the first + saved.counts.pop(0) + + # - Change the second + saved.counts[0].num_nodes = 100 + + # - Delete the third + saved.counts[1].num_nodes = 0 + + # - Add a fourth + saved.counts.append(models.OvercloudCategoryCount( + resource_category_id='cat-4', + num_nodes=4, + )) + + self.connection.update_overcloud(self.overcloud_1) + + # Verify + found = self.connection.get_overcloud_by_id(saved.id) + + self.assertEqual(3, len(found.counts)) + self.assertEqual(found.counts[0].resource_category_id, 'cat-1') + self.assertEqual(found.counts[0].num_nodes, 4) + self.assertEqual(found.counts[1].resource_category_id, 'cat-2') + self.assertEqual(found.counts[1].num_nodes, 100) + self.assertEqual(found.counts[2].resource_category_id, 'cat-4') + self.assertEqual(found.counts[2].num_nodes, 4) + + def test_update_nonexistent(self): + fake = models.Overcloud(id='fake') + self.assertRaises(exception.OvercloudNotFound, + self.connection.update_overcloud, + fake) + def test_delete_overcloud(self): # Setup - saved = self.connection.save_overcloud(self.overcloud_1) + saved = self.connection.create_overcloud(self.overcloud_1) # Test self.connection.delete_overcloud_by_id(saved.id) @@ -344,8 +355,8 @@ class OvercloudTests(db_base.DbTestCase): # This test also verifies that the attributes are eagerly loaded # Setup - self.connection.save_overcloud(self.overcloud_1) - self.connection.save_overcloud(self.overcloud_2) + self.connection.create_overcloud(self.overcloud_1) + self.connection.create_overcloud(self.overcloud_2) # Test all_overclouds = self.connection.get_overclouds() @@ -373,8 +384,8 @@ class OvercloudTests(db_base.DbTestCase): def test_get_overcloud_by_id(self): # Setup - self.connection.save_overcloud(self.overcloud_1) - saved_2 = self.connection.save_overcloud(self.overcloud_2) + self.connection.create_overcloud(self.overcloud_1) + saved_2 = self.connection.create_overcloud(self.overcloud_2) # Test found = self.connection.get_overcloud_by_id(saved_2.id) diff --git a/tuskar/tests/drivers/__init__.py b/tuskar/tests/drivers/__init__.py deleted file mode 100644 index e69de29b..00000000