Add a baymodel object

The baymodel object and ReST object is used to control bay properties
for different bay models.  It is then possible to launch bay models
using this functionality.

Change-Id: I317bda292719ca3f4649283f7e27674c813cc3b0
This commit is contained in:
Steven Dake 2014-12-15 19:07:28 -07:00
parent e9ff12b521
commit 3f520bbde5
10 changed files with 940 additions and 27 deletions

View File

@ -28,6 +28,7 @@ import wsmeext.pecan as wsme_pecan
from magnum.api.controllers import link from magnum.api.controllers import link
from magnum.api.controllers.v1 import bay from magnum.api.controllers.v1 import bay
from magnum.api.controllers.v1 import baymodel
from magnum.api.controllers.v1 import container from magnum.api.controllers.v1 import container
from magnum.api.controllers.v1 import node from magnum.api.controllers.v1 import node
from magnum.api.controllers.v1 import pod from magnum.api.controllers.v1 import pod
@ -89,6 +90,9 @@ class V1(APIBase):
pods = [link.Link] pods = [link.Link]
"""Links to the pods resource""" """Links to the pods resource"""
baymodels = [link.Link]
"""Links to the baymodels resource"""
bays = [link.Link] bays = [link.Link]
"""Links to the bays resource""" """Links to the bays resource"""
@ -119,6 +123,13 @@ class V1(APIBase):
'pods', '', 'pods', '',
bookmark=True) bookmark=True)
] ]
v1.baymodels = [link.Link.make_link('self', pecan.request.host_url,
'baymodels', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'bays', '',
bookmark=True)
]
v1.bays = [link.Link.make_link('self', pecan.request.host_url, v1.bays = [link.Link.make_link('self', pecan.request.host_url,
'bays', ''), 'bays', ''),
link.Link.make_link('bookmark', link.Link.make_link('bookmark',
@ -147,6 +158,7 @@ class Controller(rest.RestController):
"""Version 1 API controller root.""" """Version 1 API controller root."""
bays = bay.BaysController() bays = bay.BaysController()
baymodels = baymodel.BayModelsController()
containers = container.ContainersController() containers = container.ContainersController()
nodes = node.NodesController() nodes = node.NodesController()
pods = pod.PodsController() pods = pod.PodsController()

View File

@ -0,0 +1,336 @@
# 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.
import datetime
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from magnum.api.controllers import base
from magnum.api.controllers import link
from magnum.api.controllers.v1 import collection
from magnum.api.controllers.v1 import types
from magnum.api.controllers.v1 import utils as api_utils
from magnum.common import exception
from magnum import objects
class BayModelPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/baymodel_uuid']
class BayModel(base.APIBase):
"""API representation of a baymodel.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of a baymodel.
"""
_baymodel_uuid = None
def _get_baymodel_uuid(self):
return self._baymodel_uuid
def _set_baymodel_uuid(self, value):
if value and self._baymodel_uuid != value:
try:
# FIXME(comstud): One should only allow UUID here, but
# there seems to be a bug in that tests are passing an
# ID. See bug #1301046 for more details.
baymodel = objects.BayModel.get(pecan.request.context, value)
self._baymodel_uuid = baymodel.uuid
self.baymodel_id = baymodel.id
except exception.BayModelNotFound as e:
# Change error code because 404 (NotFound) is inappropriate
# response for a POST request to create a BayModel
e.code = 400 # BadRequest
raise e
elif value == wtypes.Unset:
self._baymodel_uuid = wtypes.Unset
uuid = types.uuid
"""Unique UUID for this baymodel"""
name = wtypes.text
"""The name of the bay model"""
image_id = wtypes.text
"""The image name or UUID to use as a base image for this baymodel"""
flavor_id = wtypes.text
"""The flavor of this bay model"""
dns_nameserver = wtypes.text
"""The DNS nameserver address"""
keypair_id = wtypes.text
"""The name or id of the nova ssh keypair"""
external_network = wtypes.text
"""The external network to attach the Bay"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link and associated baymodel links"""
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.BayModel.fields)
fields.append('baymodel_uuid')
for field in fields:
# Skip fields we do not expose.
if not hasattr(self, field):
continue
self.fields.append(field)
setattr(self, field, kwargs.get(field, wtypes.Unset))
# NOTE(lucasagomes): baymodel_id is an attribute created on-the-fly
# by _set_baymodel_uuid(), it needs to be present in the fields so
# that as_dict() will contain baymodel_id field when converting it
# before saving it in the database.
self.fields.append('baymodel_id')
setattr(self, 'baymodel_uuid', kwargs.get('baymodel_id', wtypes.Unset))
@staticmethod
def _convert_with_links(baymodel, url, expand=True):
if not expand:
baymodel.unset_fields_except(['uuid', 'name', 'type', 'image_id',
'ironic_baymodel_id'])
# never expose the baymodel_id attribute
baymodel.baymodel_id = wtypes.Unset
baymodel.links = [link.Link.make_link('self', url,
'baymodels', baymodel.uuid),
link.Link.make_link('bookmark', url,
'baymodels', baymodel.uuid,
bookmark=True)
]
return baymodel
@classmethod
def convert_with_links(cls, rpc_baymodel, expand=True):
baymodel = BayModel(**rpc_baymodel.as_dict())
return cls._convert_with_links(baymodel, pecan.request.host_url,
expand)
@classmethod
def sample(cls, expand=True):
sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c',
name='example',
type='virt',
image_id='Fedora-k8s',
baymodel_count=1,
created_at=datetime.datetime.utcnow(),
updated_at=datetime.datetime.utcnow())
# NOTE(lucasagomes): baymodel_uuid getter() method look at the
# _baymodel_uuid variable
sample._baymodel_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae'
return cls._convert_with_links(sample, 'http://localhost:9511', expand)
class BayModelCollection(collection.Collection):
"""API representation of a collection of baymodels."""
baymodels = [BayModel]
"""A list containing baymodels objects"""
def __init__(self, **kwargs):
self._type = 'baymodels'
@staticmethod
def convert_with_links(rpc_baymodels, limit, url=None, expand=False,
**kwargs):
collection = BayModelCollection()
collection.baymodels = [BayModel.convert_with_links(p, expand)
for p in rpc_baymodels]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
@classmethod
def sample(cls):
sample = cls()
sample.baymodels = [BayModel.sample(expand=False)]
return sample
class BayModelsController(rest.RestController):
"""REST controller for BayModels."""
from_baymodels = False
"""A flag to indicate if the requests to this controller are coming
from the top-level resource BayModels."""
_custom_actions = {
'detail': ['GET'],
}
def _get_baymodels_collection(self, marker, limit,
sort_key, sort_dir, expand=False,
resource_url=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.BayModel.get_by_uuid(pecan.request.context,
marker)
baymodels = objects.BayModel.list(pecan.request.context, limit,
marker_obj, sort_key=sort_key,
sort_dir=sort_dir)
return BayModelCollection.convert_with_links(baymodels, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(BayModelCollection, types.uuid,
types.uuid, int, wtypes.text, wtypes.text)
def get_all(self, baymodel_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of baymodels.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
return self._get_baymodels_collection(marker, limit, sort_key,
sort_dir)
@wsme_pecan.wsexpose(BayModelCollection, types.uuid,
types.uuid, int, wtypes.text, wtypes.text)
def detail(self, baymodel_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of baymodels with detail.
:param baymodel_uuid: UUID of a baymodel, to get only baymodels for
that baymodel.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "baymodels":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['baymodels', 'detail'])
return self._get_baymodels_collection(marker, limit,
sort_key, sort_dir, expand,
resource_url)
@wsme_pecan.wsexpose(BayModel, types.uuid)
def get_one(self, baymodel_uuid):
"""Retrieve information about the given baymodel.
:param baymodel_uuid: UUID of a baymodel.
"""
if self.from_baymodels:
raise exception.OperationNotPermitted
rpc_baymodel = objects.BayModel.get_by_uuid(pecan.request.context,
baymodel_uuid)
return BayModel.convert_with_links(rpc_baymodel)
@wsme_pecan.wsexpose(BayModel, body=BayModel, status_code=201)
def post(self, baymodel):
"""Create a new baymodel.
:param baymodel: a baymodel within the request body.
"""
if self.from_baymodels:
raise exception.OperationNotPermitted
new_baymodel = objects.BayModel(pecan.request.context,
**baymodel.as_dict())
new_baymodel.create()
# Set the HTTP Location Header
pecan.response.location = link.build_url('baymodels',
new_baymodel.uuid)
return BayModel.convert_with_links(new_baymodel)
@wsme.validate(types.uuid, [BayModelPatchType])
@wsme_pecan.wsexpose(BayModel, types.uuid, body=[BayModelPatchType])
def patch(self, baymodel_uuid, patch):
"""Update an existing baymodel.
:param baymodel_uuid: UUID of a baymodel.
:param patch: a json PATCH document to apply to this baymodel.
"""
if self.from_baymodels:
raise exception.OperationNotPermitted
rpc_baymodel = objects.BayModel.get_by_uuid(pecan.request.context,
baymodel_uuid)
try:
baymodel_dict = rpc_baymodel.as_dict()
# NOTE(lucasagomes):
# 1) Remove baymodel_id because it's an internal value and
# not present in the API object
# 2) Add baymodel_uuid
baymodel_dict['baymodel_uuid'] = baymodel_dict.pop('baymodel_id',
None)
baymodel = BayModel(**api_utils.apply_jsonpatch(baymodel_dict,
patch))
except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
# Update only the fields that have changed
for field in objects.BayModel.fields:
try:
patch_val = getattr(baymodel, field)
except AttributeError:
# Ignore fields that aren't exposed in the API
continue
if patch_val == wtypes.Unset:
patch_val = None
if rpc_baymodel[field] != patch_val:
rpc_baymodel[field] = patch_val
if hasattr(pecan.request, 'rpcapi'):
rpc_baymodel = objects.BayModel.get_by_id(pecan.request.context,
rpc_baymodel.baymodel_id)
topic = pecan.request.rpcapi.get_topic_for(rpc_baymodel)
new_baymodel = pecan.request.rpcapi.update_baymodel(
pecan.request.context, rpc_baymodel, topic)
return BayModel.convert_with_links(new_baymodel)
else:
rpc_baymodel.save()
return BayModel.convert_with_links(rpc_baymodel)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, baymodel_uuid):
"""Delete a baymodel.
:param baymodel_uuid: UUID of a baymodel.
"""
if self.from_baymodels:
raise exception.OperationNotPermitted
rpc_baymodel = objects.BayModel.get_by_uuid(pecan.request.context,
baymodel_uuid)
rpc_baymodel.destroy()

View File

@ -28,6 +28,21 @@ class API(rpc_service.API):
super(API, self).__init__(transport, context, super(API, self).__init__(transport, context,
topic=cfg.CONF.conductor.topic) topic=cfg.CONF.conductor.topic)
# Bay Model Operations
def baymodel_create(self, bay):
return self._call('baymodel_create', bay=bay)
def baymodel_list(self, context, limit, marker, sort_key, sort_dir):
return objects.BayModel.list(context, limit, marker, sort_key,
sort_dir)
def baymodel_delete(self, uuid):
return self._call('baymodel_delete', uuid=uuid)
def baymodel_show(self, uuid):
return self._call('baymodel_show', uuid=uuid)
# Bay Operations # Bay Operations
def bay_create(self, bay): def bay_create(self, bay):

View File

@ -148,6 +148,113 @@ class Connection(object):
:raises: BayNotFound :raises: BayNotFound
""" """
@abc.abstractmethod
def get_baymodel_list(self, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
"""Get specific columns for matching baymodels.
Return a list of the specified columns for all baymodels that match the
specified filters.
:param columns: List of column names to return.
Defaults to 'id' column when columns == None.
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of baymodels to return.
:param marker: the last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: direction in which results should be sorted.
(asc, desc)
:returns: A list of tuples of the specified columns.
"""
@abc.abstractmethod
def reserve_baymodel(self, tag, baymodel_id):
"""Reserve a baymodel.
To prevent other ManagerServices from manipulating the given
BayModel while a Task is performed, mark it reserved by this host.
:param tag: A string uniquely identifying the reservation holder.
:param baymodel_id: A baymodel id or uuid.
:returns: A BayModel object.
:raises: BayModelNotFound if the baymodel is not found.
:raises: BayModelLocked if the baymodel is already reserved.
"""
@abc.abstractmethod
def release_baymodel(self, tag, baymodel_id):
"""Release the reservation on a baymodel.
:param tag: A string uniquely identifying the reservation holder.
:param baymodel_id: A baymodel id or uuid.
:raises: BayModelNotFound if the baymodel is not found.
:raises: BayModelLocked if the baymodel is reserved by another host.
:raises: BayModelNotLocked if the baymodel was found to not have a
reservation at all.
"""
@abc.abstractmethod
def create_baymodel(self, values):
"""Create a new baymodel.
:param values: A dict containing several items used to identify
and track the baymodel, and several dicts which are
passed into the Drivers when managing this baymodel.
For example:
::
{
'uuid': utils.generate_uuid(),
'name': 'example',
'type': 'virt'
}
:returns: A baymodel.
"""
@abc.abstractmethod
def get_baymodel_by_id(self, baymodel_id):
"""Return a baymodel.
:param baymodel_id: The id of a baymodel.
:returns: A baymodel.
"""
@abc.abstractmethod
def get_baymodel_by_uuid(self, baymodel_uuid):
"""Return a baymodel.
:param baymodel_uuid: The uuid of a baymodel.
:returns: A baymodel.
"""
@abc.abstractmethod
def get_baymodel_by_instance(self, instance):
"""Return a baymodel.
:param instance: The instance name or uuid to search for.
:returns: A baymodel.
"""
@abc.abstractmethod
def destroy_baymodel(self, baymodel_id):
"""Destroy a baymodel and all associated interfaces.
:param baymodel_id: The id or uuid of a baymodel.
"""
@abc.abstractmethod
def update_baymodel(self, baymodel_id, values):
"""Update properties of a baymodel.
:param baymodel_id: The id or uuid of a baymodel.
:returns: A baymodel.
:raises: BayModelAssociated
:raises: BayModelNotFound
"""
@abc.abstractmethod @abc.abstractmethod
def get_container_list(self, columns=None, filters=None, limit=None, def get_container_list(self, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None): marker=None, sort_key=None, sort_dir=None):

View File

@ -43,6 +43,23 @@ def upgrade():
mysql_ENGINE='InnoDB', mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8' mysql_DEFAULT_CHARSET='UTF8'
) )
op.create_table(
'baymodel',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uuid', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('type', sa.String(length=20), nullable=True),
sa.Column('image_id', sa.String(length=255), nullable=True),
sa.Column('flavor_id', sa.String(length=255), nullable=True),
sa.Column('keypair_id', sa.String(length=255), nullable=True),
sa.Column('external_network_id', sa.String(length=255), nullable=True),
sa.Column('dns_nameserver', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8'
)
op.create_table( op.create_table(
'container', 'container',
sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=True),
@ -95,6 +112,7 @@ def upgrade():
def downgrade(): def downgrade():
op.drop_table('bay') op.drop_table('bay')
op.drop_table('baymodel')
op.drop_table('container') op.drop_table('container')
op.drop_table('node') op.drop_table('node')
op.drop_table('service') op.drop_table('service')

View File

@ -292,6 +292,178 @@ class Connection(api.Connection):
ref.update(values) ref.update(values)
return ref return ref
def _add_baymodels_filters(self, query, filters):
if filters is None:
filters = []
if 'associated' in filters:
if filters['associated']:
query = query.filter(models.BayModel.instance_uuid is not None)
else:
query = query.filter(models.BayModel.instance_uuid is None)
if 'reserved' in filters:
if filters['reserved']:
query = query.filter(models.BayModel.reservation is not None)
else:
query = query.filter(models.BayModel.reservation is None)
if 'maintenance' in filters:
query = query.filter_by(maintenance=filters['maintenance'])
if 'driver' in filters:
query = query.filter_by(driver=filters['driver'])
if 'provision_state' in filters:
query = query.filter_by(provision_state=filters['provision_state'])
if 'provisioned_before' in filters:
limit = timeutils.utcnow() - datetime.timedelta(
seconds=filters['provisioned_before'])
query = query.filter(models.BayModel.provision_updated_at < limit)
return query
def get_baymodelinfo_list(self, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
# list-ify columns default values because it is bad form
# to include a mutable list in function definitions.
if columns is None:
columns = [models.BayModel.id]
else:
columns = [getattr(models.BayModel, c) for c in columns]
query = model_query(*columns, base_model=models.BayModel)
query = self._add_baymodels_filters(query, filters)
return _paginate_query(models.BayModel, limit, marker,
sort_key, sort_dir, query)
def get_baymodel_list(self, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
query = model_query(models.BayModel)
query = self._add_baymodels_filters(query, filters)
return _paginate_query(models.BayModel, limit, marker,
sort_key, sort_dir, query)
def reserve_baymodel(self, tag, baymodel_id):
session = get_session()
with session.begin():
query = model_query(models.BayModel, session=session)
query = add_identity_filter(query, baymodel_id)
# be optimistic and assume we usually create a reservation
count = query.filter_by(reservation=None).update(
{'reservation': tag}, synchronize_session=False)
try:
baymodel = query.one()
if count != 1:
# Nothing updated and baymodel exists. Must already be
# locked.
raise exception.BayModelLocked(baymodel=baymodel_id,
host=baymodel['reservation'])
return baymodel
except NoResultFound:
raise exception.BayModelNotFound(baymodel_id)
def release_baymodel(self, tag, baymodel_id):
session = get_session()
with session.begin():
query = model_query(models.BayModel, session=session)
query = add_identity_filter(query, baymodel_id)
# be optimistic and assume we usually release a reservation
count = query.filter_by(reservation=tag).update(
{'reservation': None}, synchronize_session=False)
try:
if count != 1:
baymodel = query.one()
if baymodel['reservation'] is None:
raise exception.BayModelNotLocked(baymodel=baymodel_id)
else:
raise exception.BayModelLocked(baymodel=baymodel_id,
host=baymodel['reservation'])
except NoResultFound:
raise exception.BayModelNotFound(baymodel_id)
def create_baymodel(self, values):
# ensure defaults are present for new baymodels
if not values.get('uuid'):
values['uuid'] = utils.generate_uuid()
baymodel = models.BayModel()
baymodel.update(values)
try:
baymodel.save()
except db_exc.DBDuplicateEntry as exc:
if 'instance_uuid' in exc.columns:
raise exception.InstanceAssociated(
instance_uuid=values['instance_uuid'],
baymodel=values['uuid'])
raise exception.BayModelAlreadyExists(uuid=values['uuid'])
return baymodel
def get_baymodel_by_id(self, baymodel_id):
query = model_query(models.BayModel).filter_by(id=baymodel_id)
try:
return query.one()
except NoResultFound:
raise exception.BayModelNotFound(baymodel=baymodel_id)
def get_baymodel_by_uuid(self, baymodel_uuid):
query = model_query(models.BayModel).filter_by(uuid=baymodel_uuid)
try:
return query.one()
except NoResultFound:
raise exception.BayModelNotFound(baymodel=baymodel_uuid)
def get_baymodel_by_instance(self, instance):
if not utils.is_uuid_like(instance):
raise exception.InvalidUUID(uuid=instance)
query = (model_query(models.BayModel)
.filter_by(instance_uuid=instance))
try:
result = query.one()
except NoResultFound:
raise exception.InstanceNotFound(instance=instance)
return result
def destroy_baymodel(self, baymodel_id):
session = get_session()
with session.begin():
query = model_query(models.BayModel, session=session)
query = add_identity_filter(query, baymodel_id)
query.delete()
def update_baymodel(self, baymodel_id, values):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing BayModel.")
raise exception.InvalidParameterValue(err=msg)
try:
return self._do_update_baymodel(baymodel_id, values)
except db_exc.DBDuplicateEntry:
raise exception.InstanceAssociated(
instance_uuid=values['instance_uuid'],
baymodel=baymodel_id)
def _do_update_baymodel(self, baymodel_id, values):
session = get_session()
with session.begin():
query = model_query(models.BayModel, session=session)
query = add_identity_filter(query, baymodel_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.BayModelNotFound(baymodel=baymodel_id)
# Prevent instance_uuid overwriting
if values.get("instance_uuid") and ref.instance_uuid:
raise exception.BayModelAssociated(baymodel=baymodel_id,
instance=ref.instance_uuid)
if 'provision_state' in values:
values['provision_updated_at'] = timeutils.utcnow()
ref.update(values)
return ref
def _add_containers_filters(self, query, filters): def _add_containers_filters(self, query, filters):
if filters is None: if filters is None:
filters = [] filters = []

View File

@ -127,6 +127,24 @@ class Bay(Base):
node_count = Column(Integer()) node_count = Column(Integer())
class BayModel(Base):
"""Represents a bay model."""
__tablename__ = 'baymodel'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_baymodel0uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(255))
image_id = Column(String(255))
flavor_id = Column(String(255))
keypair_id = Column(String(255))
external_network_id = Column(String(255))
dns_nameserver = Column(String(255))
class Container(Base): class Container(Base):
"""Represents a container.""" """Represents a container."""

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
from magnum.objects import bay from magnum.objects import bay
from magnum.objects import baymodel
from magnum.objects import container from magnum.objects import container
from magnum.objects import node from magnum.objects import node
from magnum.objects import pod from magnum.objects import pod
@ -21,11 +22,13 @@ from magnum.objects import service
Container = container.Container Container = container.Container
Bay = bay.Bay Bay = bay.Bay
BayModel = baymodel.BayModel
Node = node.Node Node = node.Node
Pod = pod.Pod Pod = pod.Pod
Service = service.Service Service = service.Service
__all__ = (Bay, __all__ = (Bay,
BayModel,
Container, Container,
Node, Node,
Pod, Pod,

184
magnum/objects/baymodel.py Normal file
View File

@ -0,0 +1,184 @@
# coding=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.
from magnum.common import exception
from magnum.common import utils
from magnum.db import api as dbapi
from magnum.objects import base
from magnum.objects import utils as obj_utils
class BayModel(base.MagnumObject):
# Version 1.0: Initial version
# Version 1.1: Add get() and get_by_id() and get_by_address() and
# make get_by_uuid() only work with a uuid
# Version 1.2: Add create() and destroy()
# Version 1.3: Add list()
# Version 1.4: Add list_by_node_id()
VERSION = '1.0'
dbapi = dbapi.get_instance()
fields = {
'id': int,
'uuid': obj_utils.str_or_none,
'name': obj_utils.str_or_none,
'image_id': obj_utils.str_or_none,
'flavor_id': obj_utils.str_or_none,
'keypair_id': obj_utils.str_or_none,
'dns_nameserver': obj_utils.str_or_none,
'external_network_id': obj_utils.str_or_none
}
@staticmethod
def _from_db_object(baymodel, db_baymodel):
"""Converts a database entity to a formal object."""
for field in baymodel.fields:
baymodel[field] = db_baymodel[field]
baymodel.obj_reset_changes()
return baymodel
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [BayModel._from_db_object(cls(context), obj) for obj in
db_objects]
@base.remotable_classmethod
def get(cls, context, baymodel_id):
"""Find a baymodel based on its id or uuid and return a BayModel object.
:param baymodel_id: the id *or* uuid of a baymodel.
:returns: a :class:`BayModel` object.
"""
if utils.is_int_like(baymodel_id):
return cls.get_by_id(context, baymodel_id)
elif utils.is_uuid_like(baymodel_id):
return cls.get_by_uuid(context, baymodel_id)
else:
raise exception.InvalidIdentity(identity=baymodel_id)
@base.remotable_classmethod
def get_by_id(cls, context, baymodel_id):
"""Find a baymodel based on its integer id and return a BayModel object.
:param baymodel_id: the id of a baymodel.
:returns: a :class:`BayModel` object.
"""
db_baymodel = cls.dbapi.get_baymodel_by_id(baymodel_id)
baymodel = BayModel._from_db_object(cls(context), db_baymodel)
return baymodel
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find a baymodel based on uuid and return a :class:`BayModel` object.
:param uuid: the uuid of a baymodel.
:param context: Security context
:returns: a :class:`BayModel` object.
"""
db_baymodel = cls.dbapi.get_baymodel_by_uuid(uuid)
baymodel = BayModel._from_db_object(cls(context), db_baymodel)
return baymodel
@base.remotable_classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""Return a list of BayModel objects.
:param context: Security context.
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:returns: a list of :class:`BayModel` object.
"""
db_baymodels = cls.dbapi.get_baymodel_list(limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
return BayModel._from_db_object_list(db_baymodels, cls, context)
@base.remotable
def create(self, context=None):
"""Create a BayModel record in the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: BayModel(context)
"""
values = self.obj_get_changes()
db_baymodel = self.dbapi.create_baymodel(values)
self._from_db_object(self, db_baymodel)
@base.remotable
def destroy(self, context=None):
"""Delete the BayModel from the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: BayModel(context)
"""
self.dbapi.destroy_baymodel(self.uuid)
self.obj_reset_changes()
@base.remotable
def save(self, context=None):
"""Save updates to this BayModel.
Updates will be made column by column based on the result
of self.what_changed().
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: BayModel(context)
"""
updates = self.obj_get_changes()
self.dbapi.update_baymodel(self.uuid, updates)
self.obj_reset_changes()
@base.remotable
def refresh(self, context=None):
"""Loads updates for this BayModel.
Loads a baymodel with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded baymodel column by column, if there are any updates.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: BayModel(context)
"""
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
if (hasattr(self, base.get_attrname(field)) and
self[field] != current[field]):
self[field] = current[field]

View File

@ -34,34 +34,36 @@ class TestRootController(tests.FunctionalTest):
self.assertEqual(expected, response.json) self.assertEqual(expected, response.json)
def test_v1_controller(self): def test_v1_controller(self):
api_spec_url = (u'http://docs.openstack.org/developer'
u'/magnum/dev/api-spec-v1.html')
expected = {u'media_types': expected = {u'media_types':
[{u'base': u'application/json', [{u'base': u'application/json',
u'type': u'application/vnd.openstack.magnum.v1+json'}], u'type': u'application/vnd.openstack.magnum.v1+json'}],
u'links': [{u'href': u'http://localhost/v1/', u'links': [{u'href': u'http://localhost/v1/',
u'rel': u'self'}, u'rel': u'self'},
{u'href': api_spec_url, {u'href':
u'type': u'text/html', u'http://docs.openstack.org/developer'
u'rel': u'describedby'}], '/magnum/dev/api-spec-v1.html',
u'bays': [{u'href': u'http://localhost/v1/bays/', u'type': u'text/html', u'rel': u'describedby'}],
u'rel': u'self'}, u'bays': [{u'href': u'http://localhost/v1/bays/',
{u'href': u'http://localhost/bays/', u'rel': u'self'},
u'rel': u'bookmark'}], {u'href': u'http://localhost/bays/',
u'services': [{u'href': u'http://localhost/v1/services/', u'rel': u'bookmark'}],
u'rel': u'self'}, u'services': [{u'href': u'http://localhost/v1/services/',
{u'href': u'http://localhost/services/', u'rel': u'self'},
u'rel': u'bookmark'}], {u'href': u'http://localhost/services/',
u'pods': [{u'href': u'http://localhost/v1/pods/', u'rel': u'bookmark'}],
u'rel': u'self'}, u'baymodels': [{u'href': u'http://localhost/v1/baymodels/',
{u'href': u'http://localhost/pods/', u'rel': u'self'},
u'rel': u'bookmark'}], {u'href': u'http://localhost/bays/',
u'id': u'v1', u'rel': u'bookmark'}],
u'containers': [{u'href': u'pods': [{u'href': u'http://localhost/v1/pods/',
u'http://localhost/v1/containers/', u'rel': u'self'},
u'rel': u'self'}, {u'href': u'http://localhost/pods/',
{u'href': u'http://localhost/containers/', u'rel': u'bookmark'}],
u'rel': u'bookmark'}]} u'id': u'v1',
u'containers': [{u'href': u'http://localhost/v1/containers/',
u'rel': u'self'},
{u'href': u'http://localhost/containers/',
u'rel': u'bookmark'}]}
response = self.app.get('/v1/') response = self.app.get('/v1/')
self.assertEqual(expected, response.json) self.assertEqual(expected, response.json)
@ -120,6 +122,52 @@ class TestBayController(db_base.DbTestCase):
self.assertEqual(0, len(c)) self.assertEqual(0, len(c))
class TestBayModelController(db_base.DbTestCase):
def simulate_rpc_baymodel_create(self, baymodel):
baymodel.create()
return baymodel
def test_bay_model_api(self):
with patch.object(api.API, 'baymodel_create') as mock_method:
# Create a bay_model
mock_method.side_effect = self.simulate_rpc_baymodel_create
params = '{"name": "bay_model_example_A", "image_id": "nerdherd"}'
response = self.app.post('/v1/baymodels',
params=params,
content_type='application/json')
self.assertEqual(response.status_int, 201)
# Get all baymodels
response = self.app.get('/v1/baymodels')
self.assertEqual(response.status_int, 200)
self.assertEqual(1, len(response.json))
c = response.json['baymodels'][0]
self.assertIsNotNone(c.get('uuid'))
self.assertEqual('bay_model_example_A', c.get('name'))
self.assertEqual('nerdherd', c.get('image_id'))
# Get just the one we created
response = self.app.get('/v1/baymodels/%s' % c.get('uuid'))
self.assertEqual(response.status_int, 200)
# Update the description
params = [{'path': '/name',
'value': 'bay_model_example_B',
'op': 'replace'}]
response = self.app.patch_json('/v1/baymodels/%s' % c.get('uuid'),
params=params)
self.assertEqual(response.status_int, 200)
# Delete the bay_model we created
response = self.app.delete('/v1/baymodels/%s' % c.get('uuid'))
self.assertEqual(response.status_int, 204)
response = self.app.get('/v1/baymodels')
self.assertEqual(response.status_int, 200)
c = response.json['baymodels']
self.assertEqual(0, len(c))
class TestNodeController(db_base.DbTestCase): class TestNodeController(db_base.DbTestCase):
def test_node_api(self): def test_node_api(self):
# Create a node # Create a node