Implement FPGA data model and corresponding API

This patch implements the Deployables data model and provides corresponding
CRUD REST/RPC API.

Since the CREATE/UPDATE/DELETE method of Deployables should only be used
by internal component, currently keep these in code is just for convenient
test purpose, which will be removed later.

blueprint cyborg-fpga-modelling

Change-Id: I8a028954bd27d015c0b062f730673ea39701774d
This commit is contained in:
zhuli 2018-02-01 21:13:24 +08:00
parent b22761ab0d
commit aee55527a9
14 changed files with 616 additions and 21 deletions

View File

@ -22,6 +22,7 @@ from wsme import types as wtypes
from cyborg.api.controllers import base
from cyborg.api.controllers import link
from cyborg.api.controllers.v1 import accelerators
from cyborg.api.controllers.v1 import deployables
from cyborg.api import expose
@ -51,6 +52,7 @@ class Controller(rest.RestController):
"""Version 1 API controller root"""
accelerators = accelerators.AcceleratorsController()
deployables = deployables.DeployablesController()
@expose.expose(V1)
def get(self):

View File

@ -0,0 +1,205 @@
# Copyright 2018 Huawei Technologies Co.,LTD.
# 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 pecan
from pecan import rest
from six.moves import http_client
import wsme
from wsme import types as wtypes
from cyborg.api.controllers import base
from cyborg.api.controllers import link
from cyborg.api.controllers.v1 import types
from cyborg.api.controllers.v1 import utils as api_utils
from cyborg.api import expose
from cyborg.common import exception
from cyborg.common import policy
from cyborg import objects
class Deployable(base.APIBase):
"""API representation of a deployable.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
a deployable.
"""
uuid = types.uuid
"""The UUID of the deployable"""
name = wtypes.text
"""The name of the deployable"""
parent_uuid = types.uuid
"""The parent UUID of the deployable"""
root_uuid = types.uuid
"""The root UUID of the deployable"""
pcie_address = wtypes.text
"""The pcie address of the deployable"""
host = wtypes.text
"""The host on which the deployable is located"""
board = wtypes.text
"""The board of the deployable"""
vendor = wtypes.text
"""The vendor of the deployable"""
version = wtypes.text
"""The version of the deployable"""
type = wtypes.text
"""The type of the deployable"""
assignable = types.boolean
"""Whether the deployable is assignable"""
instance_uuid = types.uuid
"""The UUID of the instance which deployable is assigned to"""
availability = wtypes.text
"""The availability of the deployable"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link"""
def __init__(self, **kwargs):
super(Deployable, self).__init__(**kwargs)
self.fields = []
for field in objects.Deployable.fields:
self.fields.append(field)
setattr(self, field, kwargs.get(field, wtypes.Unset))
@classmethod
def convert_with_links(cls, obj_dep):
api_dep = cls(**obj_dep.as_dict())
url = pecan.request.public_url
api_dep.links = [
link.Link.make_link('self', url, 'deployables', api_dep.uuid),
link.Link.make_link('bookmark', url, 'deployables', api_dep.uuid,
bookmark=True)
]
return api_dep
class DeployableCollection(base.APIBase):
"""API representation of a collection of deployables."""
deployables = [Deployable]
"""A list containing deployable objects"""
@classmethod
def convert_with_links(cls, obj_deps):
collection = cls()
collection.deployables = [Deployable.convert_with_links(obj_dep)
for obj_dep in obj_deps]
return collection
class DeployablePatchType(types.JsonPatchType):
_api_base = Deployable
@staticmethod
def internal_attrs():
defaults = types.JsonPatchType.internal_attrs()
return defaults + ['/pcie_address', '/host', '/type']
class DeployablesController(rest.RestController):
"""REST controller for Deployables."""
@policy.authorize_wsgi("cyborg:deployable", "create", False)
@expose.expose(Deployable, body=types.jsontype,
status_code=http_client.CREATED)
def post(self, dep):
"""Create a new deployable.
:param dep: a deployable within the request body.
"""
context = pecan.request.context
obj_dep = objects.Deployable(context, **dep)
new_dep = pecan.request.conductor_api.deployable_create(context,
obj_dep)
# Set the HTTP Location Header
pecan.response.location = link.build_url('deployables', new_dep.uuid)
return Deployable.convert_with_links(new_dep)
@policy.authorize_wsgi("cyborg:deployable", "get_one")
@expose.expose(Deployable, types.uuid)
def get_one(self, uuid):
"""Retrieve information about the given deployable.
:param uuid: UUID of a deployable.
"""
obj_dep = objects.Deployable.get(pecan.request.context, uuid)
return Deployable.convert_with_links(obj_dep)
@policy.authorize_wsgi("cyborg:deployable", "get_all")
@expose.expose(DeployableCollection, int, types.uuid, wtypes.text,
wtypes.text, types.boolean)
def get_all(self):
"""Retrieve a list of deployables."""
obj_deps = objects.Deployable.list(pecan.request.context)
return DeployableCollection.convert_with_links(obj_deps)
@policy.authorize_wsgi("cyborg:deployable", "update")
@expose.expose(Deployable, types.uuid, body=[DeployablePatchType])
def patch(self, uuid, patch):
"""Update a deployable.
:param uuid: UUID of a deployable.
:param patch: a json PATCH document to apply to this deployable.
"""
context = pecan.request.context
obj_dep = objects.Deployable.get(context, uuid)
try:
api_dep = Deployable(
**api_utils.apply_jsonpatch(obj_dep.as_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.Deployable.fields:
try:
patch_val = getattr(api_dep, field)
except AttributeError:
# Ignore fields that aren't exposed in the API
continue
if patch_val == wtypes.Unset:
patch_val = None
if obj_dep[field] != patch_val:
obj_dep[field] = patch_val
new_dep = pecan.request.conductor_api.deployable_update(context,
obj_dep)
return Deployable.convert_with_links(new_dep)
@policy.authorize_wsgi("cyborg:deployable", "delete")
@expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
def delete(self, uuid):
"""Delete a deployable.
:param uuid: UUID of a deployable.
"""
context = pecan.request.context
obj_dep = objects.Deployable.get(context, uuid)
pecan.request.conductor_api.deployable_delete(context, obj_dep)

View File

@ -94,6 +94,10 @@ class AcceleratorAlreadyExists(CyborgException):
_msg_fmt = _("Accelerator with uuid %(uuid)s already exists.")
class DeployableAlreadyExists(CyborgException):
_msg_fmt = _("Deployable with uuid %(uuid)s already exists.")
class Invalid(CyborgException):
_msg_fmt = _("Invalid parameters.")
code = http_client.BAD_REQUEST
@ -139,15 +143,23 @@ class AcceleratorNotFound(NotFound):
_msg_fmt = _("Accelerator %(uuid)s could not be found.")
class DeployableNotFound(NotFound):
_msg_fmt = _("Deployable %(uuid)s could not be found.")
class Conflict(CyborgException):
_msg_fmt = _('Conflict.')
code = http_client.CONFLICT
class DuplicateName(Conflict):
class DuplicateAcceleratorName(Conflict):
_msg_fmt = _("An accelerator with name %(name)s already exists.")
class DuplicateDeployableName(Conflict):
_msg_fmt = _("A deployable with name %(name)s already exists.")
class PlacementEndpointNotFound(NotFound):
message = _("Placement API endpoint not found")
@ -164,3 +176,7 @@ class PlacementInventoryNotFound(NotFound):
class PlacementInventoryUpdateConflict(Conflict):
message = _("Placement inventory update conflict for resource provider "
"%(resource_provider)s, resource class %(resource_class)s.")
class ObjectActionError(CyborgException):
_msg_fmt = _('Object action %(action)s failed because: %(reason)s')

View File

@ -88,9 +88,27 @@ accelerator_policies = [
description='Update accelerator records'),
]
deployable_policies = [
policy.RuleDefault('cyborg:deployable:get_one',
'rule:allow',
description='Show deployable detail'),
policy.RuleDefault('cyborg:deployable:get_all',
'rule:allow',
description='Retrieve all deployable records'),
policy.RuleDefault('cyborg:deployable:create',
'rule:admin_api',
description='Create deployable records'),
policy.RuleDefault('cyborg:deployable:delete',
'rule:admin_api',
description='Delete deployable records'),
policy.RuleDefault('cyborg:deployable:update',
'rule:admin_api',
description='Update deployable records'),
]
def list_policies():
return default_policies + accelerator_policies
return default_policies + accelerator_policies + deployable_policies
@lockutils.synchronized('policy_enforcer', 'cyborg-')

View File

@ -65,3 +65,57 @@ class ConductorManager(object):
:param obj_acc: an accelerator object to delete.
"""
obj_acc.destroy(context)
def deployable_create(self, context, obj_dep):
"""Create a new deployable.
:param context: request context.
:param obj_dep: a changed (but not saved) obj_dep object.
:returns: created obj_dep object.
"""
obj_dep.create(context)
return obj_dep
def deployable_update(self, context, obj_dep):
"""Update a deployable.
:param context: request context.
:param obj_dep: a deployable object to update.
:returns: updated deployable object.
"""
obj_dep.save(context)
return obj_dep
def deployable_delete(self, context, obj_dep):
"""Delete a deployable.
:param context: request context.
:param obj_dep: a deployable object to delete.
"""
obj_dep.destroy(context)
def deployable_get(self, context, uuid):
"""Retrieve a deployable.
:param context: request context.
:param uuid: UUID of a deployable.
:returns: requested deployable object.
"""
return objects.Deployable.get(context, uuid)
def deployable_get_by_host(self, context, host):
"""Retrieve a deployable.
:param context: request context.
:param host: host on which the deployable is located.
:returns: requested deployable object.
"""
return objects.Deployable.get_by_host(context, host)
def deployable_list(self, context):
"""Retrieve a list of deployables.
:param context: request context.
:returns: a list of deployable objects.
"""
return objects.Deployable.list(context)

View File

@ -75,3 +75,61 @@ class ConductorAPI(object):
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
cctxt.call(context, 'accelerator_delete', obj_acc=obj_acc)
def deployable_create(self, context, obj_dep):
"""Signal to conductor service to create a deployable.
:param context: request context.
:param obj_dep: a created (but not saved) deployable object.
:returns: created deployable object.
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'deployable_create', obj_dep=obj_dep)
def deployable_update(self, context, obj_dep):
"""Signal to conductor service to update a deployable.
:param context: request context.
:param obj_dep: a deployable object to update.
:returns: updated deployable object.
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'deployable_update', obj_dep=obj_dep)
def deployable_delete(self, context, obj_dep):
"""Signal to conductor service to delete a deployable.
:param context: request context.
:param obj_dep: a deployable object to delete.
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
cctxt.call(context, 'deployable_delete', obj_dep=obj_dep)
def deployable_get(self, context, uuid):
"""Signal to conductor service to get a deployable.
:param context: request context.
:param uuid: UUID of a deployable.
:returns: requested deployable object.
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'deployable_get', uuid=uuid)
def deployable_get_by_host(self, context, host):
"""Signal to conductor service to get a deployable by host.
:param context: request context.
:param host: host on which the deployable is located.
:returns: requested deployable object.
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'deployable_get_by_host', host=host)
def deployable_list(self, context):
"""Signal to conductor service to get a list of deployables.
:param context: request context.
:returns: a list of deployable objects.
"""
cctxt = self.client.prepare(topic=self.topic, server=CONF.host)
return cctxt.call(context, 'deployable_list')

