create, destroy, list and show commands for plugin have been implemented

Change-Id: I5a65c940edb676150a83adf62a57cb689fc0591e
This commit is contained in:
Fabio Verboso 2017-02-21 14:02:24 +01:00
parent ac70869ee9
commit 7f717e995b
15 changed files with 827 additions and 62 deletions

View File

@ -3,6 +3,7 @@
function build_install { function build_install {
python setup.py build python setup.py build
python setup.py install python setup.py install
cp utils/iotronic_curl_client /usr/bin/iotronic
} }
function restart_apache { function restart_apache {

View File

@ -31,6 +31,7 @@ app = {
'/', '/',
'/v1', '/v1',
'/v1/nodes/[a-z0-9\-]', '/v1/nodes/[a-z0-9\-]',
'/v1/plugins/[a-z0-9\-]',
], ],
} }

View File

@ -19,6 +19,7 @@ Version 1 of the Iotronic API
from iotronic.api.controllers import base from iotronic.api.controllers import base
from iotronic.api.controllers import link from iotronic.api.controllers import link
from iotronic.api.controllers.v1 import node from iotronic.api.controllers.v1 import node
from iotronic.api.controllers.v1 import plugin
from iotronic.api import expose from iotronic.api import expose
from iotronic.common.i18n import _ from iotronic.common.i18n import _
import pecan import pecan
@ -52,6 +53,8 @@ class V1(base.APIBase):
nodes = [link.Link] nodes = [link.Link]
"""Links to the nodes resource""" """Links to the nodes resource"""
plugins = [link.Link]
@staticmethod @staticmethod
def convert(): def convert():
v1 = V1() v1 = V1()
@ -65,6 +68,14 @@ class V1(base.APIBase):
bookmark=True) 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.links = [link.Link.make_link('self', pecan.request.host_url,
'v1', '', bookmark=True), 'v1', '', bookmark=True),
@ -82,6 +93,7 @@ class Controller(rest.RestController):
"""Version 1 API controller root.""" """Version 1 API controller root."""
nodes = node.NodesController() nodes = node.NodesController()
plugins = plugin.PluginsController()
@expose.expose(V1) @expose.expose(V1)
def get(self): def get(self):

View File

@ -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)

View File

@ -92,10 +92,34 @@ def get_rpc_node(node_ident):
raise exception.InvalidUuidOrName(name=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) 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): def is_valid_node_name(name):
"""Determine if the provided name is a valid node 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. :returns: True if the name is valid, False otherwise.
""" """
return utils.is_hostname_safe(name) and (not uuidutils.is_uuid_like(name)) 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)

View File

@ -578,3 +578,7 @@ class PathNotFound(IotronicException):
class DirectoryNotWritable(IotronicException): class DirectoryNotWritable(IotronicException):
message = _("Directory %(dir)s is not writable.") message = _("Directory %(dir)s is not writable.")
class PluginNotFound(NotFound):
message = _("Plugin %(plugin)s could not be found.")

View File

@ -120,7 +120,11 @@ class ConductorEndpoint(object):
prov.conf_clean() prov.conf_clean()
p = prov.get_config() p = prov.get_config()
LOG.debug('sending this conf %s', p) LOG.debug('sending this conf %s', p)
try:
self.execute_on_node(ctx, node_id, 'destroyNode', (p,)) 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() node.destroy()
@ -161,3 +165,23 @@ class ConductorEndpoint(object):
return self.wamp_agent_client.call(ctx, full_topic, return self.wamp_agent_client.call(ctx, full_topic,
wamp_rpc_call=full_wamp_call, wamp_rpc_call=full_wamp_call,
data=wamp_rpc_args) 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)

View File

@ -93,10 +93,6 @@ class ConductorAPI(object):
"""Synchronously, have a conductor update the node's information. """Synchronously, have a conductor update the node's information.
Update the node's information in the database and return a node object. 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. Note that power_state should not be passed via this method.
Use change_node_power_state for initiating driver actions. 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, return cctxt.call(context, 'execute_on_node', node_uuid=node_uuid,
wamp_rpc_call=wamp_rpc_call, wamp_rpc_call=wamp_rpc_call,
wamp_rpc_args=wamp_rpc_args) 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)

View File

