API controllers for icehouse domain model
Change-Id: I2cf5c5fe67cc4c5befb53f8323ee4b32edc3d520
This commit is contained in:
parent
67a6b6d3ff
commit
276c5a3707
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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):
|
||||
|
@ -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)
|
@ -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):
|
||||
|
@ -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"))
|
@ -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)
|
142
tuskar/api/controllers/v1/models.py
Normal file
142
tuskar/api/controllers/v1/models.py
Normal 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
|
@ -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)
|
@ -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
|
||||
|
@ -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
|
144
tuskar/api/controllers/v1/resource_category.py
Normal file
144
tuskar/api/controllers/v1/resource_category.py
Normal 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
|
@ -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)
|
@ -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)
|
@ -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"))
|
@ -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
|
@ -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]
|
@ -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
|
@ -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()))
|
@ -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)
|
@ -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)
|
@ -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]
|
@ -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))
|
@ -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]
|
@ -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()))
|
@ -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):
|
||||
|
@ -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.
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
155
tuskar/tests/api/controllers/v1/test_models.py
Normal file
155
tuskar/tests/api/controllers/v1/test_models.py
Normal 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)
|
130
tuskar/tests/api/controllers/v1/test_overcloud.py
Normal file
130
tuskar/tests/api/controllers/v1/test_overcloud.py
Normal 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)
|
128
tuskar/tests/api/controllers/v1/test_resource_category.py
Normal file
128
tuskar/tests/api/controllers/v1/test_resource_category.py
Normal 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)
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user