Cloud Services development

Starting to manage cloud services for the board.

Change-Id: I09cb167ec356ea08101bc3d4621f7fa73a18ec1c
This commit is contained in:
Fabio Verboso 2018-02-12 12:15:09 +01:00
parent 88d5be7930
commit 6e9e02e9c3
13 changed files with 895 additions and 21 deletions

View File

@ -26,6 +26,7 @@ from wsme import types as wtypes
from iotronic.api.controllers import base
from iotronic.api.controllers import link
from iotronic.api.controllers.v1 import plugin
from iotronic.api.controllers.v1 import service
# from iotronic.api.controllers.v1 import driver
# from iotronic.api.controllers.v1 import port
# from iotronic.api.controllers.v1 import portgroup
@ -60,6 +61,12 @@ class V1(base.APIBase):
boards = [link.Link]
"""Links to the boards resource"""
plugins = [link.Link]
"""Links to the boards resource"""
services = [link.Link]
"""Links to the boards resource"""
@staticmethod
def convert():
v1 = V1()
@ -89,6 +96,14 @@ class V1(base.APIBase):
bookmark=True)
]
v1.services = [link.Link.make_link('self', pecan.request.public_url,
'services', ''),
link.Link.make_link('bookmark',
pecan.request.public_url,
'services', '',
bookmark=True)
]
return v1
@ -97,6 +112,7 @@ class Controller(rest.RestController):
boards = board.BoardsController()
plugins = plugin.PluginsController()
services = service.ServicesController()
@expose.expose(V1)
def get(self):

View File

