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 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): class PluginAction(base.APIBase):
action = wsme.wsattr(wtypes.text) action = wsme.wsattr(wtypes.text)
parameters = types.jsontype parameters = types.jsontype
class ServiceAction(base.APIBase):
action = wsme.wsattr(wtypes.text)
parameters = types.jsontype
class BoardPluginsController(rest.RestController): class BoardPluginsController(rest.RestController):
def __init__(self, board_ident): def __init__(self, board_ident):
self.board_ident = board_ident self.board_ident = board_ident
@ -282,11 +321,72 @@ class BoardPluginsController(rest.RestController):
rpc_board.uuid) 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): class BoardsController(rest.RestController):
"""REST controller for Boards.""" """REST controller for Boards."""
_subcontroller_map = { _subcontroller_map = {
'plugins': BoardPluginsController, 'plugins': BoardPluginsController,
'services': BoardServicesController,
} }
invalid_sort_key_list = ['extra', 'location'] invalid_sort_key_list = ['extra', 'location']

View File

@ -597,3 +597,15 @@ class ErrorExecutionOnBoard(IotronicException):
class ServiceNotFound(NotFound): class ServiceNotFound(NotFound):
message = _("Service %(Service)s could not be found.") 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(): def list_policies():
policies = (default_policies policies = (default_policies
@ -143,6 +157,7 @@ def list_policies():
+ plugin_policies + plugin_policies
+ injection_plugin_policies + injection_plugin_policies
+ service_policies + service_policies
+ exposed_service_policies
) )
return policies return policies

View File

@ -39,6 +39,26 @@ def get_best_agent(ctx):
return agent.hostname 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): class ConductorEndpoint(object):
def __init__(self, ragent): def __init__(self, ragent):
transport = oslo_messaging.get_transport(cfg.CONF) transport = oslo_messaging.get_transport(cfg.CONF)
@ -119,10 +139,12 @@ class ConductorEndpoint(object):
board_id, board_id,
'destroyBoard', 'destroyBoard',
(p,)) (p,))
except exception: except exception:
return exception return exception
board.destroy() board.destroy()
if result: if result:
result = manage_result(result, 'destroyBoard', board_id)
LOG.debug(result) LOG.debug(result)
return result return result
return return
@ -164,18 +186,7 @@ class ConductorEndpoint(object):
data=wamp_rpc_args) data=wamp_rpc_args)
res = wm.deserialize(res) res = wm.deserialize(res)
if res.result == wm.SUCCESS: return res
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)
def destroy_plugin(self, ctx, plugin_id): def destroy_plugin(self, ctx, plugin_id):
LOG.info('Destroying plugin with id %s', LOG.info('Destroying plugin with id %s',
@ -231,7 +242,9 @@ class ConductorEndpoint(object):
injection = objects.InjectionPlugin(ctx, **inj_data) injection = objects.InjectionPlugin(ctx, **inj_data)
injection.create() injection.create()
result = manage_result(result, 'PluginInject', board_uuid)
LOG.debug(result) LOG.debug(result)
return result return result
def remove_plugin(self, ctx, plugin_uuid, board_uuid): def remove_plugin(self, ctx, plugin_uuid, board_uuid):
@ -247,7 +260,7 @@ class ConductorEndpoint(object):
(plugin.uuid,)) (plugin.uuid,))
except exception: except exception:
return exception return exception
result = manage_result(result, 'PluginRemove', board_uuid)
LOG.debug(result) LOG.debug(result)
injection.destroy() injection.destroy()
return result return result
@ -267,7 +280,7 @@ class ConductorEndpoint(object):
(plugin.uuid,)) (plugin.uuid,))
except exception: except exception:
return exception return exception
result = manage_result(result, action, board_uuid)
LOG.debug(result) LOG.debug(result)
return result return result
@ -290,3 +303,151 @@ class ConductorEndpoint(object):
LOG.debug('Updating service %s', service.name) LOG.debug('Updating service %s', service.name)
service.save() service.save()
return serializer.serialize_entity(ctx, service) 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') cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
return cctxt.call(context, 'update_service', service_obj=service_obj) 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: ServiceAssociated
:raises: ServiceNotFound :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) ref.update(values)
return ref 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')) board_uuid = Column(String(36), ForeignKey('boards.uuid'))
service_uuid = Column(String(36), ForeignKey('services.uuid')) service_uuid = Column(String(36), ForeignKey('services.uuid'))
public_port = Column(Integer) public_port = Column(Integer)
pid = Column(Integer)

View File

@ -14,6 +14,7 @@
from iotronic.objects import board from iotronic.objects import board
from iotronic.objects import conductor from iotronic.objects import conductor
from iotronic.objects import exposedservice
from iotronic.objects import injectionplugin from iotronic.objects import injectionplugin
from iotronic.objects import location from iotronic.objects import location
from iotronic.objects import plugin from iotronic.objects import plugin
@ -26,6 +27,7 @@ Board = board.Board
Location = location.Location Location = location.Location
Plugin = plugin.Plugin Plugin = plugin.Plugin
InjectionPlugin = injectionplugin.InjectionPlugin InjectionPlugin = injectionplugin.InjectionPlugin
ExposedService = exposedservice.ExposedService
SessionWP = sessionwp.SessionWP SessionWP = sessionwp.SessionWP
WampAgent = wampagent.WampAgent WampAgent = wampagent.WampAgent
Service = service.Service Service = service.Service
@ -39,4 +41,5 @@ __all__ = (
Service, Service,
Plugin, Plugin,
InjectionPlugin, 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 base
from iotronic.objects import utils as obj_utils from iotronic.objects import utils as obj_utils
"""
ACTIONS = ['ServiceCall', 'ServiceStop', 'ServiceStart', ACTIONS = ['ServiceEnable', 'ServiceDisable', 'ServiceRestore']
'ServiceStatus', 'ServiceReboot']
CUSTOM_PARAMS = ['ServiceCall', 'ServiceStart', 'ServiceReboot']
NO_PARAMS = ['ServiceStatus']
def is_valid_action(action): def is_valid_action(action):
@ -34,16 +31,6 @@ def is_valid_action(action):
return True 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): class Service(base.IotronicObject):
# Version 1.0: Initial version # Version 1.0: Initial version
VERSION = '1.0' VERSION = '1.0'
@ -154,6 +141,7 @@ class Service(base.IotronicObject):
object, e.g.: Service(context) object, e.g.: Service(context)
""" """
values = self.obj_get_changes() values = self.obj_get_changes()
db_service = self.dbapi.create_service(values) db_service = self.dbapi.create_service(values)
self._from_db_object(self, db_service) 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, `board_uuid` VARCHAR(36) NOT NULL,
`service_uuid` VARCHAR(36) NOT NULL, `service_uuid` VARCHAR(36) NOT NULL,
`public_port` INT(5) NOT NULL, `public_port` INT(5) NOT NULL,
`pid` INT(5) NOT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `board_uuid` (`board_uuid` ASC), INDEX `board_uuid` (`board_uuid` ASC),
CONSTRAINT unique_index
UNIQUE (service_uuid, board_uuid, pid),
CONSTRAINT `fk_board_uuid` CONSTRAINT `fk_board_uuid`
FOREIGN KEY (`board_uuid`) FOREIGN KEY (`board_uuid`)
REFERENCES `iotronic`.`boards` (`uuid`) REFERENCES `iotronic`.`boards` (`uuid`)