Exposed Cloud Service

Iotronic exposes the services of the boards

Change-Id: Id88c4e6a9d5752d1bffbcfd99827eca0b9b679f7
This commit is contained in:
Fabio Verboso 2018-02-13 18:06:24 +01:00
parent 6e9e02e9c3
commit f450f91c70
12 changed files with 626 additions and 29 deletions

View File

@ -152,11 +152,50 @@ class InjectionCollection(collection.Collection):
return collection
class ExposedService(base.APIBase):
service = types.uuid_or_name
board_uuid = types.uuid_or_name
public_port = wsme.types.IntegerType()
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.ExposedService.fields)
fields.remove('board_uuid')
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))
setattr(self, 'service', kwargs.get('service_uuid', wtypes.Unset))
class ExposedCollection(collection.Collection):
"""API representation of a collection of injection."""
exposed = [ExposedService]
def __init__(self, **kwargs):
self._type = 'exposed'
@staticmethod
def get_list(exposed, fields=None):
collection = ExposedCollection()
collection.exposed = [ExposedService(**n.as_dict())
for n in exposed]
return collection
class PluginAction(base.APIBase):
action = wsme.wsattr(wtypes.text)
parameters = types.jsontype
class ServiceAction(base.APIBase):
action = wsme.wsattr(wtypes.text)
parameters = types.jsontype
class BoardPluginsController(rest.RestController):
def __init__(self, board_ident):
self.board_ident = board_ident
@ -282,11 +321,72 @@ class BoardPluginsController(rest.RestController):
rpc_board.uuid)
class BoardServicesController(rest.RestController):
_custom_actions = {
'action': ['POST'],
}
def __init__(self, board_ident):
self.board_ident = board_ident
def _get_services_on_board_collection(self, board_uuid, fields=None):
services = objects.ExposedService.list(pecan.request.context,
board_uuid)
return ExposedCollection.get_list(services,
fields=fields)
@expose.expose(ExposedCollection,
status_code=200)
def get_all(self):
"""Retrieve a list of services of a board.
"""
rpc_board = api_utils.get_rpc_board(self.board_ident)
cdict = pecan.request.context.to_policy_values()
cdict['project_id'] = rpc_board.project
policy.authorize('iot:service_on_board:get', cdict, cdict)
return self._get_services_on_board_collection(rpc_board.uuid)
@expose.expose(wtypes.text, types.uuid_or_name, body=ServiceAction,
status_code=200)
def action(self, service_ident, ServiceAction):
if not ServiceAction.action:
raise exception.MissingParameterValue(
("Action is not specified."))
if not ServiceAction.parameters:
ServiceAction.parameters = {}
rpc_board = api_utils.get_rpc_board(self.board_ident)
rpc_service = api_utils.get_rpc_service(service_ident)
try:
cdict = pecan.request.context.to_policy_values()
cdict['owner'] = rpc_board.owner
policy.authorize('iot:service_action:post', cdict, cdict)
except exception:
return exception
rpc_board.check_if_online()
result = pecan.request.rpcapi.action_service(pecan.request.context,
rpc_service.uuid,
rpc_board.uuid,
ServiceAction.action)
return result
class BoardsController(rest.RestController):
"""REST controller for Boards."""
_subcontroller_map = {
'plugins': BoardPluginsController,
'services': BoardServicesController,
}
invalid_sort_key_list = ['extra', 'location']

View File

@ -597,3 +597,15 @@ class ErrorExecutionOnBoard(IotronicException):
class ServiceNotFound(NotFound):
message = _("Service %(Service)s could not be found.")
class ServiceAlreadyExists(Conflict):
message = _("A Service with UUID %(uuid)s already exists.")
class ServiceAlreadyExposed(Conflict):
message = _("A Service with UUID %(uuid)s already exposed.")
class ExposedServiceNotFound(NotFound):
message = _("ExposedService %(uuid)s could not be found.")

View File

