API controllers for icehouse domain model

Change-Id: I2cf5c5fe67cc4c5befb53f8323ee4b32edc3d520
This commit is contained in:
Jay Dobies 2014-01-23 13:13:37 -05:00
parent 67a6b6d3ff
commit 276c5a3707
36 changed files with 1167 additions and 1417 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -1,14 +1,10 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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):

View File

@ -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)

View File

@ -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):

View File

@ -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"))

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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"))

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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()))

View File

@ -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)

View File

@ -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)

View File

@ -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]

View File

@ -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))

View File

@ -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]

View File

@ -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()))

View File

@ -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):

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)