From 7f717e995bcab33edf280cb193aad7d08a031a93 Mon Sep 17 00:00:00 2001 From: Fabio Verboso Date: Tue, 21 Feb 2017 14:02:24 +0100 Subject: [PATCH] create, destroy, list and show commands for plugin have been implemented Change-Id: I5a65c940edb676150a83adf62a57cb689fc0591e --- install.sh | 1 + iotronic/api/config.py | 1 + iotronic/api/controllers/v1/__init__.py | 12 ++ iotronic/api/controllers/v1/plugin.py | 168 ++++++++++++++++++++ iotronic/api/controllers/v1/utils.py | 37 ++++- iotronic/common/exception.py | 4 + iotronic/conductor/endpoints.py | 26 +++- iotronic/conductor/rpcapi.py | 46 +++++- iotronic/db/api.py | 71 +++++++-- iotronic/db/sqlalchemy/api.py | 194 ++++++++++++++++++------ iotronic/db/sqlalchemy/models.py | 28 ++++ iotronic/objects/__init__.py | 3 + iotronic/objects/plugin.py | 187 +++++++++++++++++++++++ utils/iotronic.sql | 51 +++++++ utils/iotronic_curl_client | 60 ++++++++ 15 files changed, 827 insertions(+), 62 deletions(-) create mode 100644 iotronic/api/controllers/v1/plugin.py create mode 100644 iotronic/objects/plugin.py create mode 100755 utils/iotronic_curl_client diff --git a/install.sh b/install.sh index 4fb1d6a..453afb8 100755 --- a/install.sh +++ b/install.sh @@ -3,6 +3,7 @@ function build_install { python setup.py build python setup.py install + cp utils/iotronic_curl_client /usr/bin/iotronic } function restart_apache { diff --git a/iotronic/api/config.py b/iotronic/api/config.py index 72649a6..c1f8966 100644 --- a/iotronic/api/config.py +++ b/iotronic/api/config.py @@ -31,6 +31,7 @@ app = { '/', '/v1', '/v1/nodes/[a-z0-9\-]', + '/v1/plugins/[a-z0-9\-]', ], } diff --git a/iotronic/api/controllers/v1/__init__.py b/iotronic/api/controllers/v1/__init__.py index 4fee099..40e7917 100644 --- a/iotronic/api/controllers/v1/__init__.py +++ b/iotronic/api/controllers/v1/__init__.py @@ -19,6 +19,7 @@ Version 1 of the Iotronic API from iotronic.api.controllers import base from iotronic.api.controllers import link from iotronic.api.controllers.v1 import node +from iotronic.api.controllers.v1 import plugin from iotronic.api import expose from iotronic.common.i18n import _ import pecan @@ -52,6 +53,8 @@ class V1(base.APIBase): nodes = [link.Link] """Links to the nodes resource""" + plugins = [link.Link] + @staticmethod def convert(): v1 = V1() @@ -65,6 +68,14 @@ class V1(base.APIBase): bookmark=True) ] + v1.plugins = [link.Link.make_link('self', pecan.request.host_url, + 'plugins', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'plugins', '', + bookmark=True) + ] + ''' v1.links = [link.Link.make_link('self', pecan.request.host_url, 'v1', '', bookmark=True), @@ -82,6 +93,7 @@ class Controller(rest.RestController): """Version 1 API controller root.""" nodes = node.NodesController() + plugins = plugin.PluginsController() @expose.expose(V1) def get(self): diff --git a/iotronic/api/controllers/v1/plugin.py b/iotronic/api/controllers/v1/plugin.py new file mode 100644 index 0000000..87502d2 --- /dev/null +++ b/iotronic/api/controllers/v1/plugin.py @@ -0,0 +1,168 @@ +# 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.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 import objects +import pecan +from pecan import rest +import wsme +from wsme import types as wtypes + + +class Plugin(base.APIBase): + """API representation of a plugin. + + """ + + uuid = types.uuid + name = wsme.wsattr(wtypes.text) + config = wsme.wsattr(wtypes.text) + extra = types.jsontype + + @staticmethod + def _convert(plugin, url, expand=True, show_password=True): + if not expand: + except_list = ['name', 'code', 'status', 'uuid', 'session', 'type'] + plugin.unset_fields_except(except_list) + return plugin + return plugin + + @classmethod + def convert(cls, rpc_plugin, expand=True): + plugin = Plugin(**rpc_plugin.as_dict()) + # plugin.id = rpc_plugin.id + return cls._convert(plugin, pecan.request.host_url, + expand, + pecan.request.context.show_password) + + def __init__(self, **kwargs): + self.fields = [] + fields = list(objects.Plugin.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)) + + +class PluginCollection(collection.Collection): + """API representation of a collection of plugins.""" + + plugins = [Plugin] + """A list containing plugins objects""" + + def __init__(self, **kwargs): + self._type = 'plugins' + + @staticmethod + def convert(plugins, limit, url=None, expand=False, **kwargs): + collection = PluginCollection() + collection.plugins = [ + Plugin.convert( + n, expand) for n in plugins] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class PluginsController(rest.RestController): + invalid_sort_key_list = [] + + def _get_plugins_collection(self, marker, limit, sort_key, sort_dir, + expand=False, resource_url=None): + + limit = api_utils.validate_limit(limit) + sort_dir = api_utils.validate_sort_dir(sort_dir) + + marker_obj = None + if marker: + marker_obj = objects.Plugin.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 = {} + plugins = objects.Plugin.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 PluginCollection.convert(plugins, limit, + url=resource_url, + expand=expand, + **parameters) + + @expose.expose(PluginCollection, types.uuid, int, wtypes.text, wtypes.text) + def get_all(self, marker=None, limit=None, sort_key='id', + sort_dir='asc'): + """Retrieve a list of plugins. + + :param marker: pagination marker for large data sets. + :param limit: maximum number of resources to return in a single result. + :param sort_key: column to sort results by. Default: id. + :param sort_dir: direction to sort. "asc" or "desc". Default: asc. + """ + return self._get_plugins_collection(marker, + limit, sort_key, sort_dir) + + @expose.expose(Plugin, types.uuid_or_name) + def get(self, plugin_ident): + """Retrieve information about the given plugin. + + :param plugin_ident: UUID or logical name of a plugin. + """ + rpc_plugin = api_utils.get_rpc_plugin(plugin_ident) + plugin = Plugin(**rpc_plugin.as_dict()) + plugin.id = rpc_plugin.id + return Plugin.convert(plugin) + + @expose.expose(Plugin, body=Plugin, status_code=201) + def post(self, Plugin): + """Create a new Plugin. + + :param Plugin: a Plugin within the request body. + """ + if not Plugin.name: + raise exception.MissingParameterValue( + ("Name is not specified.")) + + if Plugin.name: + if not api_utils.is_valid_name(Plugin.name): + msg = ("Cannot create plugin with invalid name %(name)s") + raise wsme.exc.ClientSideError(msg % {'name': Plugin.name}, + status_code=400) + + new_Plugin = objects.Plugin(pecan.request.context, + **Plugin.as_dict()) + + new_Plugin = pecan.request.rpcapi.create_plugin(pecan.request.context, + new_Plugin) + return Plugin.convert(new_Plugin) + + @expose.expose(None, types.uuid_or_name, status_code=204) + def delete(self, plugin_ident): + """Delete a plugin. + + :param plugin_ident: UUID or logical name of a plugin. + """ + rpc_plugin = api_utils.get_rpc_plugin(plugin_ident) + pecan.request.rpcapi.destroy_plugin(pecan.request.context, + rpc_plugin.uuid) diff --git a/iotronic/api/controllers/v1/utils.py b/iotronic/api/controllers/v1/utils.py index 7971dd6..a63bb71 100644 --- a/iotronic/api/controllers/v1/utils.py +++ b/iotronic/api/controllers/v1/utils.py @@ -92,10 +92,34 @@ def get_rpc_node(node_ident): raise exception.InvalidUuidOrName(name=node_ident) - # Ensure we raise the same exception as we did for the Juno release raise exception.NodeNotFound(node=node_ident) +def get_rpc_plugin(plugin_ident): + """Get the RPC plugin from the plugin uuid or logical name. + + :param plugin_ident: the UUID or logical name of a plugin. + + :returns: The RPC Plugin. + :raises: InvalidUuidOrName if the name or uuid provided is not valid. + :raises: PluginNotFound if the plugin is not found. + """ + # Check to see if the plugin_ident is a valid UUID. If it is, treat it + # as a UUID. + if uuidutils.is_uuid_like(plugin_ident): + return objects.Plugin.get_by_uuid(pecan.request.context, plugin_ident) + + # We can refer to plugins by their name, if the client supports it + # if allow_plugin_logical_names(): + # if utils.is_hostname_safe(plugin_ident): + else: + return objects.Plugin.get_by_name(pecan.request.context, plugin_ident) + + raise exception.InvalidUuidOrName(name=plugin_ident) + + raise exception.PluginNotFound(plugin=plugin_ident) + + def is_valid_node_name(name): """Determine if the provided name is a valid node name. @@ -105,3 +129,14 @@ def is_valid_node_name(name): :returns: True if the name is valid, False otherwise. """ return utils.is_hostname_safe(name) and (not uuidutils.is_uuid_like(name)) + + +def is_valid_name(name): + """Determine if the provided name is a valid name. + + Check to see that the provided node name isn't a UUID. + + :param: name: the node name to check. + :returns: True if the name is valid, False otherwise. + """ + return not uuidutils.is_uuid_like(name) diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py index 2ffb0b3..a76af14 100644 --- a/iotronic/common/exception.py +++ b/iotronic/common/exception.py @@ -578,3 +578,7 @@ class PathNotFound(IotronicException): class DirectoryNotWritable(IotronicException): message = _("Directory %(dir)s is not writable.") + + +class PluginNotFound(NotFound): + message = _("Plugin %(plugin)s could not be found.") diff --git a/iotronic/conductor/endpoints.py b/iotronic/conductor/endpoints.py index 2f1105f..3c10ca0 100644 --- a/iotronic/conductor/endpoints.py +++ b/iotronic/conductor/endpoints.py @@ -120,7 +120,11 @@ class ConductorEndpoint(object): prov.conf_clean() p = prov.get_config() LOG.debug('sending this conf %s', p) - self.execute_on_node(ctx, node_id, 'destroyNode', (p,)) + try: + self.execute_on_node(ctx, node_id, 'destroyNode', (p,)) + except Exception: + LOG.error('cannot execute remote destroynode on %s. ' + 'Maybe it is OFFLINE', node_id) node.destroy() @@ -161,3 +165,23 @@ class ConductorEndpoint(object): return self.wamp_agent_client.call(ctx, full_topic, wamp_rpc_call=full_wamp_call, data=wamp_rpc_args) + + def destroy_plugin(self, ctx, plugin_id): + LOG.info('Destroying plugin with id %s', + plugin_id) + plugin = objects.Plugin.get_by_uuid(ctx, plugin_id) + plugin.destroy() + return + + def update_plugin(self, ctx, plugin_obj): + plugin = serializer.deserialize_entity(ctx, plugin_obj) + LOG.debug('Updating plugin %s', plugin.name) + plugin.save() + return serializer.serialize_entity(ctx, plugin) + + def create_plugin(self, ctx, plugin_obj): + new_plugin = serializer.deserialize_entity(ctx, plugin_obj) + LOG.debug('Creating plugin %s', + new_plugin.name) + new_plugin.create() + return serializer.serialize_entity(ctx, new_plugin) diff --git a/iotronic/conductor/rpcapi.py b/iotronic/conductor/rpcapi.py index 6157b4d..d53fdb6 100644 --- a/iotronic/conductor/rpcapi.py +++ b/iotronic/conductor/rpcapi.py @@ -93,10 +93,6 @@ class ConductorAPI(object): """Synchronously, have a conductor update the node's information. Update the node's information in the database and return a node object. - The conductor will lock the node while it validates the supplied - information. If driver_info is passed, it will be validated by - the core drivers. If instance_uuid is passed, it will be set or unset - only if the node is properly configured. Note that power_state should not be passed via this method. Use change_node_power_state for initiating driver actions. @@ -130,3 +126,45 @@ class ConductorAPI(object): return cctxt.call(context, 'execute_on_node', node_uuid=node_uuid, wamp_rpc_call=wamp_rpc_call, wamp_rpc_args=wamp_rpc_args) + + def create_plugin(self, context, plugin_obj, topic=None): + """Add a plugin on the cloud + + :param context: request context. + :param plugin_obj: a changed (but not saved) plugin object. + :param topic: RPC topic. Defaults to self.topic. + :returns: created plugin object + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'create_plugin', + plugin_obj=plugin_obj) + + def update_plugin(self, context, plugin_obj, topic=None): + """Synchronously, have a conductor update the plugin's information. + + Update the plugin's information in the database and + return a plugin object. + + :param context: request context. + :param plugin_obj: a changed (but not saved) plugin object. + :param topic: RPC topic. Defaults to self.topic. + :returns: updated plugin object, including all fields. + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'update_plugin', plugin_obj=plugin_obj) + + def destroy_plugin(self, context, plugin_id, topic=None): + """Delete a plugin. + + :param context: request context. + :param plugin_id: plugin id or uuid. + :raises: PluginLocked if plugin is locked by another conductor. + :raises: PluginAssociated if the plugin contains an instance + associated with it. + :raises: InvalidState if the plugin 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_plugin', plugin_id=plugin_id) diff --git a/iotronic/db/api.py b/iotronic/db/api.py index b12a27d..07859e1 100644 --- a/iotronic/db/api.py +++ b/iotronic/db/api.py @@ -204,6 +204,20 @@ class Connection(object): :returns: A session. """ + @abc.abstractmethod + def get_session_by_node_uuid(self, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + """Return a Wamp session of a Node + + :param filters: Filters to apply. Defaults to None. + :param limit: Maximum number of wampagents to return. + :param marker: the last item of the previous page; we return the next + result set. + :param sort_key: Attribute by which results should be sorted. + :param sort_dir: direction in which results should be sorted. + (asc, desc) + """ + @abc.abstractmethod def create_location(self, values): """Create a new location. @@ -290,15 +304,52 @@ class Connection(object): """ @abc.abstractmethod - def get_session_by_node_uuid(self, filters=None, limit=None, marker=None, - sort_key=None, sort_dir=None): - """Return a Wamp session of a Node + def get_plugin_by_id(self, plugin_id): + """Return a plugin. - :param filters: Filters to apply. Defaults to None. - :param limit: Maximum number of wampagents to return. - :param marker: the last item of the previous page; we return the next - result set. - :param sort_key: Attribute by which results should be sorted. - :param sort_dir: direction in which results should be sorted. - (asc, desc) + :param plugin_id: The id of a plugin. + :returns: A plugin. + """ + + @abc.abstractmethod + def get_plugin_by_uuid(self, plugin_uuid): + """Return a plugin. + + :param plugin_uuid: The uuid of a plugin. + :returns: A plugin. + """ + + @abc.abstractmethod + def get_plugin_by_name(self, plugin_name): + """Return a plugin. + + :param plugin_name: The logical name of a plugin. + :returns: A plugin. + """ + + @abc.abstractmethod + def create_plugin(self, values): + """Create a new plugin. + + :param values: A dict containing several items used to identify + and track the plugin + :returns: A plugin. + """ + + @abc.abstractmethod + def destroy_plugin(self, plugin_id): + """Destroy a plugin and all associated interfaces. + + :param plugin_id: The id or uuid of a plugin. + """ + + @abc.abstractmethod + def update_plugin(self, plugin_id, values): + """Update properties of a plugin. + + :param plugin_id: The id or uuid of a plugin. + :param values: Dict of values to update. + :returns: A plugin. + :raises: PluginAssociated + :raises: PluginNotFound """ diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py index e0727e4..95b78af 100644 --- a/iotronic/db/sqlalchemy/api.py +++ b/iotronic/db/sqlalchemy/api.py @@ -108,21 +108,20 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None, return query.all() -def add_location_filter_by_node(query, value): - if strutils.is_int_like(value): - return query.filter_by(node_id=value) - else: - query = query.join(models.Node, - models.Location.node_id == models.Node.id) - return query.filter(models.Node.uuid == value) - - class Connection(api.Connection): """SqlAlchemy connection.""" def __init__(self): pass + def _add_location_filter_by_node(self, query, value): + if strutils.is_int_like(value): + return query.filter_by(node_id=value) + else: + query = query.join(models.Node, + models.Location.node_id == models.Node.id) + return query.filter(models.Node.uuid == value) + def _add_nodes_filters(self, query, filters): if filters is None: filters = [] @@ -135,6 +134,13 @@ class Connection(api.Connection): return query + def _add_plugins_filters(self, query, filters): + if filters is None: + filters = [] + # TBD + + return query + def _add_wampagents_filters(self, query, filters): if filters is None: filters = [] @@ -145,8 +151,34 @@ class Connection(api.Connection): else: query = query.filter(models.WampAgent.online == 0) + if 'no_ragent' in filters: + if filters['no_ragent']: + query = query.filter(models.WampAgent.ragent == 0) + else: + query = query.filter(models.WampAgent.ragent == 1) + return query + def _do_update_node(self, node_id, values): + session = get_session() + with session.begin(): + query = model_query(models.Node, session=session) + query = add_identity_filter(query, node_id) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.NodeNotFound(node=node_id) + + # Prevent instance_uuid overwriting + if values.get("instance_uuid") and ref.instance_uuid: + raise exception.NodeAssociated( + node=node_id, instance=ref.instance_uuid) + + ref.update(values) + return ref + + # NODE api + def get_nodeinfo_list(self, columns=None, filters=None, limit=None, marker=None, sort_key=None, sort_dir=None): # list-ify columns default values because it is bad form @@ -182,7 +214,7 @@ class Connection(api.Connection): except db_exc.DBDuplicateEntry as exc: if 'code' in exc.columns: raise exception.DuplicateCode(code=values['code']) - raise exception.BoardAlreadyExists(uuid=values['uuid']) + raise exception.NodeAlreadyExists(uuid=values['uuid']) return node def get_node_by_id(self, node_id): @@ -230,7 +262,7 @@ class Connection(api.Connection): node_id = node_ref['id'] location_query = model_query(models.Location, session=session) - location_query = add_location_filter_by_node( + location_query = self._add_location_filter_by_node( location_query, node_id) location_query.delete() @@ -256,23 +288,7 @@ class Connection(api.Connection): else: raise e - def _do_update_node(self, node_id, values): - session = get_session() - with session.begin(): - query = model_query(models.Node, session=session) - query = add_identity_filter(query, node_id) - try: - ref = query.with_lockmode('update').one() - except NoResultFound: - raise exception.NodeNotFound(node=node_id) - - # Prevent instance_uuid overwriting - if values.get("instance_uuid") and ref.instance_uuid: - raise exception.NodeAssociated( - node=node_id, instance=ref.instance_uuid) - - ref.update(values) - return ref + # CONDUCTOR api def register_conductor(self, values, update_existing=False): session = get_session() @@ -323,24 +339,7 @@ class Connection(api.Connection): if count == 0: raise exception.ConductorNotFound(conductor=hostname) - def create_session(self, values): - session = models.SessionWP() - session.update(values) - session.save() - return session - - def update_session(self, ses_id, values): - # NOTE(dtantsur): this can lead to very strange errors - session = get_session() - try: - with session.begin(): - query = model_query(models.SessionWP, session=session) - query = add_identity_filter(query, ses_id) - ref = query.one() - ref.update(values) - except NoResultFound: - raise exception.SessionWPNotFound(ses=ses_id) - return ref + # LOCATION api def create_location(self, values): location = models.Location() @@ -377,6 +376,27 @@ class Connection(api.Connection): return _paginate_query(models.Location, limit, marker, sort_key, sort_dir, query) + # SESSION api + + def create_session(self, values): + session = models.SessionWP() + session.update(values) + session.save() + return session + + def update_session(self, ses_id, values): + # NOTE(dtantsur): this can lead to very strange errors + session = get_session() + try: + with session.begin(): + query = model_query(models.SessionWP, session=session) + query = add_identity_filter(query, ses_id) + ref = query.one() + ref.update(values) + except NoResultFound: + raise exception.SessionWPNotFound(ses=ses_id) + return ref + def get_session_by_node_uuid(self, node_uuid, valid): query = model_query( models.SessionWP).filter_by( @@ -394,6 +414,8 @@ class Connection(api.Connection): except NoResultFound: return None + # WAMPAGENT api + def register_wampagent(self, values, update_existing=False): session = get_session() with session.begin(): @@ -457,3 +479,83 @@ class Connection(api.Connection): query = self._add_wampagents_filters(query, filters) return _paginate_query(models.WampAgent, limit, marker, sort_key, sort_dir, query) + + # PLUGIN api + + def get_plugin_by_id(self, plugin_id): + query = model_query(models.Plugin).filter_by(id=plugin_id) + try: + return query.one() + except NoResultFound: + raise exception.PluginNotFound(plugin=plugin_id) + + def get_plugin_by_uuid(self, plugin_uuid): + query = model_query(models.Plugin).filter_by(uuid=plugin_uuid) + try: + return query.one() + except NoResultFound: + raise exception.PluginNotFound(plugin=plugin_uuid) + + def get_plugin_by_name(self, plugin_name): + query = model_query(models.Plugin).filter_by(name=plugin_name) + try: + return query.one() + except NoResultFound: + raise exception.PluginNotFound(plugin=plugin_name) + + def destroy_plugin(self, plugin_id): + + session = get_session() + with session.begin(): + query = model_query(models.Plugin, session=session) + query = add_identity_filter(query, plugin_id) + try: + plugin_ref = query.one() + except NoResultFound: + raise exception.PluginNotFound(plugin=plugin_id) + + # Get plugin ID, if an UUID was supplied. The ID is + # required for deleting all ports, attached to the plugin. + if uuidutils.is_uuid_like(plugin_id): + plugin_id = plugin_ref['id'] + + query.delete() + + def update_plugin(self, plugin_id, values): + # NOTE(dtantsur): this can lead to very strange errors + if 'uuid' in values: + msg = _("Cannot overwrite UUID for an existing Plugin.") + raise exception.InvalidParameterValue(err=msg) + + try: + return self._do_update_plugin(plugin_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.PluginAlreadyExists(uuid=values['uuid']) + elif 'instance_uuid' in e.columns: + raise exception.InstanceAssociated( + instance_uuid=values['instance_uuid'], + plugin=plugin_id) + else: + raise e + + def create_plugin(self, values): + # ensure defaults are present for new plugins + if 'uuid' not in values: + values['uuid'] = uuidutils.generate_uuid() + plugin = models.Plugin() + plugin.update(values) + try: + plugin.save() + except db_exc.DBDuplicateEntry: + raise exception.PluginAlreadyExists(uuid=values['uuid']) + return plugin + + def get_plugin_list(self, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.Plugin) + query = self._add_plugins_filters(query, filters) + return _paginate_query(models.Plugin, limit, marker, + sort_key, sort_dir, query) diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py index 0b0d991..6f78d49 100644 --- a/iotronic/db/sqlalchemy/models.py +++ b/iotronic/db/sqlalchemy/models.py @@ -190,3 +190,31 @@ class SessionWP(Base): session_id = Column(String(15)) node_uuid = Column(String(36)) node_id = Column(Integer, ForeignKey('nodes.id')) + + +class Plugin(Base): + """Represents a plugin.""" + + __tablename__ = 'plugins' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_plugins0uuid'), + table_args()) + id = Column(Integer, primary_key=True) + uuid = Column(String(36)) + name = Column(String(36)) + config = Column(TEXT) + extra = Column(JSONEncodedDict) + + +class Injected_Plugin(Base): + """Represents an plugin injection on board.""" + + __tablename__ = 'injected_plugins' + __table_args__ = ( + table_args()) + id = Column(Integer, primary_key=True) + node_uuid = Column(String(36)) + node_id = Column(Integer, ForeignKey('nodes.id')) + plugin_uuid = Column(String(36)) + plugin_id = Column(Integer, ForeignKey('plugins.id')) + status = Column(String(15)) diff --git a/iotronic/objects/__init__.py b/iotronic/objects/__init__.py index bf0a431..476365c 100644 --- a/iotronic/objects/__init__.py +++ b/iotronic/objects/__init__.py @@ -15,12 +15,14 @@ from iotronic.objects import conductor from iotronic.objects import location from iotronic.objects import node +from iotronic.objects import plugin from iotronic.objects import sessionwp from iotronic.objects import wampagent Conductor = conductor.Conductor Node = node.Node Location = location.Location +Plugin = plugin.Plugin SessionWP = sessionwp.SessionWP WampAgent = wampagent.WampAgent @@ -30,4 +32,5 @@ __all__ = ( Location, SessionWP, WampAgent, + Plugin, ) diff --git a/iotronic/objects/plugin.py b/iotronic/objects/plugin.py new file mode 100644 index 0000000..c9c4374 --- /dev/null +++ b/iotronic/objects/plugin.py @@ -0,0 +1,187 @@ +# 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 + + +class Plugin(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, + 'config': obj_utils.str_or_none, + 'extra': obj_utils.dict_or_none, + } + + @staticmethod + def _from_db_object(plugin, db_plugin): + """Converts a database entity to a formal object.""" + for field in plugin.fields: + plugin[field] = db_plugin[field] + plugin.obj_reset_changes() + return plugin + + @base.remotable_classmethod + def get(cls, context, plugin_id): + """Find a plugin based on its id or uuid and return a Node object. + + :param plugin_id: the id *or* uuid of a plugin. + :returns: a :class:`Node` object. + """ + if strutils.is_int_like(plugin_id): + return cls.get_by_id(context, plugin_id) + elif uuidutils.is_uuid_like(plugin_id): + return cls.get_by_uuid(context, plugin_id) + else: + raise exception.InvalidIdentity(identity=plugin_id) + + @base.remotable_classmethod + def get_by_id(cls, context, plugin_id): + """Find a plugin based on its integer id and return a Node object. + + :param plugin_id: the id of a plugin. + :returns: a :class:`Node` object. + """ + db_plugin = cls.dbapi.get_plugin_by_id(plugin_id) + plugin = Plugin._from_db_object(cls(context), db_plugin) + return plugin + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + """Find a plugin based on uuid and return a Node object. + + :param uuid: the uuid of a plugin. + :returns: a :class:`Node` object. + """ + db_plugin = cls.dbapi.get_plugin_by_uuid(uuid) + plugin = Plugin._from_db_object(cls(context), db_plugin) + return plugin + + @base.remotable_classmethod + def get_by_name(cls, context, name): + """Find a plugin based on name and return a Node object. + + :param name: the logical name of a plugin. + :returns: a :class:`Node` object. + """ + db_plugin = cls.dbapi.get_plugin_by_name(name) + plugin = Plugin._from_db_object(cls(context), db_plugin) + return plugin + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, sort_key=None, + sort_dir=None, filters=None): + """Return a list of Plugin 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:`Plugin` object. + + """ + db_plugins = cls.dbapi.get_plugin_list(filters=filters, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return [Plugin._from_db_object(cls(context), obj) + for obj in db_plugins] + + @base.remotable + def create(self, context=None): + """Create a Plugin 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 + plugin 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.: Plugin(context) + + """ + values = self.obj_get_changes() + db_plugin = self.dbapi.create_plugin(values) + self._from_db_object(self, db_plugin) + + @base.remotable + def destroy(self, context=None): + """Delete the Plugin 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.: Plugin(context) + """ + self.dbapi.destroy_plugin(self.uuid) + self.obj_reset_changes() + + @base.remotable + def save(self, context=None): + """Save updates to this Plugin. + + 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 + plugin 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.: Plugin(context) + """ + updates = self.obj_get_changes() + self.dbapi.update_plugin(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.: Plugin(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 8bd8be8..e86dfca 100644 --- a/utils/iotronic.sql +++ b/utils/iotronic.sql @@ -132,6 +132,57 @@ ENGINE = InnoDB AUTO_INCREMENT = 10 DEFAULT CHARACTER SET = utf8; +-- ----------------------------------------------------- +-- Table `iotronic`.`plugins` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `iotronic`.`plugins` ; + +CREATE TABLE IF NOT EXISTS `iotronic`.`plugins` ( + `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, + `config` TEXT NULL DEFAULT NULL, + `extra` TEXT NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `uuid` (`uuid` ASC)) +ENGINE = InnoDB +AUTO_INCREMENT = 132 +DEFAULT CHARACTER SET = utf8; + +-- ----------------------------------------------------- +-- Table `iotronic`.`injected_plugins` +-- ----------------------------------------------------- +DROP TABLE IF EXISTS `iotronic`.`injected_plugins` ; + +CREATE TABLE IF NOT EXISTS `iotronic`.`injected_plugins` ( + `created_at` DATETIME NULL DEFAULT NULL, + `updated_at` DATETIME NULL DEFAULT NULL, + `id` INT(11) NOT NULL AUTO_INCREMENT, + `node_uuid` VARCHAR(36) NOT NULL, + `node_id` INT(11) NOT NULL, + `plugin_uuid` VARCHAR(36) NOT NULL, + `plugin_id` INT(11) NOT NULL, + `status` VARCHAR(15) NOT NULL DEFAULT 'injected', + PRIMARY KEY (`id`), + INDEX `node_id` (`node_id` ASC), + CONSTRAINT `node_id` + FOREIGN KEY (`node_id`) + REFERENCES `iotronic`.`nodes` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + INDEX `plugin_id` (`plugin_id` ASC), + CONSTRAINT `plugin_id` + FOREIGN KEY (`plugin_id`) + REFERENCES `iotronic`.`plugins` (`id`) + 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; diff --git a/utils/iotronic_curl_client b/utils/iotronic_curl_client new file mode 100755 index 0000000..ca2a00a --- /dev/null +++ b/utils/iotronic_curl_client @@ -0,0 +1,60 @@ +#!/bin/bash + +HOST='localhost' +PORT='1288' +VERSION='v1' +BASE=http://$HOST:$PORT/$VERSION + +function node_manager() { + case "$1" in + list) curl -sS $BASE/nodes/ | python -m json.tool + echo ""; + ;; + create) curl -sS -H "Content-Type: application/json" -X POST $BASE/nodes/ \ + -d '{"type":"'"$7"'","code":"'"$2"'","name":"'"$3"'","location":[{"latitude":"'"$4"'","longitude":"'"$5"'","altitude":"'"$6"'"}]}' | python -m json.tool + echo ""; + ;; + delete) curl -sS -X DELETE $BASE/nodes/$2 | python -m json.tool + echo ""; + ;; + show) curl -sS $BASE/nodes/$2 | python -m json.tool + echo ""; + ;; + *) echo "node list|create|delete|show" + esac +} + +function plugin_manager() { + case "$1" in + list) curl -sS $BASE/plugins/ | python -m json.tool + echo ""; + ;; + create) echo "TBI" + echo ""; + ;; + delete) echo "TBI" + echo ""; + ;; + show) curl -sS $BASE/plugins/$2 | python -m json.tool + echo ""; + ;; + *) echo "plugin list|create|delete|show" + esac +} + +if [ $# -lt 1 ] +then +echo "USAGE: iotronic node|plugin [OPTIONS]" +exit +fi + +case "$1" in +node) node_manager "${@:2}"; +echo ""; +;; +plugin) plugin_manager "${@:2}" +echo ""; +;; +*) echo "USAGE: iotronic node|plugin [OPTIONS]" +esac +