@ -204,6 +204,20 @@ class Connection(object):
:returns: A session. :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 @abc.abstractmethod
def create_location(self, values): def create_location(self, values):
"""Create a new location. """Create a new location.
@ -290,15 +304,52 @@ class Connection(object):
""" """
@abc.abstractmethod @abc.abstractmethod
def get_session_by_node_uuid(self, filters=None, limit=None, marker=None, def get_plugin_by_id(self, plugin_id):
sort_key=None, sort_dir=None): """Return a plugin.
"""Return a Wamp session of a Node
:param filters: Filters to apply. Defaults to None. :param plugin_id: The id of a plugin.
:param limit: Maximum number of wampagents to return. :returns: A plugin.
: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. @abc.abstractmethod
:param sort_dir: direction in which results should be sorted. def get_plugin_by_uuid(self, plugin_uuid):
(asc, desc) """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
""" """

View File

@ -108,7 +108,13 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None,
return query.all() return query.all()
def add_location_filter_by_node(query, 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): if strutils.is_int_like(value):
return query.filter_by(node_id=value) return query.filter_by(node_id=value)
else: else:
@ -116,13 +122,6 @@ def add_location_filter_by_node(query, value):
models.Location.node_id == models.Node.id) models.Location.node_id == models.Node.id)
return query.filter(models.Node.uuid == value) return query.filter(models.Node.uuid == value)
class Connection(api.Connection):
"""SqlAlchemy connection."""
def __init__(self):
pass
def _add_nodes_filters(self, query, filters): def _add_nodes_filters(self, query, filters):
if filters is None: if filters is None:
filters = [] filters = []
@ -135,6 +134,13 @@ class Connection(api.Connection):
return query return query
def _add_plugins_filters(self, query, filters):
if filters is None:
filters = []
# TBD
return query
def _add_wampagents_filters(self, query, filters): def _add_wampagents_filters(self, query, filters):
if filters is None: if filters is None:
filters = [] filters = []
@ -145,8 +151,34 @@ class Connection(api.Connection):
else: else:
query = query.filter(models.WampAgent.online == 0) 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 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, def get_nodeinfo_list(self, columns=None, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None): marker=None, sort_key=None, sort_dir=None):
# list-ify columns default values because it is bad form # list-ify columns default values because it is bad form
@ -182,7 +214,7 @@ class Connection(api.Connection):
except db_exc.DBDuplicateEntry as exc: except db_exc.DBDuplicateEntry as exc:
if 'code' in exc.columns: if 'code' in exc.columns:
raise exception.DuplicateCode(code=values['code']) raise exception.DuplicateCode(code=values['code'])
raise exception.BoardAlreadyExists(uuid=values['uuid']) raise exception.NodeAlreadyExists(uuid=values['uuid'])
return node return node
def get_node_by_id(self, node_id): def get_node_by_id(self, node_id):
@ -230,7 +262,7 @@ class Connection(api.Connection):
node_id = node_ref['id'] node_id = node_ref['id']
location_query = model_query(models.Location, session=session) 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, node_id)
location_query.delete() location_query.delete()
@ -256,23 +288,7 @@ class Connection(api.Connection):
else: else:
raise e raise e
def _do_update_node(self, node_id, values): # CONDUCTOR api
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
def register_conductor(self, values, update_existing=False): def register_conductor(self, values, update_existing=False):
session = get_session() session = get_session()
@ -323,24 +339,7 @@ class Connection(api.Connection):
if count == 0: if count == 0:
raise exception.ConductorNotFound(conductor=hostname) raise exception.ConductorNotFound(conductor=hostname)
def create_session(self, values): # LOCATION api
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 create_location(self, values): def create_location(self, values):
location = models.Location() location = models.Location()
@ -377,6 +376,27 @@ class Connection(api.Connection):
return _paginate_query(models.Location, limit, marker, return _paginate_query(models.Location, limit, marker,
sort_key, sort_dir, query) 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): def get_session_by_node_uuid(self, node_uuid, valid):
query = model_query( query = model_query(
models.SessionWP).filter_by( models.SessionWP).filter_by(
@ -394,6 +414,8 @@ class Connection(api.Connection):
except NoResultFound: except NoResultFound:
return None return None
# WAMPAGENT api
def register_wampagent(self, values, update_existing=False): def register_wampagent(self, values, update_existing=False):
session = get_session() session = get_session()
with session.begin(): with session.begin():
@ -457,3 +479,83 @@ class Connection(api.Connection):
query = self._add_wampagents_filters(query, filters) query = self._add_wampagents_filters(query, filters)
return _paginate_query(models.WampAgent, limit, marker, return _paginate_query(models.WampAgent, limit, marker,
sort_key, sort_dir, query) 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)

View File

@ -190,3 +190,31 @@ class SessionWP(Base):
session_id = Column(String(15)) session_id = Column(String(15))
node_uuid = Column(String(36)) node_uuid = Column(String(36))
node_id = Column(Integer, ForeignKey('nodes.id')) 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))

View File

@ -15,12 +15,14 @@
from iotronic.objects import conductor from iotronic.objects import conductor
from iotronic.objects import location from iotronic.objects import location
from iotronic.objects import node from iotronic.objects import node
from iotronic.objects import plugin
from iotronic.objects import sessionwp from iotronic.objects import sessionwp
from iotronic.objects import wampagent from iotronic.objects import wampagent
Conductor = conductor.Conductor Conductor = conductor.Conductor
Node = node.Node Node = node.Node
Location = location.Location Location = location.Location
Plugin = plugin.Plugin
SessionWP = sessionwp.SessionWP SessionWP = sessionwp.SessionWP
WampAgent = wampagent.WampAgent WampAgent = wampagent.WampAgent
@ -30,4 +32,5 @@ __all__ = (
Location, Location,
SessionWP, SessionWP,
WampAgent, WampAgent,
Plugin,
) )

187
iotronic/objects/plugin.py Normal file
View File

@ -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]

View File

@ -132,6 +132,57 @@ ENGINE = InnoDB
AUTO_INCREMENT = 10 AUTO_INCREMENT = 10
DEFAULT CHARACTER SET = utf8; 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 SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

60
utils/iotronic_curl_client Executable file
View File

@ -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