@ -136,6 +136,20 @@ service_policies = [
]
exposed_service_policies = [
policy.RuleDefault('iot:service_on_board:get',
'rule:admin_or_owner',
description='Retrieve Service records'),
policy.RuleDefault('iot:service_remove:delete', 'rule:admin_or_owner',
description='Delete Service records'),
policy.RuleDefault('iot:service_action:post',
'rule:admin_or_owner',
description='Create Service records'),
policy.RuleDefault('iot:service_inject:put', 'rule:admin_or_owner',
description='Retrieve a Service record'),
]
def list_policies():
policies = (default_policies
@ -143,6 +157,7 @@ def list_policies():
+ plugin_policies
+ injection_plugin_policies
+ service_policies
+ exposed_service_policies
)
return policies

View File

@ -39,6 +39,26 @@ def get_best_agent(ctx):
return agent.hostname
def random_public_port():
return random.randint(6000, 7000)
def manage_result(res, wamp_rpc_call, board_uuid):
if res.result == wm.SUCCESS:
return res.message
elif res.result == wm.WARNING:
LOG.warning('Warning in the execution of %s on %s', wamp_rpc_call,
board_uuid)
return res.message
elif res.result == wm.ERROR:
LOG.error('Error in the execution of %s on %s: %s', wamp_rpc_call,
board_uuid, res.message)
raise exception.ErrorExecutionOnBoard(call=wamp_rpc_call,
board=board_uuid,
error=res.message)
return res.message
class ConductorEndpoint(object):
def __init__(self, ragent):
transport = oslo_messaging.get_transport(cfg.CONF)
@ -119,10 +139,12 @@ class ConductorEndpoint(object):
board_id,
'destroyBoard',
(p,))
except exception:
return exception
board.destroy()
if result:
result = manage_result(result, 'destroyBoard', board_id)
LOG.debug(result)
return result
return
@ -164,18 +186,7 @@ class ConductorEndpoint(object):
data=wamp_rpc_args)
res = wm.deserialize(res)
if res.result == wm.SUCCESS:
return res.message
elif res.result == wm.WARNING:
LOG.warning('Warning in the execution of %s on %s', wamp_rpc_call,
board_uuid)
return res.message
elif res.result == wm.ERROR:
LOG.error('Error in the execution of %s on %s: %s', wamp_rpc_call,
board_uuid, res.message)
raise exception.ErrorExecutionOnBoard(call=wamp_rpc_call,
board=board.uuid,
error=res.message)
return res
def destroy_plugin(self, ctx, plugin_id):
LOG.info('Destroying plugin with id %s',
@ -231,7 +242,9 @@ class ConductorEndpoint(object):
injection = objects.InjectionPlugin(ctx, **inj_data)
injection.create()
result = manage_result(result, 'PluginInject', board_uuid)
LOG.debug(result)
return result
def remove_plugin(self, ctx, plugin_uuid, board_uuid):
@ -247,7 +260,7 @@ class ConductorEndpoint(object):
(plugin.uuid,))
except exception:
return exception
result = manage_result(result, 'PluginRemove', board_uuid)
LOG.debug(result)
injection.destroy()
return result
@ -267,7 +280,7 @@ class ConductorEndpoint(object):
(plugin.uuid,))
except exception:
return exception
result = manage_result(result, action, board_uuid)
LOG.debug(result)
return result
@ -290,3 +303,151 @@ class ConductorEndpoint(object):
LOG.debug('Updating service %s', service.name)
service.save()
return serializer.serialize_entity(ctx, service)
def action_service(self, ctx, service_uuid, board_uuid, action):
LOG.info('Enable service with id %s into the board %s',
service_uuid, board_uuid)
service = objects.Service.get(ctx, service_uuid)
objects.service.is_valid_action(action)
if action == "ServiceEnable":
try:
objects.ExposedService.get(ctx,
board_uuid,
service_uuid)
return exception.ServiceAlreadyExposed(uuid=service_uuid)
except Exception:
name = service.name
public_port = random_public_port()
port = service.port
res = self.execute_on_board(ctx, board_uuid, action,
(name, public_port, port))
if res.result == wm.SUCCESS:
pid = res.message[0]
exp_data = {
'board_uuid': board_uuid,
'service_uuid': service_uuid,
'public_port': public_port,
'pid': pid,
}
exposed = objects.ExposedService(ctx, **exp_data)
exposed.create()
res.message = res.message[1]
elif res.result == wm.ERROR:
LOG.error('Error in the execution of %s on %s: %s',
action,
board_uuid, res.message)
raise exception.ErrorExecutionOnBoard(call=action,
board=board_uuid,
error=res.message)
LOG.debug(res.message)
return res.message
elif action == "ServiceDisable":
exposed = objects.ExposedService.get(ctx,
board_uuid,
service_uuid)
res = self.execute_on_board(ctx, board_uuid, action,
(service.name, exposed.pid))
result = manage_result(res, action, board_uuid)
LOG.debug(res.message)
exposed.destroy()
return result
elif action == "ServiceRestore":
exposed = objects.ExposedService.get(ctx, board_uuid,
service_uuid)
print(exposed)
res = self.execute_on_board(ctx, board_uuid, action,
(service.name, exposed.public_port,
service.port, exposed.pid))
if res.result == wm.SUCCESS:
pid = res.message[0]
exp_data = {
'id': exposed.id,
'board_uuid': board_uuid,
'service_uuid': service_uuid,
'public_port': exposed.public_port,
'pid': pid,
}
exposed = objects.ExposedService(ctx, **exp_data)
exposed.save()
res.message = res.message[1]
elif res.result == wm.ERROR:
LOG.error('Error in the execution of %s on %s: %s',
action,
board_uuid, res.message)
raise exception.ErrorExecutionOnBoard(call=action,
board=board_uuid,
error=res.message)
LOG.debug(res.message)
return res.message
# try:
#
#
# return exception.ServiceAlreadyExposed(uuid=service_uuid)
# except:
# name=service.name
# public_port=random_public_port()
# port=service.port
#
# res = self.execute_on_board(ctx, board_uuid, action,
# (name, public_port, port))
#
# if res.result == wm.SUCCESS:
# pid = res.message[0]
#
# exp_data = {
# 'board_uuid': board_uuid,
# 'service_uuid': service_uuid,
# 'public_port': public_port,
# 'pid': pid,
# }
# exposed = objects.ExposedService(ctx, **exp_data)
# exposed.create()
#
# res.message = res.message[1]
# elif res.result == wm.ERROR:
# LOG.error('Error in the execution of %s on %s: %s',
# action,
# board_uuid, res.message)
# raise exception.ErrorExecutionOnBoard(call=action,
# board=board_uuid,
# error=res.message)
# LOG.debug(res.message)
# return res.message
#
#
#
#
#
#
#
#
#
#
#
# exposed = objects.ExposedService.get(ctx, board_uuid,
# service_uuid)
#
# res = self.execute_on_board(ctx, board_uuid, action,
# (service.name, exposed.pid))
#
# result=manage_result(res,action,board_uuid)
# LOG.debug(res.message)
# exposed.destroy()
# return result

