Cloud Services development
Starting to manage cloud services for the board. Change-Id: I09cb167ec356ea08101bc3d4621f7fa73a18ec1c
This commit is contained in:
parent
88d5be7930
commit
6e9e02e9c3
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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]
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue