Skip to content
Snippets Groups Projects
plugins.py 3.63 KiB
Newer Older
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):
        def decorator(func):
            tag = Tag(func.__name__ if name is None else name,
                      lambda: taggify(func()), ttl=ttl, refresh=refresh)
            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)