Permalink
Browse files

add plugin system to gafferd.

Plugins are a way to enhance the basic gafferd functionality in a custom
manner. Plugins allows you to load any gaffer application and site
plugins.
  • Loading branch information...
1 parent b5ee0bc commit c1c6eb7254c37262cd3ddfb28ae4317fed4f614b @benoitc committed Oct 28, 2012
View
128 docs/gafferd.rst
@@ -5,33 +5,38 @@ Gafferd is a server able to launch and manage processes. It can be
controlled via the :doc:`http` .
Usage
-+++++
+-----
::
- $ gafferd --help
- usage: gafferd [-h] [-v] [-vv] [--daemon] [--pidfile PIDFILE] [--bind BIND]
- [--certfile CERTFILE] [--keyfile KEYFILE] [--backlog BACKLOG]
+ $ gafferd -h
+ usage: gafferd [-h] [-c CONFIG_FILE] [-p PLUGINS_DIR] [-v] [-vv] [--daemon]
+ [--pidfile PIDFILE] [--bind BIND] [--certfile CERTFILE]
+ [--keyfile KEYFILE] [--backlog BACKLOG]
[config]
Run some watchers.
positional arguments:
- config configuration file
+ config configuration file
optional arguments:
- -h, --help show this help message and exit
- -v verbose mode
- -vv like verbose mode but output stream too
- --daemon Start gaffer in the background
+ -h, --help show this help message and exit
+ -c CONFIG_FILE, --config CONFIG_FILE
+ configuration file
+ -p PLUGINS_DIR, --plugins-dir PLUGINS_DIR
+ default plugin dir
+ -v verbose mode
+ -vv like verbose mode but output stream too
+ --daemon Start gaffer in the background
--pidfile PIDFILE
- --bind BIND default HTTP binding
- --certfile CERTFILE SSL certificate file for the default binding
- --keyfile KEYFILE SSL key file for the default binding
- --backlog BACKLOG default backlog
+ --bind BIND default HTTP binding
+ --certfile CERTFILE SSL certificate file for the default binding
+ --keyfile KEYFILE SSL key file for the default binding
+ --backlog BACKLOG default backlog
Config file example
-+++++++++++++++++++
+-------------------
::
@@ -67,3 +72,98 @@ Config file example
numprocesses = 1
redirect_output = stdout, stderr
redirect_input = true
+
+Plugins
+-------
+
+Plugins are a way to enhance the basic gafferd functionality in a custom manner.
+Plugins allows you to load any gaffer application and site plugins. You
+can for example use the plugin system to add a simple UI to administrate
+gaffer using the HTTP interface.
+
+A plugin has the following structure::
+
+ /pluginname
+ _site/
+ plugin/
+ __init__.py
+ ...
+ ***.py
+
+A plugin can be discovered by adding one ore more module that expose a
+class inheriting from ``gaffer.Plugin``. Every plugin file should have a
+__all__ attribute containing the implemented plugin class. Ex::
+
+
+ from gaffer import Plugin
+
+ __all__ = ['DummyPlugin']
+
+ from .app import DummyApp
+
+
+ class DummyPlugin(Plugin):
+ name = "dummy"
+ version = "1.0"
+ description = "test"
+
+ def app(self, cfg):
+ return DummyApp()
+
+
+The dummy app here only print some info when started or stopped::
+
+
+ class DummyApp(object):
+
+ def start(self, loop, manager):
+ print("start dummy app")
+
+ def stop(sef):
+ print("stop dummy")
+
+ def rester(self):
+ print("restart dummy")
+
+
+See the :doc:`overview` for more infos. You can try it in the example
+folder::
+
+ $ cd examples
+ $ gafferd -c gaffer.ini -p plugins/
+
+
+Install plugins
++++++++++++++++
+
+Installing plugins can be done by placing the plugin in the plugin
+folder. The plugin folder is either set in the setting file using the
+**plugin_dir** in the gaffer section or using the ``-p`` option of the
+command line.
+
+The default plugin dir is set to ``~/.gafferd/plugins`` .
+
+Site plugins
+++++++++++++
+
+Plugins can have “sites” in them, any plugin that exists under the
+plugins directory with a _site directory, its content will be statically
+served when hitting ``/_plugin/[plugin_name]/`` url. Those can be added even
+after the process has started.
+
+Installed plugins that do not contain any Python related content, will
+automatically be detected as site plugins, and their content will be
+moved under _site.
+
+
+Mandatory Plugins
++++++++++++++++++
+
+If you rely on some plugins, you can define mandatory plugins using the
+``mandatory`` attribute of a the plugin class, for example, here is a
+sample config::
+
+
+ class DummyPlugin(Plugin):
+ ...
+ mandatory = ['somedep']
View
1 examples/plugins/dummy/_site/index.html
@@ -0,0 +1 @@
+hello dummy
View
0 examples/plugins/dummy/dummy/__init__.py
No changes.
View
10 examples/plugins/dummy/dummy/app.py
@@ -0,0 +1,10 @@
+class DummyApp(object):
+
+ def start(self, loop, manager):
+ print("start dummy app")
+
+ def stop(sef):
+ print("stop dummy")
+
+ def rester(self):
+ print("restart dummy")
View
15 examples/plugins/dummy/dummy/main.py
@@ -0,0 +1,15 @@
+from gaffer import Plugin
+
+__all__ = ['DummyPlugin']
+
+from .app import DummyApp
+
+
+class DummyPlugin(Plugin):
+ name = "dummy"
+ version = "1.0"
+ description = "test"
+
+
+ def app(self, cfg):
+ return DummyApp()
View
1 examples/plugins/dummy1/_site/index.html
@@ -0,0 +1 @@
+hello dummy1
View
0 examples/plugins/dummy1/dummy1/__init__.py
No changes.
View
23 examples/plugins/dummy1/dummy1/main.py
@@ -0,0 +1,23 @@
+from gaffer import Plugin
+
+__plugins__ = ['DummyPlugin']
+
+class DummyApp(object):
+
+ def start(self, loop, manager):
+ print("start dummy app")
+
+ def stop(sef):
+ print("stop dummy")
+
+ def rester(self):
+ print("restart dummy")
+
+class DummyPlugin(Plugin):
+ name = "dummy"
+ version = "1.0"
+ description = "test"
+
+
+ def app(self, cfg):
+ return DummyApp()
View
1 examples/plugins/hello/index.html
@@ -0,0 +1 @@
+hello world
View
1 gaffer/__init__.py
@@ -6,3 +6,4 @@
__version__ = ".".join(map(str, version_info))
from gaffer.manager import Manager
+from gaffer.node.plugins import Plugin
View
10 gaffer/http_handler.py
@@ -105,7 +105,7 @@ class HttpHandler(object):
sockets) with different options. Each endpoint can also listen on
different interfaces """
- def __init__(self, endpoints=[], handlers=None):
+ def __init__(self, endpoints=[], handlers=None, **settings):
self.endpoints = endpoints or []
if not endpoints: # if no endpoints passed add a default
self.endpoints.append(HttpEndpoint())
@@ -114,10 +114,16 @@ def __init__(self, endpoints=[], handlers=None):
self.handlers = copy.copy(DEFAULT_HANDLERS)
self.handlers.extend(handlers or [])
+ # custom settings
+ if 'manager' in settings:
+ del settings['manager']
+ self.settings = settings
+
def start(self, loop, manager):
self.loop = loop
self.manager = manager
- self.app = Application(self.handlers, manager=self.manager)
+ self.app = Application(self.handlers, manager=self.manager,
+ **self.settings)
# start endpoints
for endpoint in self.endpoints:
View
77 gaffer/node/gafferd.py
@@ -22,6 +22,8 @@
from ..util import daemonize, setproctitle_
from ..webhooks import WebHooks
+from .plugins import PluginManager
+from .util import user_path
ENDPOINT_DEFAULTS = dict(
uri = None,
@@ -67,19 +69,16 @@ class Server(object):
def __init__(self, args):
self.args = args
+ self.cfg = None
- if not args.config:
- self.apps, self.processes = self.defaults()
+ config_file = args.config or args.config_file
+ if not config_file:
+ self.set_defaults()
else:
- self.apps, self.processes = self.get_config(args.config)
-
- if args.verboseful:
- self.apps.append(ConsoleOutput(actions=['.']))
- elif args.verbose:
- self.apps.append(ConsoleOutput(output_streams=False,
- actions=['.']))
+ self.parse_config(config_file)
self.manager = Manager()
+ self.plugin_manager = PluginManager(self.plugins_dir)
def default_endpoint(self):
params = ENDPOINT_DEFAULTS.copy()
@@ -99,14 +98,38 @@ def default_endpoint(self):
params['uri'] = self.args.bind or '127.0.0.1:5000'
return HttpEndpoint(**params)
- def defaults(self):
- apps = [SigHandler(),
- WebHooks(),
- HttpHandler(endpoints=[self.default_endpoint()])]
- return apps, []
+ def set_defaults(self):
+ self.plugins_dir = self.args.plugins_dir
+ self.webhooks = []
+ self.endpoints = [self.default_endpoint()]
+ self.processes = []
def run(self):
- self.manager.start(apps=self.apps)
+ # check if any plugin dependancy is missing
+ self.plugin_manager.check_mandatory()
+
+ # setup the http api
+ static_sites = self.plugin_manager.get_sites()
+ http_handler = HttpHandler(endpoints=self.endpoints,
+ handlers=static_sites)
+
+ # setup gaffer apps
+ apps = [SigHandler(),
+ WebHooks(hooks=self.webhooks),
+ http_handler]
+
+ # extend with plugin apps
+ plugin_apps = self.plugin_manager.get_apps(self.cfg)
+ apps.extend(plugin_apps)
+
+ # verbose mode
+ if self.args.verboseful:
+ apps.append(ConsoleOutput(actions=['.']))
+ elif self.args.verbose:
+ apps.append(ConsoleOutput(output_streams=False,
+ actions=['.']))
+
+ self.manager.start(apps=apps)
# add processes
for name, cmd, params in self.processes:
@@ -136,8 +159,12 @@ def read_config(self, config_path):
return cfg, cfg_files_read
- def get_config(self, config_file):
+ def parse_config(self, config_file):
cfg, cfg_files_read = self.read_config(config_file)
+ self.cfg = cfg
+
+ self.plugins_dir = cfg.dget('gaffer', 'plugins_dir',
+ self.args.plugins_dir)
# you can setup multiple endpoints in the config
endpoints_str = cfg.dget('gaffer', 'http_endpoints', '')
@@ -239,17 +266,24 @@ def get_config(self, config_file):
# we create a default endpoint
endpoints = [self.default_endpoint()]
- apps = [SigHandler(),
- WebHooks(hooks=webhooks),
- HttpHandler(endpoints=endpoints)]
-
- return apps, processes
+ self.endpoints = endpoints
+ self.webhooks = webhooks
+ self.processes = processes
def run():
+ # default plugins dir
+ plugins_dir = os.path.join(user_path(), "plugins")
+
+ # define the argument parser
parser = argparse.ArgumentParser(description='Run some watchers.')
parser.add_argument('config', help='configuration file',
nargs='?')
+ parser.add_argument('-c', '--config', dest='config_file',
+ help='configuration file')
+ parser.add_argument('-p', '--plugins-dir', dest='plugins_dir',
+ help="default plugin dir", default=plugins_dir),
+
parser.add_argument('-v', dest='verbose', action='store_true',
help="verbose mode")
parser.add_argument('-vv', dest='verboseful', action='store_true',
@@ -266,7 +300,6 @@ def run():
parser.add_argument('--backlog', dest='backlog', type=int,
default=128, help="default backlog"),
-
args = parser.parse_args()
if args.daemonize:
View
146 gaffer/node/plugins.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -
+#
+# This file is part of gaffer. See the NOTICE for more information.
+
+import importlib
+import os
+import sys
+
+from tornado import web
+
+class Plugin(object):
+ """ basic plugin interfacce """
+
+ name = ""
+ version = "?"
+ descripton = ""
+ mandatory = []
+
+ def app(self, cfg):
+ """ return a gaffer application """
+ return None
+
+
+class PluginDir(object):
+
+ def __init__(self, name, rootdir):
+ self.name = name
+ self.root = rootdir
+ self.plugins = []
+ self.names = []
+ self.mandatory = []
+
+ # load plugins
+ self._scan()
+
+ site_path = os.path.join(rootdir, '_site')
+ if os.path.isdir(site_path):
+ self.site = site_path
+ else:
+ self.site = None
+
+ if not self.plugins and self.site is None:
+ if os.path.isfile(os.path.join(rootdir, 'index.html')):
+ self.site = rootdir
+
+
+ def _scan(self):
+ plugins = []
+ dirs = []
+
+ # initial pass, read
+ for name in os.listdir(self.root):
+ if name in (".", "..",):
+ continue
+
+ path = os.path.join(self.root, name)
+ if (os.path.isdir(path) and
+ os.path.isfile(os.path.join(path, '__init__.py'))):
+ # no conflict
+ if path not in sys.path:
+ dirs.append((name, path))
+ sys.path.insert(0, os.path.join(self.root, '..',
+ name))
+ sys.path.insert(0, os.path.join(self.root, name))
+
+ for (name, d) in dirs:
+ try:
+ for f in os.listdir(d):
+ if f.endswith(".py") and f != "__init__.py":
+ plugins.append(("%s.%s" % (name, f[:-3]), d))
+ except OSError:
+ sys.stderr.write("error, loading %s" % f)
+ sys.stderr.flush()
+ continue
+
+ mod = None
+ for (name, d) in plugins:
+ mod = importlib.import_module(name)
+ if hasattr(mod, "__all__"):
+ for attr in mod.__all__:
+ plug = getattr(mod, attr)
+ if issubclass(plug, Plugin):
+ self._load_plugin(plug())
+
+ def _load_plugin(self, plug):
+ if not plug.name:
+ raise RuntimeError("invalid plugin: %s [%s]" % (self.name,
+ self.root))
+
+ self.plugins.append(plug)
+ self.names.append(plug.name)
+ self.mandatory.extend(plug.mandatory or [])
+
+
+class PluginManager(object):
+
+ def __init__(self, plugin_dir):
+ self.plugin_dir = plugin_dir
+ self.plugins = {}
+ self.installed = []
+
+ # scan all plugins
+ self.scan()
+
+ def scan(self):
+ if not os.path.isdir(self.plugin_dir):
+ return
+
+ for name in os.listdir(self.plugin_dir):
+ if name in (".", "..",):
+ continue
+
+ path = os.path.abspath(os.path.join(self.plugin_dir, name))
+ if os.path.isdir(path):
+ plug = PluginDir(name, path)
+ self.plugins[name] = plug
+ self.installed.extend(plug.names)
+
+ def check_mandatory(self):
+ sinstalled = set(self.installed)
+ for name, plug in self.plugins.items():
+ smandatory = set(plug.mandatory)
+ diff = smandatory.difference(sinstalled)
+ if diff:
+ raise RuntimeError("%s requires %s to be used" % (name,
+ diff))
+
+ def get_sites(self):
+ handlers = []
+ for name, plug in self.plugins.items():
+ if plug.site is not None:
+ static_path = r"/_plugin/%s/(.*)" % name
+ rule = (static_path, web.StaticFileHandler,
+ {"path": plug.site,
+ "default_filename": "index.html"})
+ handlers.append(rule)
+ return handlers
+
+ def get_apps(self, cfg):
+ apps = []
+ for name, plugdir in self.plugins.items():
+ for plug in plugdir.plugins:
+ app = plug.app(cfg)
+ if app is not None:
+ apps.append(app)
+ return apps
View
13 gaffer/node/util.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -
+#
+# This file is part of gaffer. See the NOTICE for more information.
+
+import os
+
+if os.name == 'nt':
+ def user_path():
+ home = os.path.expanduser('~')
+ return os.path.join(home, '_gafferd')
+else:
+ def user_path():
+ return os.path.expanduser('~/.gafferd')

0 comments on commit c1c6eb7

Please sign in to comment.