Skip to content
plugins.py 4.37 KiB
Newer Older
# This file is part of CloudControl.
#
# CloudControl is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CloudControl is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CloudControl.  If not, see <http://www.gnu.org/licenses/>.


import imp
import logging
from collections import defaultdict

from cloudcontrol.common.client.exc import TagConflict
from cloudcontrol.common.client.tags import Tag
from cloudcontrol.common.jobs import Job
from cloudcontrol.common.tql.db.helpers import taggify

from cloudcontrol.node.exc import PluginError


logger = logging.getLogger(__name__)


def _tag_direct_use_error(*args, **kwargs):
    raise RuntimeError('Plugins tags can\'t be called directly')


class Plugin(object):

    """ Represent a loaded cloudcontrol plugin.
    """

    def __init__(self, logger, tag_db, name, sha1, plugin_body):
        self.logger = logger
        self.name = name
        self.sha1 = sha1
        self.tag_db = tag_db
        self.methods = {}
        self.events = defaultdict(lambda: [])
        self._load_module(plugin_body)
        self.trigger('install', self)

    def _load_module(self, plugin_body):
        # load the code into a module:
        module = imp.new_module(self.name)
        module.logger = self.logger
        module.tag = self._decorator_tag
        module.method = self._decorator_method
        module.on = self._decorator_event
        try:
            exec plugin_body in module.__dict__
            conflicts = self.tag_db.check_tags_conflict(*[tag.name for tag in
                                                          self.tags])
            if conflicts:
                raise TagConflict(
                    'Tags with names %s already exist' % ','.join(conflicts))
            else:
                self.tag_db.add_tags(self.tags)
        except Exception as exc:
            err_msg = 'Error during plugin installation (%s): %s' % (
                self.name, exc)
            logger.exception(err_msg)
            # make sure all tags are stopped
            self.tag_db.set_parent(None)
            raise RuntimeError(err_msg)
        else:
            # prevents module from being garbage collected
            self.module = module

    def _decorator_tag(self, ttl=-1, refresh=None, name=None, background=False):
        def decorator(func):
            tag = Tag(func.__name__ if name is None else name,
                      lambda: taggify(func()), ttl=ttl, refresh=refresh,
                      background=background)
            return _tag_direct_use_error
        return decorator

    def _decorator_method(self, name=None):
        def decorator(func):
            register_name = func.__name__ if name is None else name
            if register_name in self.methods:
                raise PluginError('Already defined method %s' % register_name)
            self.methods[register_name] = func
            return func
        return decorator

    def _decorator_event(self, event):
        def decorator(func):
            self.events[event].append(func)
            return func
        return decorator

    def trigger(self, event, *args, **kwargs):
        """Trigger an event."""
        for func in self.events[event]:
            func(*args, **kwargs)

    def uninstall(self):
        self.trigger('uninstall')
        # shutdown the tags database:
        self.tag_db.set_parent(None)


class PluginMethodJob(Job):

    """Job that run a plugin method."""

    def job(self, plugin_name, method_name, method, method_kwargs):
        kwargs_str = ', '.join(('%s=%s' % (k, v) for k, v in method_kwargs.iteritems()))
        self.title = 'Plugin: %s.%s(%s)' % (plugin_name, method_name, kwargs_str)
        try:
            returned = method(self, **method_kwargs)
        except Exception:
            self.logger.exception('Error while executing method')
            raise
        if returned is not None:
            self.attachment('output').write(returned)