View File

@ -249,3 +249,17 @@ class ConductorAPI(object):
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
return cctxt.call(context, 'update_service', service_obj=service_obj)
def action_service(self, context, service_uuid,
board_uuid, action, topic=None):
"""Action on a service into a board.
:param context: request context.
:param service_uuid: service id or uuid.
:param board_uuid: board id or uuid.
"""
cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
return cctxt.call(context, 'action_service', service_uuid=service_uuid,
board_uuid=board_uuid, action=action)

View File

@ -473,3 +473,57 @@ class Connection(object):
:raises: ServiceAssociated
:raises: ServiceNotFound
"""
@abc.abstractmethod
def get_exposed_service_by_board_uuid(self, board_uuid):
"""get an exposed of a service using a board_uuid
:param board_uuid: The id or uuid of a board.
:returns: An exposed_service.
"""
@abc.abstractmethod
def get_exposed_service_by_uuids(self, board_uuid, service_uuid):
"""get an exposed of a service using a board_uuid and service_uuid
:param board_uuid: The id or uuid of a board.
:param service_uuid: The id or uuid of a service.
:returns: An exposed_service.
"""
@abc.abstractmethod
def create_exposed_service(self, values):
"""Create a new exposed_service.
:param values: A dict containing several items used to identify
and track the service
:returns: An exposed service.
"""
@abc.abstractmethod
def destroy_exposed_service(self, exposed_service_id):
"""Destroy an exposed service and all associated interfaces.
:param exposed_service_id: The id or uuid of a service.
"""
@abc.abstractmethod
def update_exposed_service(self, service_exposed_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
"""
@abc.abstractmethod
def get_exposed_service_list(self, board_uuid):
"""Return a list of exposed_services.
:param board_uuid: The id or uuid of a service.
:returns: A list of ExposedServices on the board.
"""