View File

@ -60,5 +60,30 @@ class Connection(object):
"""Update an accelerator."""
@abc.abstractmethod
def accelerator_destroy(self, context, uuid):
def accelerator_delete(self, context, uuid):
"""Delete an accelerator."""
# deployable
@abc.abstractmethod
def deployable_create(self, context, values):
"""Create a new deployable."""
@abc.abstractmethod
def deployable_get(self, context, uuid):
"""Get requested deployable."""
@abc.abstractmethod
def deployable_get_by_host(self, context, host):
"""Get requested deployable by host."""
@abc.abstractmethod
def deployable_list(self, context):
"""Get requested list of deployables."""
@abc.abstractmethod
def deployable_update(self, context, uuid, values):
"""Update a deployable."""
@abc.abstractmethod
def deployable_delete(self, context, uuid):
"""Delete a deployable."""

View File

@ -46,6 +46,35 @@ def upgrade():
sa.Column('product_id', sa.Text(), nullable=False),
sa.Column('remotable', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_accelerators0uuid'),
mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8'
)
op.create_table(
'deployables',
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=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('parent_uuid', sa.String(length=36),
sa.ForeignKey('deployables.uuid'), nullable=True),
sa.Column('root_uuid', sa.String(length=36),
sa.ForeignKey('deployables.uuid'), nullable=True),
sa.Column('pcie_address', sa.Text(), nullable=False),
sa.Column('host', sa.Text(), nullable=False),
sa.Column('board', sa.Text(), nullable=False),
sa.Column('vendor', sa.Text(), nullable=False),
sa.Column('version', sa.Text(), nullable=False),
sa.Column('type', sa.Text(), nullable=False),
sa.Column('assignable', sa.Boolean(), nullable=False),
sa.Column('instance_uuid', sa.String(length=36), nullable=True),
sa.Column('availability', sa.Text(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uuid', name='uniq_deployables0uuid'),
sa.Index('deployables_parent_uuid_idx', 'parent_uuid'),
sa.Index('deployables_root_uuid_idx', 'root_uuid'),
mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8'
)

View File

@ -153,7 +153,7 @@ class Connection(api.Connection):
return self._do_update_accelerator(context, uuid, values)
except db_exc.DBDuplicateEntry as e:
if 'name' in e.columns:
raise exception.DuplicateName(name=values['name'])
raise exception.DuplicateAcceleratorName(name=values['name'])
@oslo_db_api.retry_on_deadlock
def _do_update_accelerator(self, context, uuid, values):
@ -169,10 +169,78 @@ class Connection(api.Connection):
return ref
@oslo_db_api.retry_on_deadlock
def accelerator_destroy(self, context, uuid):
def accelerator_delete(self, context, uuid):
with _session_for_write():
query = model_query(context, models.Accelerator)
query = add_identity_filter(query, uuid)
count = query.delete()
if count != 1:
raise exception.AcceleratorNotFound(uuid=uuid)
def deployable_create(self, context, values):
if not values.get('uuid'):
values['uuid'] = uuidutils.generate_uuid()
deployable = models.Deployable()
deployable.update(values)
with _session_for_write() as session:
try:
session.add(deployable)
session.flush()
except db_exc.DBDuplicateEntry:
raise exception.DeployableAlreadyExists(uuid=values['uuid'])
return deployable
def deployable_get(self, context, uuid):
query = model_query(
context,
models.Deployable).filter_by(uuid=uuid)
try:
return query.one()
except NoResultFound:
raise exception.DeployableNotFound(uuid=uuid)
def deployable_get_by_host(self, context, host):
query = model_query(
context,
models.Deployable).filter_by(host=host)
return query.all()
def deployable_list(self, context):
query = model_query(context, models.Deployable)
return query.all()
def deployable_update(self, context, uuid, values):
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Deployable.")
raise exception.InvalidParameterValue(err=msg)
try:
return self._do_update_deployable(context, uuid, values)
except db_exc.DBDuplicateEntry as e:
if 'name' in e.columns:
raise exception.DuplicateDeployableName(name=values['name'])
@oslo_db_api.retry_on_deadlock
def _do_update_deployable(self, context, uuid, values):
with _session_for_write():
query = model_query(context, models.Deployable)
query = add_identity_filter(query, uuid)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.DeployableNotFound(uuid=uuid)
ref.update(values)
return ref
@oslo_db_api.retry_on_deadlock
def deployable_delete(self, context, uuid):
with _session_for_write():
query = model_query(context, models.Deployable)
query = add_identity_filter(query, uuid)
query.update({'root_uuid': None})
count = query.delete()
if count != 1:
raise exception.DeployableNotFound(uuid=uuid)

View File

@ -19,7 +19,7 @@ from oslo_db import options as db_options
from oslo_db.sqlalchemy import models
import six.moves.urllib.parse as urlparse
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, String, Integer
from sqlalchemy import Column, String, Integer, Boolean, ForeignKey, Index
from sqlalchemy import schema
from cyborg.common import paths
@ -72,3 +72,32 @@ class Accelerator(Base):
vendor_id = Column(String(255), nullable=False)
product_id = Column(String(255), nullable=False)
remotable = Column(Integer, nullable=False)
class Deployable(Base):
"""Represents the deployables."""
__tablename__ = 'deployables'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_deployables0uuid'),
Index('deployables_parent_uuid_idx', 'parent_uuid'),
Index('deployables_root_uuid_idx', 'root_uuid'),
table_args()
)
id = Column(Integer, primary_key=True)
uuid = Column(String(36), nullable=False)
name = Column(String(255), nullable=False)
parent_uuid = Column(String(36),
ForeignKey('deployables.uuid'), nullable=True)
root_uuid = Column(String(36),
ForeignKey('deployables.uuid'), nullable=True)
pcie_address = Column(String(255), nullable=False)
host = Column(String(255), nullable=False)
board = Column(String(255), nullable=False)
vendor = Column(String(255), nullable=False)
version = Column(String(255), nullable=False)
type = Column(String(255), nullable=False)
assignable = Column(Boolean, nullable=False)
instance_uuid = Column(String(36), nullable=True)
availability = Column(String(255), nullable=False)

View File

@ -26,3 +26,4 @@ def register_all():
# function in order for it to be registered by services that may
# need to receive it via RPC.
__import__('cyborg.objects.accelerator')
__import__('cyborg.objects.deployable')

View File

@ -73,5 +73,5 @@ class Accelerator(base.CyborgObject, object_base.VersionedObjectDictCompat):
def destroy(self, context):
"""Delete the Accelerator from the DB."""
self.dbapi.accelerator_destroy(context, self.uuid)
self.dbapi.accelerator_delete(context, self.uuid)
self.obj_reset_changes()

View File

@ -0,0 +1,98 @@
# Copyright 2018 Huawei Technologies Co.,LTD.
# 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.
from oslo_log import log as logging
from oslo_versionedobjects import base as object_base
from cyborg.common import exception
from cyborg.db import api as dbapi
from cyborg.objects import base
from cyborg.objects import fields as object_fields
LOG = logging.getLogger(__name__)
@base.CyborgObjectRegistry.register
class Deployable(base.CyborgObject, object_base.VersionedObjectDictCompat):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = dbapi.get_instance()
fields = {
'uuid': object_fields.UUIDField(nullable=False),
'name': object_fields.StringField(nullable=False),
'parent_uuid': object_fields.UUIDField(nullable=True),
'root_uuid': object_fields.UUIDField(nullable=True),
'pcie_address': object_fields.StringField(nullable=False),
'host': object_fields.StringField(nullable=False),
'board': object_fields.StringField(nullable=False),
'vendor': object_fields.StringField(nullable=False),
'version': object_fields.StringField(nullable=False),
'type': object_fields.StringField(nullable=False),
'assignable': object_fields.BooleanField(nullable=False),
'instance_uuid': object_fields.UUIDField(nullable=True),
'availability': object_fields.StringField(nullable=False),
}
def _get_parent_root_uuid(self):
obj_dep = Deployable.get(None, self.parent_uuid)
return obj_dep.root_uuid
def create(self, context):
"""Create a Deployable record in the DB."""
if 'uuid' not in self:
raise exception.ObjectActionError(action='create',
reason='uuid is required')
if self.parent_uuid is None:
self.root_uuid = self.uuid
else:
self.root_uuid = self._get_parent_root_uuid()
values = self.obj_get_changes()
db_dep = self.dbapi.deployable_create(context, values)
self._from_db_object(self, db_dep)
@classmethod
def get(cls, context, uuid):
"""Find a DB Deployable and return an Obj Deployable."""
db_dep = cls.dbapi.deployable_get(context, uuid)
obj_dep = cls._from_db_object(cls(context), db_dep)
return obj_dep
@classmethod
def get_by_host(cls, context, host):
"""Get a Deployable by host."""
db_deps = cls.dbapi.deployable_get_by_host(context, host)
return cls._from_db_object_list(db_deps, context)
@classmethod
def list(cls, context):
"""Return a list of Deployable objects."""
db_deps = cls.dbapi.deployable_list(context)
return cls._from_db_object_list(db_deps, context)
def save(self, context):
"""Update a Deployable record in the DB."""
updates = self.obj_get_changes()
db_dep = self.dbapi.deployable_update(context, self.uuid, updates)
self._from_db_object(self, db_dep)
def destroy(self, context):
"""Delete a Deployable from the DB."""
self.dbapi.deployable_delete(context, self.uuid)
self.obj_reset_changes()

View File

@ -16,17 +16,9 @@
from oslo_versionedobjects import fields as object_fields
class IntegerField(object_fields.IntegerField):
pass
class UUIDField(object_fields.UUIDField):
pass
class StringField(object_fields.StringField):
pass
class DateTimeField(object_fields.DateTimeField):
pass
# Import fields from oslo_versionedobjects
IntegerField = object_fields.IntegerField
UUIDField = object_fields.UUIDField
StringField = object_fields.StringField
DateTimeField = object_fields.DateTimeField
BooleanField = object_fields.BooleanField