Completed
Push — master ( d058c9...ca4b9c )
by Kenny
45s
created

plumd.plugins.config_plugin_writers()   C

Complexity

Conditions 7

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 7
dl 0
loc 26
rs 5.5
1
# -*- coding: utf-8 -*-
2
"""Utility classes to work with plugins - mainly plugin loading.
3
4
.. moduleauthor:: Kenny Freeman <[email protected]>
5
6
"""
7
__author__ = 'Kenny Freeman'
8
__email__ = '[email protected]'
9
__license__ = "ISCL"
10
__docformat__ = 'reStructuredText'
11
12
from abc import ABCMeta, abstractmethod
13
import imp
14
import inspect
15
import os.path
16
import traceback
17
18
import structlog    # pip install structlog
19
20
import plumd
21
import plumd.util
22
23
24
def load_instance(log, cname, mod, cobj):
25
    """Returns an instance of the class cname from the specified module.
26
27
    :param cname: The class name to load
28
    :type cname: str
29
    :param mod: A module object from eg. imp.load_source or __import__
30
    :type mod: module
31
    :param cobj: A conf configuration helper instance to pass to the class
32
    :type cobj: conf
33
    :rtype: Object -- an instance of the class requested
34
    :raises: PluginLoadError: many things can go wrong during loading
35
    """
36
    obj = None
37
    # find the class in the module
38
    for n, d in inspect.getmembers(mod, inspect.isclass):
39
        if n == cname:
40
            # try to instantiate the class
41
            try:
42
                obj = d(log, cobj)
43
            # plugins may raise pretty much any exception
44
            except Exception as e:
45
                tb = traceback.format_exc()
46
                estr = "class {0}, module {1} raised: {2}, trace:{3}"
47
                eargs = [cname, mod.__name__, e, tb]
48
                raise plumd.PluginLoadError(estr.format(*eargs))
49
            break
50
    # the module does not have the expected class in it
51
    if obj is None:
52
        estr = "{0} does not define class {1}".format(mod.__name__, cname)
53
        raise plumd.PluginLoadError(estr)
54
    return obj
55
56
57
def load_file_instance(log, cname, fname, cobj):
58
    """Imports the class cname from the python file fname and returns
59
    an instance of it.
60
61
    raises:
62
        PluginNotFoundError if the fname doesn't exist.
63
64
    :param cname: The name of the class to load
65
    :type cname: str
66
    :param fname: The full path to the python file to load from
67
    :type fname: str
68
    :param cobj: A conf configuration helper instance to pass to the class
69
    :type cobj: conf
70
    :rtype: Object -- an instance of the class requested
71
    :raises: PluginNotFoundError
72
    """
73
    mod = None
74
    if os.path.isfile(fname):
75
        # load the module that has the same name as the file (minus .py)
76
        mod = imp.load_source(cname, fname)
77
    else:
78
        raise plumd.PluginNotFoundError("file {0} does not exist".format(fname))
79
    return load_instance(log, cname, mod, cobj)
80
81
82
def load_module_instance(log, cname, mname, cobj):
83
    """Imports the class cname from the python module mname and returns
84
    an instance of it.
85
86
    raises:
87
        PluginLoadError - so many things can go wrong loading plugins..
88
89
    :param cname: The name of the class to load
90
    :type cname: str
91
    :param mname: The name of the module to load from
92
    :type fname: str
93
    :param cobj: A conf configuration helper instance to pass to the class
94
    :type cobj: conf
95
    :rtype: Object -- an instance of the class requested
96
    :raises: PluginLoadError
97
    """
98
    mod = None
99
    fname = None
100
    if "." in mname:
101
        fname = mname
102
    try:
103
        mod = __import__(mname, fromlist=fname)
104
    except Exception as e:
105
        msg = "module {0} import failed: {1}".format(mname, e)
106
        raise plumd.PluginLoadError(msg)
107
    return load_instance(log, cname, mod, cobj)