@ -0,0 +1,362 @@
# 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 iotronic.api.controllers import base
from iotronic.api.controllers import link
from iotronic.api.controllers.v1 import collection
from iotronic.api.controllers.v1 import types
from iotronic.api.controllers.v1 import utils as api_utils
from iotronic.api import expose
from iotronic.common import exception
from iotronic.common import policy
from iotronic import objects
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
_DEFAULT_RETURN_FIELDS = (
'name', 'uuid', 'project', 'port', 'protocol', 'extra')
class Service(base.APIBase):
"""API representation of a service.
"""
uuid = types.uuid
name = wsme.wsattr(wtypes.text)
project = types.uuid
port = wsme.types.IntegerType()
protocol = wsme.wsattr(wtypes.text)
extra = types.jsontype
links = wsme.wsattr([link.Link], readonly=True)
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.Service.fields)
for k in fields:
# Skip fields we do not expose.
if not hasattr(self, k):
continue
self.fields.append(k)
setattr(self, k, kwargs.get(k, wtypes.Unset))
@staticmethod
def _convert_with_links(service, url, fields=None):
service_uuid = service.uuid
if fields is not None:
service.unset_fields_except(fields)
service.links = [link.Link.make_link('self', url, 'services',
service_uuid),
link.Link.make_link('bookmark', url, 'services',
service_uuid, bookmark=True)
]
return service
@classmethod
def convert_with_links(cls, rpc_service, fields=None):
service = Service(**rpc_service.as_dict())
if fields is not None:
api_utils.check_for_invalid_fields(fields, service.as_dict())
return cls._convert_with_links(service, pecan.request.public_url,
fields=fields)
class ServiceCollection(collection.Collection):
"""API representation of a collection of services."""
services = [Service]
"""A list containing services objects"""
def __init__(self, **kwargs):
self._type = 'services'
@staticmethod
def convert_with_links(services, limit, url=None, fields=None, **kwargs):
collection = ServiceCollection()
collection.services = [Service.convert_with_links(n, fields=fields)
for n in services]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
class PublicServicesController(rest.RestController):
"""REST controller for Public Services."""
invalid_sort_key_list = ['extra', 'location']
def _get_services_collection(self, marker, limit,
sort_key, sort_dir,
fields=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Service.get_by_uuid(pecan.request.context,
marker)
if sort_key in self.invalid_sort_key_list:
raise exception.InvalidParameterValue(
("The sort_key value %(key)s is an invalid field for "
"sorting") % {'key': sort_key})
filters = {}
filters['public'] = True
services = objects.Service.list(pecan.request.context, limit,
marker_obj,
sort_key=sort_key, sort_dir=sort_dir,
filters=filters)
parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
return ServiceCollection.convert_with_links(services, limit,
fields=fields,
**parameters)
@expose.expose(ServiceCollection, types.uuid, int, wtypes.text,
wtypes.text, types.listtype, types.boolean, types.boolean)
def get_all(self, marker=None,
limit=None, sort_key='id', sort_dir='asc',
fields=None):
"""Retrieve a list of services.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
This value cannot be larger than the value of max_limit
in the [api] section of the ironic configuration, or only
max_limit resources will be returned.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
:param fields: Optional, a list with a specified set of fields
of the resource to be returned.
"""
cdict = pecan.request.context.to_policy_values()
policy.authorize('iot:service:get', cdict, cdict)
if fields is None:
fields = _DEFAULT_RETURN_FIELDS
return self._get_services_collection(marker,
limit, sort_key, sort_dir,
fields=fields)
class ServicesController(rest.RestController):
"""REST controller for Services."""
public = PublicServicesController()
invalid_sort_key_list = ['extra', ]
_custom_actions = {
'detail': ['GET'],
}
def _get_services_collection(self, marker, limit,
sort_key, sort_dir,
fields=None, with_public=False,
all_services=False):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.Service.get_by_uuid(pecan.request.context,
marker)
if sort_key in self.invalid_sort_key_list:
raise exception.InvalidParameterValue(
("The sort_key value %(key)s is an invalid field for "
"sorting") % {'key': sort_key})
filters = {}
if all_services and not pecan.request.context.is_admin:
msg = ("all_services parameter can only be used "
"by the administrator.")
raise wsme.exc.ClientSideError(msg,
status_code=400)
else:
if not all_services:
filters['project'] = pecan.request.context.user_id
if with_public:
filters['with_public'] = with_public
services = objects.Service.list(pecan.request.context, limit,
marker_obj,
sort_key=sort_key, sort_dir=sort_dir,
filters=filters)
parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
return ServiceCollection.convert_with_links(services, limit,
fields=fields,
**parameters)
@expose.expose(Service, types.uuid_or_name, types.listtype)
def get_one(self, service_ident, fields=None):
"""Retrieve information about the given service.
:param service_ident: UUID or logical name of a service.
:param fields: Optional, a list with a specified set of fields
of the resource to be returned.
"""
rpc_service = api_utils.get_rpc_service(service_ident)
cdict = pecan.request.context.to_policy_values()
cdict['project'] = rpc_service.project
policy.authorize('iot:service:get_one', cdict, cdict)
return Service.convert_with_links(rpc_service, fields=fields)
@expose.expose(ServiceCollection, types.uuid, int, wtypes.text,
wtypes.text, types.listtype, types.boolean, types.boolean)
def get_all(self, marker=None,
limit=None, sort_key='id', sort_dir='asc',
fields=None, with_public=False, all_services=False):
"""Retrieve a list of services.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
This value cannot be larger than the value of max_limit
in the [api] section of the ironic configuration, or only
max_limit resources will be returned.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
:param with_public: Optional boolean to get also public pluings.
:param all_services: Optional boolean to get all the pluings.
Only for the admin
:param fields: Optional, a list with a specified set of fields
of the resource to be returned.
"""
cdict = pecan.request.context.to_policy_values()
policy.authorize('iot:service:get', cdict, cdict)
if fields is None:
fields = _DEFAULT_RETURN_FIELDS
return self._get_services_collection(marker,
limit, sort_key, sort_dir,
with_public=with_public,
all_services=all_services,
fields=fields)
@expose.expose(Service, body=Service, status_code=201)
def post(self, Service):
"""Create a new Service.
:param Service: a Service within the request body.
"""
context = pecan.request.context
cdict = context.to_policy_values()
policy.authorize('iot:service:create', cdict, cdict)
if not Service.name:
raise exception.MissingParameterValue(
("Name is not specified."))
if Service.name:
if not api_utils.is_valid_name(Service.name):
msg = ("Cannot create service with invalid name %(name)s")
raise wsme.exc.ClientSideError(msg % {'name': Service.name},
status_code=400)
new_Service = objects.Service(pecan.request.context,
**Service.as_dict())
new_Service.project = cdict['project_id']
new_Service = pecan.request.rpcapi.create_service(
pecan.request.context,
new_Service)
return Service.convert_with_links(new_Service)
@expose.expose(None, types.uuid_or_name, status_code=204)
def delete(self, service_ident):
"""Delete a service.
:param service_ident: UUID or logical name of a service.
"""
context = pecan.request.context
cdict = context.to_policy_values()
policy.authorize('iot:service:delete', cdict, cdict)
rpc_service = api_utils.get_rpc_service(service_ident)
pecan.request.rpcapi.destroy_service(pecan.request.context,
rpc_service.uuid)
@expose.expose(Service, types.uuid_or_name, body=Service, status_code=200)
def patch(self, service_ident, val_Service):
"""Update a service.
:param service_ident: UUID or logical name of a service.
:param Service: values to be changed
:return updated_service: updated_service
"""
rpc_service = api_utils.get_rpc_service(service_ident)
cdict = pecan.request.context.to_policy_values()
cdict['project'] = rpc_service.project
policy.authorize('iot:service:update', cdict, cdict)
val_Service = val_Service.as_dict()
for key in val_Service:
try:
rpc_service[key] = val_Service[key]
except Exception:
pass
updated_service = pecan.request.rpcapi.update_service(
pecan.request.context, rpc_service)
return Service.convert_with_links(updated_service)
@expose.expose(ServiceCollection, types.uuid, int, wtypes.text,
wtypes.text, types.listtype, types.boolean, types.boolean)
def detail(self, marker=None,
limit=None, sort_key='id', sort_dir='asc',
fields=None, with_public=False, all_services=False):
"""Retrieve a list of services.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
This value cannot be larger than the value of max_limit
in the [api] section of the ironic configuration, or only
max_limit resources will be returned.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
:param with_public: Optional boolean to get also public service.
:param all_services: Optional boolean to get all the services.
Only for the admin
:param fields: Optional, a list with a specified set of fields
of the resource to be returned.
"""
cdict = pecan.request.context.to_policy_values()
policy.authorize('iot:service:get', cdict, cdict)
# /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "services":
raise exception.HTTPNotFound()
return self._get_services_collection(marker,
limit, sort_key, sort_dir,
with_public=with_public,
all_services=all_services,
fields=fields)

View File

@ -120,6 +120,33 @@ def get_rpc_plugin(plugin_ident):
raise exception.PluginNotFound(plugin=plugin_ident)
def get_rpc_service(service_ident):
"""Get the RPC service from the service uuid or logical name.
:param service_ident: the UUID or logical name of a service.
:returns: The RPC Service.
:raises: InvalidUuidOrName if the name or uuid provided is not valid.
:raises: ServiceNotFound if the service is not found.
"""
# Check to see if the service_ident is a valid UUID. If it is, treat it
# as a UUID.
if uuidutils.is_uuid_like(service_ident):
return objects.Service.get_by_uuid(pecan.request.context,
service_ident)
# We can refer to services by their name, if the client supports it
# if allow_service_logical_names():
# if utils.is_hostname_safe(service_ident):
else:
return objects.Service.get_by_name(pecan.request.context,
service_ident)
raise exception.InvalidUuidOrName(name=service_ident)
raise exception.ServiceNotFound(service=service_ident)
def is_valid_board_name(name):
"""Determine if the provided name is a valid board name.

View File

@ -593,3 +593,7 @@ class NeedParams(Invalid):
class ErrorExecutionOnBoard(IotronicException):
message = _("Error in the execution of %(call)s on %(board)s: %(error)s")
class ServiceNotFound(NotFound):
message = _("Service %(Service)s could not be found.")

View File

@ -120,12 +120,29 @@ injection_plugin_policies = [
]
service_policies = [
policy.RuleDefault('iot:service:get',
'rule:is_admin or rule:is_iot_member',
description='Retrieve Service records'),
policy.RuleDefault('iot:service:create',
'rule:is_iot_member',
description='Create Service records'),
policy.RuleDefault('iot:service:get_one', 'rule:admin_or_owner',
description='Retrieve a Service record'),
policy.RuleDefault('iot:service:delete', 'rule:admin_or_owner',
description='Delete Service records'),
policy.RuleDefault('iot:service:update', 'rule:admin_or_owner',
description='Update Service records'),
]
def list_policies():
policies = (default_policies
+ board_policies
+ plugin_policies
+ injection_plugin_policies
+ service_policies
)
return policies

View File

@ -270,3 +270,23 @@ class ConductorEndpoint(object):
LOG.debug(result)
return result
def create_service(self, ctx, service_obj):
new_service = serializer.deserialize_entity(ctx, service_obj)
LOG.debug('Creating service %s',
new_service.name)
new_service.create()
return serializer.serialize_entity(ctx, new_service)
def destroy_service(self, ctx, service_id):
LOG.info('Destroying service with id %s',
service_id)
service = objects.Service.get_by_uuid(ctx, service_id)
service.destroy()
return
def update_service(self, ctx, service_obj):
service = serializer.deserialize_entity(ctx, service_obj)
LOG.debug('Updating service %s', service.name)
service.save()
return serializer.serialize_entity(ctx, service)

View File

@ -207,3 +207,45 @@ class ConductorAPI(object):
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
return cctxt.call(context, 'action_plugin', plugin_uuid=plugin_uuid,
board_uuid=board_uuid, action=action, params=params)
def create_service(self, context, service_obj, topic=None):
"""Add a service on the cloud
:param context: request context.
:param service_obj: a changed (but not saved) service object.
:param topic: RPC topic. Defaults to self.topic.
:returns: created service object
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
return cctxt.call(context, 'create_service',
service_obj=service_obj)
def destroy_service(self, context, service_id, topic=None):
"""Delete a service.
:param context: request context.
:param service_id: service id or uuid.
:raises: ServiceLocked if service is locked by another conductor.
:raises: ServiceAssociated if the service contains an instance
associated with it.
:raises: InvalidState if the service is in the wrong provision
state to perform deletion.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
return cctxt.call(context, 'destroy_service', service_id=service_id)
def update_service(self, context, service_obj, topic=None):
"""Synchronously, have a conductor update the service's information.
Update the service's information in the database and
return a service object.
:param context: request context.
:param service_obj: a changed (but not saved) service object.
:param topic: RPC topic. Defaults to self.topic.
:returns: updated service object, including all fields.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
return cctxt.call(context, 'update_service', service_obj=service_obj)

View File

@ -421,5 +421,55 @@ class Connection(object):
:param board_uuid: The id or uuid of a plugin.
:returns: A list of InjectionPlugins on the board.
"""
@abc.abstractmethod
def get_service_by_id(self, service_id):
"""Return a service.
:param service_id: The id of a service.
:returns: A service.
"""
@abc.abstractmethod
def get_service_by_uuid(self, service_uuid):
"""Return a service.
:param service_uuid: The uuid of a service.
:returns: A service.
"""
@abc.abstractmethod
def get_service_by_name(self, service_name):
"""Return a service.
:param service_name: The logical name of a service.
:returns: A service.
"""
@abc.abstractmethod
def create_service(self, values):
"""Create a new service.
:param values: A dict containing several items used to identify
and track the service
:returns: A service.
"""
@abc.abstractmethod
def destroy_service(self, service_id):
"""Destroy a service and all associated interfaces.
:param service_id: The id or uuid of a service.
"""
@abc.abstractmethod
def update_service(self, service_id, values):
"""Update properties of a service.
:param service_id: The id or uuid of a service.
:param values: Dict of values to update.
:returns: A service.
:raises: ServiceAssociated
:raises: ServiceNotFound
"""

View File

@ -153,6 +153,14 @@ class Connection(api.Connection):
return query
def _add_services_filters(self, query, filters):
if filters is None:
filters = []
if 'owner' in filters:
query = query.filter(models.Plugin.owner == filters['owner'])
return query
def _add_wampagents_filters(self, query, filters):
if filters is None:
filters = []
@ -664,3 +672,92 @@ class Connection(api.Connection):
models.InjectionPlugin).filter_by(
board_uuid=board_uuid)
return query.all()
# SERVICE api
def get_service_by_id(self, service_id):
query = model_query(models.Service).filter_by(id=service_id)
try:
return query.one()
except NoResultFound:
raise exception.ServiceNotFound(service=service_id)
def get_service_by_uuid(self, service_uuid):
query = model_query(models.Service).filter_by(uuid=service_uuid)
try:
return query.one()
except NoResultFound:
raise exception.ServiceNotFound(service=service_uuid)
def get_service_by_name(self, service_name):
query = model_query(models.Service).filter_by(name=service_name)
try:
return query.one()
except NoResultFound:
raise exception.ServiceNotFound(service=service_name)
def destroy_service(self, service_id):
session = get_session()
with session.begin():
query = model_query(models.Service, session=session)
query = add_identity_filter(query, service_id)
try:
service_ref = query.one()
except NoResultFound:
raise exception.ServiceNotFound(service=service_id)
# Get service ID, if an UUID was supplied. The ID is
# required for deleting all ports, attached to the service.
if uuidutils.is_uuid_like(service_id):
service_id = service_ref['id']
query.delete()
def update_service(self, service_id, values):
# NOTE(dtantsur): this can lead to very strange errors
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Service.")
raise exception.InvalidParameterValue(err=msg)
try:
return self._do_update_service(service_id, values)
except db_exc.DBDuplicateEntry as e:
if 'name' in e.columns:
raise exception.DuplicateName(name=values['name'])
elif 'uuid' in e.columns:
raise exception.ServiceAlreadyExists(uuid=values['uuid'])
else:
raise e
def create_service(self, values):
# ensure defaults are present for new services
if 'uuid' not in values:
values['uuid'] = uuidutils.generate_uuid()
service = models.Service()
service.update(values)
try:
service.save()
except db_exc.DBDuplicateEntry:
raise exception.ServiceAlreadyExists(uuid=values['uuid'])
return service
def get_service_list(self, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
query = model_query(models.Service)
query = self._add_services_filters(query, filters)
return _paginate_query(models.Service, limit, marker,
sort_key, sort_dir, query)
def _do_update_service(self, service_id, values):
session = get_session()
with session.begin():
query = model_query(models.Service, session=session)
query = add_identity_filter(query, service_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.ServiceNotFound(service=service_id)
ref.update(values)
return ref

View File

@ -220,3 +220,31 @@ class InjectionPlugin(Base):
plugin_uuid = Column(String(36), ForeignKey('plugins.uuid'))
onboot = Column(Boolean, default=False)
status = Column(String(15))
class Service(Base):
"""Represents a service."""
__tablename__ = 'services'
__table_args__ = (
schema.UniqueConstraint('uuid', name='uniq_services0uuid'),
table_args())
id = Column(Integer, primary_key=True)
uuid = Column(String(36))
name = Column(String(36))
project = Column(String(36))
port = Column(Integer)
protocol = Column(String(3))
extra = Column(JSONEncodedDict)
class ExposedService(Base):
"""Represents an exposed service on board."""
__tablename__ = 'exposed_services'
__table_args__ = (
table_args())
id = Column(Integer, primary_key=True)
board_uuid = Column(String(36), ForeignKey('boards.uuid'))
service_uuid = Column(String(36), ForeignKey('services.uuid'))
public_port = Column(Integer)

View File

@ -17,6 +17,7 @@ from iotronic.objects import conductor
from iotronic.objects import injectionplugin
from iotronic.objects import location
from iotronic.objects import plugin
from iotronic.objects import service
from iotronic.objects import sessionwp
from iotronic.objects import wampagent
@ -27,6 +28,7 @@ Plugin = plugin.Plugin
InjectionPlugin = injectionplugin.InjectionPlugin
SessionWP = sessionwp.SessionWP
WampAgent = wampagent.WampAgent
Service = service.Service
__all__ = (
Conductor,
@ -34,6 +36,7 @@ __all__ = (
Location,
SessionWP,
WampAgent,
Service,
Plugin,
InjectionPlugin,
)

211
iotronic/objects/service.py Normal file
View File

@ -0,0 +1,211 @@
# 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 oslo_utils import strutils
from oslo_utils import uuidutils
from iotronic.common import exception
from iotronic.db import api as db_api
from iotronic.objects import base
from iotronic.objects import utils as obj_utils
"""
ACTIONS = ['ServiceCall', 'ServiceStop', 'ServiceStart',
'ServiceStatus', 'ServiceReboot']
CUSTOM_PARAMS = ['ServiceCall', 'ServiceStart', 'ServiceReboot']
NO_PARAMS = ['ServiceStatus']
def is_valid_action(action):
if action not in ACTIONS:
raise exception.InvalidServiceAction(action=action)
return True
def want_customs_params(action):
return True if action in CUSTOM_PARAMS else False
def want_params(action):
return False if action in NO_PARAMS else True
"""
class Service(base.IotronicObject):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = db_api.get_instance()
fields = {
'id': int,
'uuid': obj_utils.str_or_none,
'name': obj_utils.str_or_none,
'project': obj_utils.str_or_none,
'port': int,
'protocol': obj_utils.str_or_none,
'extra': obj_utils.dict_or_none,
}
@staticmethod
def _from_db_object(service, db_service):
"""Converts a database entity to a formal object."""
for field in service.fields:
service[field] = db_service[field]
service.obj_reset_changes()
return service
@base.remotable_classmethod
def get(cls, context, service_id):
"""Find a service based on its id or uuid and return a Board object.
:param service_id: the id *or* uuid of a service.
:returns: a :class:`Board` object.
"""
if strutils.is_int_like(service_id):
return cls.get_by_id(context, service_id)
elif uuidutils.is_uuid_like(service_id):
return cls.get_by_uuid(context, service_id)
else:
raise exception.InvalidIdentity(identity=service_id)
@base.remotable_classmethod
def get_by_id(cls, context, service_id):
"""Find a service based on its integer id and return a Board object.
:param service_id: the id of a service.
:returns: a :class:`Board` object.
"""
db_service = cls.dbapi.get_service_by_id(service_id)
service = Service._from_db_object(cls(context), db_service)
return service
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find a service based on uuid and return a Board object.
:param uuid: the uuid of a service.
:returns: a :class:`Board` object.
"""
db_service = cls.dbapi.get_service_by_uuid(uuid)
service = Service._from_db_object(cls(context), db_service)
return service
@base.remotable_classmethod
def get_by_name(cls, context, name):
"""Find a service based on name and return a Board object.
:param name: the logical name of a service.
:returns: a :class:`Board` object.
"""
db_service = cls.dbapi.get_service_by_name(name)
service = Service._from_db_object(cls(context), db_service)
return service
@base.remotable_classmethod
def list(cls, context, limit=None, marker=None, sort_key=None,
sort_dir=None, filters=None):
"""Return a list of Service 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".
:param filters: Filters to apply.
:returns: a list of :class:`Service` object.
"""
db_services = cls.dbapi.get_service_list(filters=filters,
limit=limit,
marker=marker,
sort_key=sort_key,
sort_dir=sort_dir)
return [Service._from_db_object(cls(context), obj)
for obj in db_services]
@base.remotable
def create(self, context=None):
"""Create a Service record in the DB.
Column-wise updates will be made based on the result of
self.what_changed(). If target_power_state is provided,
it will be checked against the in-database copy of the
service before updates are made.
: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.: Service(context)
"""
values = self.obj_get_changes()
db_service = self.dbapi.create_service(values)
self._from_db_object(self, db_service)
@base.remotable
def destroy(self, context=None):
"""Delete the Service 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.: Service(context)
"""
self.dbapi.destroy_service(self.uuid)
self.obj_reset_changes()
@base.remotable
def save(self, context=None):
"""Save updates to this Service.
Column-wise updates will be made based on the result of
self.what_changed(). If target_power_state is provided,
it will be checked against the in-database copy of the
service before updates are made.
: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.: Service(context)
"""
updates = self.obj_get_changes()
self.dbapi.update_service(self.uuid, updates)
self.obj_reset_changes()
@base.remotable
def refresh(self, context=None):
"""Refresh the object by re-fetching 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.: Service(context)
"""
current = self.__class__.get_by_uuid(self._context, 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

@ -134,22 +134,20 @@ AUTO_INCREMENT = 10
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `iotronic`.`plugins`
-- Table `iotronic`.`services`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `iotronic`.`plugins` ;
DROP TABLE IF EXISTS `iotronic`.`services` ;
CREATE TABLE IF NOT EXISTS `iotronic`.`plugins` (
CREATE TABLE IF NOT EXISTS `iotronic`.`services` (
`created_at` DATETIME NULL DEFAULT NULL,
`updated_at` DATETIME NULL DEFAULT NULL,
`id` INT(11) NOT NULL AUTO_INCREMENT,
`uuid` VARCHAR(36) NOT NULL,
`name` VARCHAR(255) NULL DEFAULT NULL,
`public` TINYINT(1) NOT NULL DEFAULT '0',
`code` TEXT NULL DEFAULT NULL,
`callable` TINYINT(1) NOT NULL,
`parameters` TEXT NULL DEFAULT NULL,
`port` INT(5) NOT NULL,
`project` VARCHAR(36) NOT NULL,
`protocol` VARCHAR(3) NOT NULL,
`extra` TEXT NULL DEFAULT NULL,
`owner` VARCHAR(36) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `uuid` (`uuid` ASC))
ENGINE = InnoDB
@ -157,36 +155,35 @@ AUTO_INCREMENT = 132
DEFAULT CHARACTER SET = utf8;
-- -----------------------------------------------------
-- Table `iotronic`.`injected_plugins`
-- Table `iotronic`.`exposed_services`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `iotronic`.`injection_plugins` ;
DROP TABLE IF EXISTS `iotronic`.`exposed_services` ;
CREATE TABLE IF NOT EXISTS `iotronic`.`injection_plugins` (
CREATE TABLE IF NOT EXISTS `iotronic`.`exposed_services` (
`created_at` DATETIME NULL DEFAULT NULL,
`updated_at` DATETIME NULL DEFAULT NULL,
`id` INT(11) NOT NULL AUTO_INCREMENT,
`board_uuid` VARCHAR(36) NOT NULL,
`plugin_uuid` VARCHAR(36) NOT NULL,
`status` VARCHAR(15) NOT NULL DEFAULT 'injected',
`onboot` TINYINT(1) NOT NULL DEFAULT '0',
`service_uuid` VARCHAR(36) NOT NULL,
`public_port` INT(5) NOT NULL,
PRIMARY KEY (`id`),
INDEX `board_uuid` (`board_uuid` ASC),
CONSTRAINT `board_uuid`
CONSTRAINT `fk_board_uuid`
FOREIGN KEY (`board_uuid`)
REFERENCES `iotronic`.`boards` (`uuid`)
ON DELETE CASCADE
ON UPDATE CASCADE,
INDEX `plugin_uuid` (`plugin_uuid` ASC),
CONSTRAINT `plugin_uuid`
FOREIGN KEY (`plugin_uuid`)
REFERENCES `iotronic`.`plugins` (`uuid`)
INDEX `service_uuid` (`service_uuid` ASC),
CONSTRAINT `service_uuid`
FOREIGN KEY (`service_uuid`)
REFERENCES `iotronic`.`services` (`uuid`)
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB
AUTO_INCREMENT = 132
DEFAULT CHARACTER SET = utf8;
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;