View File

@ -761,3 +761,84 @@ class Connection(api.Connection):
ref.update(values)
return ref
# EXPOSED SERVICE api
def get_exposed_service_by_board_uuid(self, board_uuid):
query = model_query(
models.ExposedService).filter_by(
board_uuid=board_uuid)
try:
return query.one()
except NoResultFound:
raise exception.ExposedServiceNotFound()
def create_exposed_service(self, values):
# ensure defaults are present for new services
if 'uuid' not in values:
values['uuid'] = uuidutils.generate_uuid()
exp_serv = models.ExposedService()
exp_serv.update(values)
try:
exp_serv.save()
except db_exc.DBDuplicateEntry:
raise exception.ServiceAlreadyExposed(uuid=values['uuid'])
return exp_serv
def update_exposed_service(self, service_exposed_id, values):
if 'uuid' in values:
msg = _("Cannot overwrite UUID for an existing Service.")
raise exception.InvalidParameterValue(err=msg)
try:
return self._do_update_exposed_service(
service_exposed_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 get_exposed_service_by_uuids(self, board_uuid, service_uuid):
query = model_query(
models.ExposedService).filter_by(
board_uuid=board_uuid).filter_by(
service_uuid=service_uuid)
try:
return query.one()
except NoResultFound:
raise exception.ExposedServiceNotFound(uuid=service_uuid)
def destroy_exposed_service(self, exposed_service_id):
session = get_session()
with session.begin():
query = model_query(models.ExposedService, session=session)
query = add_identity_filter(query, exposed_service_id)
try:
query.delete()
except NoResultFound:
raise exception.ExposedServiceNotFound()
def get_exposed_service_list(self, board_uuid):
query = model_query(
models.ExposedService).filter_by(
board_uuid=board_uuid)
return query.all()
def _do_update_exposed_service(self, service_id, values):
session = get_session()
with session.begin():
query = model_query(models.ExposedService, session=session)
query = add_identity_filter(query, service_id)
try:
ref = query.with_lockmode('update').one()
except NoResultFound:
raise exception.ServiceNotFoundNotFound(uuid=service_id)
ref.update(values)
return ref

View File

@ -248,3 +248,4 @@ class ExposedService(Base):
board_uuid = Column(String(36), ForeignKey('boards.uuid'))
service_uuid = Column(String(36), ForeignKey('services.uuid'))
public_port = Column(Integer)
pid = Column(Integer)

View File

@ -14,6 +14,7 @@
from iotronic.objects import board
from iotronic.objects import conductor
from iotronic.objects import exposedservice
from iotronic.objects import injectionplugin
from iotronic.objects import location
from iotronic.objects import plugin
@ -26,6 +27,7 @@ Board = board.Board
Location = location.Location
Plugin = plugin.Plugin
InjectionPlugin = injectionplugin.InjectionPlugin
ExposedService = exposedservice.ExposedService
SessionWP = sessionwp.SessionWP
WampAgent = wampagent.WampAgent
Service = service.Service
@ -39,4 +41,5 @@ __all__ = (
Service,
Plugin,
InjectionPlugin,
ExposedService
)

View File

@ -0,0 +1,165 @@
# 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 iotronic.db import api as db_api
from iotronic.objects import base
from iotronic.objects import utils as obj_utils
class ExposedService(base.IotronicObject):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = db_api.get_instance()
fields = {
'id': int,
'board_uuid': obj_utils.str_or_none,
'service_uuid': obj_utils.str_or_none,
'public_port': int,
'pid': int
}
@staticmethod
def _from_db_object(exposed_service, db_exposed_service):
"""Converts a database entity to a formal object."""
for field in exposed_service.fields:
exposed_service[field] = db_exposed_service[field]
exposed_service.obj_reset_changes()
return exposed_service
@base.remotable_classmethod
def get_by_id(cls, context, exposed_service_id):
"""Find a exposed_service based on its integer id and return a Board object.
:param exposed_service_id: the id of a exposed_service.
:returns: a :class:`exposed_service` object.
"""
db_exp_service = cls.dbapi.get_exposed_service_by_id(
exposed_service_id)
exp_service = ExposedService._from_db_object(cls(context),
db_exp_service)
return exp_service
@base.remotable_classmethod
def get_by_board_uuid(cls, context, board_uuid):
"""Find a exposed_service based on uuid and return a Board object.
:param board_uuid: the uuid of a exposed_service.
:returns: a :class:`exposed_service` object.
"""
db_exp_service = cls.dbapi.get_exposed_service_by_board_uuid(
board_uuid)
exp_service = ExposedService._from_db_object(cls(context),
db_exp_service)
return exp_service
@base.remotable_classmethod
def get_by_service_uuid(cls, context, service_uuid):
"""Find a exposed_service based on uuid and return a Board object.
:param service_uuid: the uuid of a exposed_service.
:returns: a :class:`exposed_service` object.
"""
db_exp_service = cls.dbapi.get_exposed_service_by_service_uuid(
service_uuid)
exp_service = ExposedService._from_db_object(cls(context),
db_exp_service)
return exp_service
@base.remotable_classmethod
def get(cls, context, board_uuid, service_uuid):
"""Find a exposed_service based on uuid and return a Service object.
:param board_uuid: the uuid of a exposed_service.
:returns: a :class:`exposed_service` object.
"""
db_exp_service = cls.dbapi.get_exposed_service_by_uuids(board_uuid,
service_uuid)
exp_service = ExposedService._from_db_object(cls(context),
db_exp_service)
return exp_service
@base.remotable_classmethod
def list(cls, context, board_uuid):
"""Return a list of ExposedService 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:`ExposedService` object.
"""
db_exps = cls.dbapi.get_exposed_service_list(board_uuid)
return [ExposedService._from_db_object(cls(context), obj)
for obj in db_exps]
@base.remotable
def create(self, context=None):
"""Create a ExposedService 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
exposed_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.: ExposedService(context)
"""
values = self.obj_get_changes()
db_exposed_service = self.dbapi.create_exposed_service(values)
self._from_db_object(self, db_exposed_service)
@base.remotable
def destroy(self, context=None):
"""Delete the ExposedService 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.: ExposedService(context)
"""
self.dbapi.destroy_exposed_service(self.id)
self.obj_reset_changes()
@base.remotable
def save(self, context=None):
"""Save updates to this ExposedService.
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
exposed_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.: ExposedService(context)
"""
updates = self.obj_get_changes()
self.dbapi.update_exposed_service(self.id, updates)
self.obj_reset_changes()

View File

@ -21,11 +21,8 @@ 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']
ACTIONS = ['ServiceEnable', 'ServiceDisable', 'ServiceRestore']
def is_valid_action(action):
@ -34,16 +31,6 @@ def is_valid_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'
@ -154,6 +141,7 @@ class Service(base.IotronicObject):
object, e.g.: Service(context)
"""
values = self.obj_get_changes()
db_service = self.dbapi.create_service(values)
self._from_db_object(self, db_service)

View File

@ -167,8 +167,11 @@ CREATE TABLE IF NOT EXISTS `iotronic`.`exposed_services` (
`board_uuid` VARCHAR(36) NOT NULL,
`service_uuid` VARCHAR(36) NOT NULL,
`public_port` INT(5) NOT NULL,
`pid` INT(5) NOT NULL,
PRIMARY KEY (`id`),
INDEX `board_uuid` (`board_uuid` ASC),
CONSTRAINT unique_index
UNIQUE (service_uuid, board_uuid, pid),
CONSTRAINT `fk_board_uuid`
FOREIGN KEY (`board_uuid`)
REFERENCES `iotronic`.`boards` (`uuid`)