108
109
110
111
def load_plugin(log, pconf):
112
    """Loads the configured plugin and returns an instance of it.
113
114
    The configuration file for pconf must define at least:
115
116
    name: <name for this instance>          # a uniq name to give this instance
117
    module: <name of module to load from>   # the module name to load
118
    file: <full path to file to load from>  # or, the filename to load
119
    pclass: <name of class to load>         # name of the class to load
120
121
    plus any plugin specific configuration. You can instantiate multiple
122
    instances of a plugin with different configurations however must ensure
123
    that they are configured with uniq names.
124
125
    Also, if both file and module are defined in the configuration the file
126
    configuration is used and module is ignored.
127
128
    Raises:
129
        ConfigError if 'file' and 'module' sources are both configured.
130
        ConfigError if the configuration is missing 'name'.
131
        PluginLoadError if the configured file/module is not found.
132
        PluginLoadError if it was unable to instantiate the requested class.
133
134
    :param pconf: a configuration object that defines the plugin to load
135
    :type pconf: plumd.config.conf
136
    :raises: PluginLoadError, ConfigError
137
    """
138
    fname = pconf.get('file')                   # full path to python file
139
    mname = pconf.get('module')                 # module name
140
    pname = pconf.get('name', exception=True)   # plugin identifier
141
    cname = pconf.get('class', exception=True)  # class name
142
    instance = None
143
144
    if fname:
145
        args = [ log.bind(src=pconf.get('name')), cname, fname, pconf ]
146
        instance = load_file_instance(*args)
147
    elif mname:
148
        try:
149
            __import__(mname)
150
        except ImportError as e:
151
            estr = "module not found: {0} : {1} : {2}"
152
            raise plumd.PluginLoadError(estr.format(mname, pconf.path, e))
153
        args = [log.bind(src=pconf.get('name')), cname, mname, pconf]
154
        instance = load_module_instance(*args)
155
156
    if instance is None:
157
        raise plumd.PluginLoadError("load failed for: {0}".format(pconf.path))
158
159
    return instance
160
161
162
def load_from_conf(log, config, pconf):
163
    """Load a plugin as defined by pconf configuration file and return an
164
    instance of it.
165
166
    raises:
167
        ConfigError if ptype has an incorrect configuration path set
168
        ConfigError if a plugin configuration is missing 'name'
169
        DuplicatePlugin if a plugin configuration has a duplicate 'name'
170
        PluginLoadError if there was an error loading the plugin
171
172
    :param log: A structlog logger instance
173
    :type log: structlog
174
    :param config: a :class:`plumd.config.Conf` instance
175
    :type config: plumd.config.Conf
176
    :param pconf: Full path to a plugin configuration.
177
    :type pconf: str
178
    :rtype: object
179
    :raises: ConfigError, DuplicatePlugin
180
    """
181
    # create a configuration and pass on select values from the main config
182
    defaults = {
183
        'poll.interval': config.get('poll.interval'),
184
        'delay.poll': config.get('delay.poll'),
185
        'meta': config.get('meta')
186
    }
187
    pconfig = plumd.config.Conf(pconf).defaults(defaults)
188
    pname = pconfig.get("name", exception=True)
189
190
    # check to see if the plugin is disabled, if so return here
191
    if not pconfig.get('enabled', default=True):
192
        return
193
194
    # create a logger for the plugin and bind the src to the plugin name
195
    plog = structlog.get_logger().bind(src=pname)
196
197
    # load the plugin
198
    log.info("loading", pname=pname, file=pconfig.path)
199
    return load_plugin(plog, pconfig)
200
201
202
def get_plugins_dict(log, plugins, pclass):
203
    """Returns a dict of plugins that are of type pclass where the dict
204
    keys are the plugins name and the values are the plugin object.
205
206
    raises:
207
        DuplicatePlugin if a plugin has a duplicate 'name'
208
209
    :param plugins: A list of plugin objects.
210
    :type plugins: list
211
    :param pclas: A class eg. :class:`plumd.Reader`, :class:`plumd.Writer`
212
    :type pclass: object
213
    :rtype: dict
214
    :raises: DuplicatePlugin
215
    """
