From 6e9e02e9c3e014bb2f40323b12123c004873d48b Mon Sep 17 00:00:00 2001 From: Fabio Verboso Date: Mon, 12 Feb 2018 12:15:09 +0100 Subject: [PATCH] Cloud Services development Starting to manage cloud services for the board. Change-Id: I09cb167ec356ea08101bc3d4621f7fa73a18ec1c --- iotronic/api/controllers/v1/__init__.py | 16 ++ iotronic/api/controllers/v1/service.py | 362 ++++++++++++++++++++++++ iotronic/api/controllers/v1/utils.py | 27 ++ iotronic/common/exception.py | 4 + iotronic/common/policy.py | 17 ++ iotronic/conductor/endpoints.py | 20 ++ iotronic/conductor/rpcapi.py | 42 +++ iotronic/db/api.py | 52 +++- iotronic/db/sqlalchemy/api.py | 97 +++++++ iotronic/db/sqlalchemy/models.py | 28 ++ iotronic/objects/__init__.py | 3 + iotronic/objects/service.py | 211 ++++++++++++++ utils/iotronic.sql | 37 ++- 13 files changed, 895 insertions(+), 21 deletions(-) create mode 100644 iotronic/api/controllers/v1/service.py create mode 100644 iotronic/objects/service.py diff --git a/iotronic/api/controllers/v1/__init__.py b/iotronic/api/controllers/v1/__init__.py index c1b53bc..ca43ea0 100644 --- a/iotronic/api/controllers/v1/__init__.py +++ b/iotronic/api/controllers/v1/__init__.py @@ -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): diff --git a/iotronic/api/controllers/v1/service.py b/iotronic/api/controllers/v1/service.py new file mode 100644 index 0000000..a69dba2 --- /dev/null +++ b/iotronic/api/controllers/v1/service.py @@ -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) diff --git a/iotronic/api/controllers/v1/utils.py b/iotronic/api/controllers/v1/utils.py index 44aac60..fac0fce 100644 --- a/iotronic/api/controllers/v1/utils.py +++ b/iotronic/api/controllers/v1/utils.py @@ -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. diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py index 7c33126..f786645 100644 --- a/iotronic/common/exception.py +++ b/iotronic/common/exception.py @@ -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.") diff --git a/iotronic/common/policy.py b/iotronic/common/policy.py index 45bba91..5fff673 100644 --- a/iotronic/common/policy.py +++ b/iotronic/common/policy.py @@ -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 diff --git a/iotronic/conductor/endpoints.py b/iotronic/conductor/endpoints.py index 3ff35d7..73226c8 100644 --- a/iotronic/conductor/endpoints.py +++ b/iotronic/conductor/endpoints.py @@ -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) diff --git a/iotronic/conductor/rpcapi.py b/iotronic/conductor/rpcapi.py index e839de5..4d5b333 100644 --- a/iotronic/conductor/rpcapi.py +++ b/iotronic/conductor/rpcapi.py @@ -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) diff --git a/iotronic/db/api.py b/iotronic/db/api.py index 68686a0..51c3e6a 100644 --- a/iotronic/db/api.py +++ b/iotronic/db/api.py @@ -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 """ diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py index 2b966ac..50132ca 100644 --- a/iotronic/db/sqlalchemy/api.py +++ b/iotronic/db/sqlalchemy/api.py @@ -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 diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py index ae4a837..5fd78ed 100644 --- a/iotronic/db/sqlalchemy/models.py +++ b/iotronic/db/sqlalchemy/models.py @@ -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) diff --git a/iotronic/objects/__init__.py b/iotronic/objects/__init__.py index 4387e2b..2cfbf31 100644 --- a/iotronic/objects/__init__.py +++ b/iotronic/objects/__init__.py @@ -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, ) diff --git a/iotronic/objects/service.py b/iotronic/objects/service.py new file mode 100644 index 0000000..b894e4b --- /dev/null +++ b/iotronic/objects/service.py @@ -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] diff --git a/utils/iotronic.sql b/utils/iotronic.sql index 7fa135e..180a254 100644 --- a/utils/iotronic.sql +++ b/utils/iotronic.sql @@ -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;