216
    plugs = {}
217
    for pobj in plugins:
218
        cpclass = pobj.__class__.__base__
219
        if pobj.__class__.__base__ == pclass:
220
            log.debug('get_plugins_dict', matched=pobj, pclass=cpclass)
221
            pconf = pobj.config
222
            pname = pconf.get('name', exception=True)
223
            if pname in plugs:
224
                err = "duplicate plugin name: {0} from {1}"
225
                raise DuplicatePlugin(err.format(pname, pconf.path))
226
            plugs[pname] = pobj
227
    log.debug('get_plugins_dict', plugins=plugs, pclass=pclass)
228
    return plugs
229
230
231
def load_all_plugins(log, config):
232
    """Returns a tuple of reader and writer plugin dicts.
233
234
    raises:
235
        ConfigError if ptype has an incorrect configuration path set
236
        ConfigError if a plugin configuration is missing 'name'
237
        DuplicatePlugin if a plugin configuration has a duplicate 'name'
238
        PluginLoadError if there was an error loading the plugin
239
240
    Returns a tuple of: (readers, writers) where each is a dict of:
241
    {'name': <plugin_instance>}. Name is the configured plugin name and
242
    <plugin_instance> is the corresponding instance of the plugin. Note plugins
243
    can be insantiated multiple times with different configurations.
244
245
    :param log: A structlog logger instance
246
    :type log: structlog
247
    :param config: a :class:`plumd.config.Conf` instance
248
    :type config: plumd.config.Conf
249
    :rtype: tuple
250
    :raises: ConfigError, DuplicatePlugin, PluginLoadError
251
    """
252
    plugins = []
253
    # get a list of configuration files
254
    # note the call to ls can raise an exception which we pass upwards
255
    for pconf in plumd.config.ls(config.get('config.plugins'), 'yaml'):
256
        # load the plugin object
257
        pobj = load_from_conf(log, config, pconf)
258
259
        # check if the plugin has been disabled in configuration
260
        if pobj is None:
261
            log.info('load_all_plugins', skipping=pconf, reason="disabled")
262
            continue
263
264
        plugins.append(pobj)
265
266
    log.debug('load_all_plugins', plugins=plugins)
267
268
    # we iterate over the plugin list multiple times..
269
    # since the list is small and there are not many types this is ok.
270
    readers = get_plugins_dict(log, plugins, plumd.plugins.Reader)
271
    writers = get_plugins_dict(log, plugins, plumd.plugins.Writer)
272
    log.debug('load_all_plugins', readers=readers)
273
    log.debug('load_all_plugins', writers=writers)
274
275
    return (readers, writers)
276
277
278
def config_plugin_writers(lobj):
279
    """Update the list of writers for each reader in the PluginLoader.
280
281
    :param lobj: a :class:`plumd.plugins.load.PluginLoader` object
282
    :type lobj: plumd.plugins.load.PluginLoader
283
284
    :raises: ConfigError
285
    """
286
    # now set the PluginReaders writers
287
    for prname, probj in lobj.readers.items():
288
        wcfg = probj.pobj.config.get('writers')
289
        # if nothing is configured the reader writes to all
290
        if wcfg is None:
291
            probj.writers = lobj.writers.values()
292
            continue
293
294
        # get list of writer names
295
        wnames = [ w for w in wcfg if w in lobj.writers ]
296
        if len(wnames) > 0:
297
            probj.writers = [ lobj.writers[w] for w in wnames ]
298
            continue
299
300
        msg = 'reader {0} from {1} has no valid writers configured'
301
        args = [ prname, probj.pobj.conf.path ]
302
        lobj.log.error('invalid reader', msg=msg.format(*args))
303
        sys.exit(1)
304