Skip to content

Commit b0c05b7

Browse files
authored
Merge pull request #97 from PUNCH-Cyber/plugin_opts_config
Cleanup defining plugin configuration
2 parents 163f75b + 5be5392 commit b0c05b7

8 files changed

+171
-82
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
### Changed
1717

18+
- Improve handling of plugin configuration options. Plugin options can now also be in stoq.cfg. (Thanks for feedback @chemberger!)
19+
- Set default precendence for plugin configuration options to be 1) `plugin_opts` when instantiating `Stoq`, 2) `stoq.cfg`, 3) Plugin config file (Thanks for feedback @chemberger!)
1820
- Make formatted exceptions more legible in results
1921

2022
## [2.0.2] - 2019-01-14

docs/dev/plugin_overview.rst

+63-31
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,62 @@ For a full listing of all publicly available plugins, check out the `stoQ public
1919
Configuration
2020
*************
2121

22-
Each plugin must have an ``.stoq`` configuration file. The configuration file resides in
22+
Plugins may be provided configuration options in one of four ways. In order of precendece:
23+
24+
- From the command line
25+
- Upon instantiation of `Stoq()`
26+
- Defined in `stoq.cfg`
27+
- Defined in the plugin's `.stoq` configuration file
28+
29+
.. _pluginconfigcmdline:
30+
31+
Command Line
32+
------------
33+
34+
When running ``stoq`` from the command line, simply add ``--plugin-opts`` to your arguments
35+
followed by the desired plugin options. The syntax for plugin options is::
36+
37+
plugin_name:option=value
38+
39+
For example, if we want to tell the plugin ``dirmon`` to monitor the directory ``/tmp/monitor``
40+
for new files by setting the option ``source_dir``, the syntax would be::
41+
42+
dirmon:source_dir=/tmp/monitor
43+
44+
45+
.. _pluginconfiginstantiation:
46+
47+
Instantiation
48+
-------------
49+
50+
When using stoQ as a framework, plugin options may be defined when instantiating ``Stoq`` using the ``plugin_opts``
51+
argument::
52+
53+
>>> from stoq import Stoq
54+
>>> plugin_options = {'dirmon': {'source_dir': '/tmp/monitor'}}
55+
>>> s = Stoq(plugin_opts=plugin_options)
56+
57+
58+
.. _pluginconfigstoqcfg:
59+
60+
stoq.cfg
61+
--------
62+
63+
The recommended location for storing static plugin configuration options is in `stoq.cfg`. The reason for this
64+
if all plugin options defined in the plugin's `.stoq` file will be overwritten when the plugin is upgraded.
65+
66+
To define plugin options in `stoq.cfg` simply add a section header of the plugin name, then define the plugin options::
67+
68+
[dirmon]
69+
source_dir = /tmp/monitor
70+
71+
72+
.. _pluginconfigpluginstoq:
73+
74+
Plugin .stoq configuration file
75+
--------------------------------
76+
77+
Each plugin must have a ``.stoq`` configuration file. The configuration file resides in
2378
the same directory as the plugin module. The plugin's configuration file allows for
2479
configuring a plugin with default or static settings. The configuration file is a standard
2580
YAML file and is parsed using the ``configparser`` module. The following is an example
@@ -55,37 +110,14 @@ Additionally, some optional settings may be defined::
55110
* **options**
56111
- **min_stoq_version**: Minimum version of stoQ required to work properly. If the version of `stoQ` is less than the version defined, a warning will be raised.
57112

58-
Custom settings may be added as required for plugins, but the plugins must be configured to
59-
load and set them. For example, our configuration file may be::
60-
61-
[Core]
62-
Name = example_plugin
63-
Module = example_plugin
64-
65-
[Documentation]
66-
Author = PUNCH Cyber
67-
Version = 0.1
68-
Website = https://github.com/PUNCH-Cyber/stoq-plugins-public
69-
Description = Example stoQ Plugin
70-
71-
[worker]
72-
source = /tmp
73-
74-
75-
Now, in the ``__init__`` method of our plugin class, we can ensure we define the ``source``
76-
setting under the ``worker`` section of the configuration file::
77-
78-
79-
if plugin_opts and 'source' in plugin_opts:
80-
self.source = plugin_opts['source']
81-
elif config.has_option('worker', 'source'):
82-
self.source = config.get('worker', 'source')
83-
113+
.. note::
114+
Plugin options *must* be under the `[options]` section header to be accessible via the other plugin configuration options.J
84115

85-
First, we are checking for any plugin options were provided to ``Stoq`` at instantiation or at the
86-
:ref:`command line <pluginoptions>`. If not, it will check the plugin's configuration file for
87-
the ``source`` setting under the ``worker`` section. If ``source`` is defined in either, the
88-
setting will be made available to the plugin by defining ``self.source``.
116+
.. warning::
117+
Plugin configuration options may be overwritten when a plugin is upgraded. Upgrading plugins is a destructive
118+
operation. This will overwrite/remove all data within the plugins directory, to include the plugin configuration
119+
file. It is highly recommended that the plugin directory be backed up regularly to ensure important information
120+
is not lost, or plugin configuration options be defined in `stoq.cfg`.
89121

90122

91123
.. _multiclass:

docs/gettingstarted.rst

+5-25
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ look for ``stoq.cfg`` in ``$STOQ_HOME`` if running from the command line, or ``$
3434
used as a library.
3535

3636

37+
Plugin options may also be defined in `stoq.cfg`. For more information on how
38+
3739
.. _stoqhome:
3840

3941
$STOQ_HOME
@@ -281,32 +283,10 @@ to what stoQ can do. For more command line options in `run` mode, simply run::
281283

282284
$ stoq run -h
283285

284-
.. _pluginoptions:
285-
286-
Plugin Options
287-
--------------
288-
289-
Plugin options allows for configuration settings of plugins to be modified upon instantiation.
290-
This is extremely useful when you need to change a configuration options on the fly, such as
291-
our `run` mode example above.
292-
293-
When running ``stoq`` from the command line, simply add ``--plugin-opts`` to your arguments
294-
followed by the desired plugin options. The syntax for plugin options is::
295-
296-
plugin_name:option=value
297-
298-
For example, if we want to tell the plugin ``dirmon`` to monitor the directory ``/tmp/monitor``
299-
for new files by setting the option ``source_dir``, the syntax would be::
300-
301-
dirmon:source_dir=/tmp/monitor
302-
303-
Additionally, plugin options may be defined when instantiating ``Stoq`` using the ``plugin_opts``
304-
argument::
305-
306-
>>> from stoq import Stoq
307-
>>> plugin_options = {'dirmon': {'source_dir': '/tmp/monitor'}}
308-
>>> s = Stoq(plugin_opts=plugin_options)
286+
Plugin configuration
287+
--------------------
309288

289+
Plugin configurations may be defined in several ways, see :ref:`plugin configuration <pluginconfig>`.
310290

311291
RequestMeta Options
312292
-------------------

docs/installation.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Plugins may be upgraded (or downgraded) by adding the `--upgrade` command line o
115115
.. warning::
116116
Upgrading plugins is a destructive operation. This will overwrite/remove all data within the plugins directory,
117117
to include the plugin configuration file. It is highly recommended that the plugin directory be backed up
118-
regularly to ensure important information is not lost.
118+
regularly to ensure important information is not lost, or plugin configuration options be defined in `stoq.cfg`.
119119

120120
.. _devenv:
121121

stoq/core.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ def __init__(
392392
)
393393
plugin_dir_list = [d.strip() for d in plugin_dir_str.split(',')]
394394

395-
super().__init__(plugin_dir_list, plugin_opts)
395+
super().__init__(plugin_dir_list, plugin_opts, config)
396396

397397
if not providers:
398398
providers_str = config.get('core', 'providers', fallback='')
@@ -650,7 +650,7 @@ def _single_scan(
650650
# Normal dispatches are the "1st round" of scanning
651651
payload.plugins_run['workers'][0].append(plugin_name)
652652
try:
653-
worker_response = plugin.scan(payload, request_meta) # pyre-ignore[16]
653+
worker_response = plugin.scan(payload, request_meta) # pyre-ignore[16]
654654
except Exception as e:
655655
msg = 'worker:failed to scan'
656656
self.log.exception(msg)

stoq/plugin_manager.py

+44-21
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import configparser
2121
import importlib.util
2222
from pkg_resources import parse_version
23-
from typing import Dict, List, Optional, Set, Tuple, Any
23+
from typing import Dict, List, Optional, Tuple, Any
2424

2525
from .exceptions import StoqException
2626
from stoq.plugins import (
@@ -37,8 +37,12 @@
3737

3838
class StoqPluginManager:
3939
def __init__(
40-
self, plugin_dir_list: List[str], plugin_opts: Optional[Dict[str, Dict]] = None
40+
self,
41+
plugin_dir_list: List[str],
42+
plugin_opts: Optional[Dict[str, Dict]] = None,
43+
stoq_config: Optional[configparser.ConfigParser] = None,
4144
) -> None:
45+
self._stoq_config = stoq_config
4246
self._plugin_opts = {} if plugin_opts is None else plugin_opts
4347
self._plugin_name_to_info: Dict[str, Tuple[str, configparser.ConfigParser]] = {}
4448
self._loaded_plugins: Dict[str, BasePlugin] = {}
@@ -68,11 +72,11 @@ def _collect_plugins(self, plugin_dir_list: List[str]) -> None:
6872
if not file.endswith('.stoq'):
6973
continue
7074
plugin_conf_path = os.path.join(root_path, file)
71-
config = configparser.ConfigParser()
75+
plugin_config = configparser.ConfigParser()
7276
try:
73-
config.read(plugin_conf_path)
74-
name = config.get('Core', 'Name')
75-
module_name = config.get('Core', 'Module')
77+
plugin_config.read(plugin_conf_path)
78+
plugin_name = plugin_config.get('Core', 'Name')
79+
module_name = plugin_config.get('Core', 'Module')
7680
except Exception:
7781
self.log.warning(
7882
f'Error parsing config file: {plugin_conf_path}',
@@ -82,34 +86,40 @@ def _collect_plugins(self, plugin_dir_list: List[str]) -> None:
8286
module_path_pyc = os.path.join(root_path, module_name) + '.pyc'
8387
module_path_py = os.path.join(root_path, module_name) + '.py'
8488
if os.path.isfile(module_path_pyc):
85-
self._plugin_name_to_info[name] = (module_path_pyc, config)
89+
self._plugin_name_to_info[plugin_name] = (
90+
module_path_pyc,
91+
plugin_config,
92+
)
8693
elif os.path.isfile(module_path_py):
87-
self._plugin_name_to_info[name] = (module_path_py, config)
94+
self._plugin_name_to_info[plugin_name] = (
95+
module_path_py,
96+
plugin_config,
97+
)
8898
else:
8999
self.log.warning(
90100
f'Unable to find module at: {module_path_pyc} or {module_path_py}',
91101
exc_info=True,
92102
)
93103
continue
94104

95-
def load_plugin(self, name: str) -> BasePlugin:
96-
name = name.strip()
97-
if name in self._loaded_plugins:
98-
return self._loaded_plugins[name]
99-
module_path, config = self._plugin_name_to_info[name]
100-
if config.has_option('options', 'min_stoq_version'):
101-
min_stoq_version = config.get('options', 'min_stoq_version')
105+
def load_plugin(self, plugin_name: str) -> BasePlugin:
106+
plugin_name = plugin_name.strip()
107+
if plugin_name in self._loaded_plugins:
108+
return self._loaded_plugins[plugin_name]
109+
module_path, plugin_config = self._plugin_name_to_info[plugin_name]
110+
if plugin_config.has_option('options', 'min_stoq_version'):
111+
min_stoq_version = plugin_config.get('options', 'min_stoq_version')
102112
# Placing this import at the top of this file causes a circular
103113
# import chain that causes stoq to crash on initialization
104114
from stoq import __version__
105115

106116
if parse_version(__version__) < parse_version(min_stoq_version):
107117
self.log.warning(
108-
f'Plugin {name} not compatible with this version of '
118+
f'Plugin {plugin_name} not compatible with this version of '
109119
'stoQ. Unpredictable results may occur!'
110120
)
111121
spec = importlib.util.spec_from_file_location(
112-
config.get('Core', 'Module'), module_path
122+
plugin_config.get('Core', 'Module'), module_path
113123
)
114124
module = importlib.util.module_from_spec(spec)
115125
spec.loader.exec_module(module) # pyre-ignore
@@ -132,16 +142,29 @@ def load_plugin(self, name: str) -> BasePlugin:
132142
)
133143
if len(plugin_classes) == 0:
134144
raise StoqException(
135-
f'No valid plugin classes found in the module for {name}'
145+
f'No valid plugin classes found in the module for {plugin_name}'
136146
)
137147
if len(plugin_classes) > 1:
138148
raise StoqException(
139-
f'Multiple possible plugin classes found in the module for {name},'
149+
f'Multiple possible plugin classes found in the module for {plugin_name},'
140150
' unable to distinguish which to use.'
141151
)
142152
_, plugin_class = plugin_classes[0]
143-
plugin = plugin_class(config, self._plugin_opts.get(name))
144-
self._loaded_plugins[name] = plugin
153+
# Plugin configuration order of precendence:
154+
# 1) plugin options provided at instantiation of `Stoq()`
155+
# 2) plugin configuration in `stoq.cfg`
156+
# 3) `plugin_name.stoq`
157+
if isinstance(
158+
self._stoq_config, configparser.ConfigParser
159+
) and self._stoq_config.has_section(plugin_name):
160+
if not plugin_config.has_section('options'):
161+
plugin_config.add_section('options')
162+
for opt in self._stoq_config.options(plugin_name):
163+
plugin_config['options'][opt] = self._stoq_config.get(plugin_name, opt)
164+
if self._plugin_opts.get(plugin_name):
165+
plugin_config.read_dict({'options': self._plugin_opts.get(plugin_name)})
166+
plugin = plugin_class(plugin_config, self._plugin_opts.get(plugin_name))
167+
self._loaded_plugins[plugin_name] = plugin
145168
return plugin
146169

147170
def list_plugins(self) -> Dict[str, Dict[str, Any]]:

stoq/tests/data/stoq.cfg

+10
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,13 @@ log_dir:
2222
# What is the maximum size of the provider queue?
2323
max_queue: 919
2424
max_recursion: 4
25+
26+
[configurable_worker]
27+
worker_test_option_bool = True
28+
worker_test_option_str = Worker Testy McTest Face
29+
worker_test_option_int = 10
30+
31+
[dummy_connector]
32+
connector_test_option_bool = False
33+
connector_test_option_str = Connector Testy McTest Face
34+
connector_test_option_int = 5

stoq/tests/test_plugin_manager.py

+44-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import unittest
2121

2222

23-
from stoq import StoqException
23+
from stoq import Stoq, StoqException
2424
from stoq.data_classes import Payload, WorkerResponse, RequestMeta
2525
from stoq.plugin_manager import StoqPluginManager
2626
from stoq.plugins import WorkerPlugin
@@ -110,6 +110,49 @@ def test_plugin_opts(self):
110110
plugin = pm.load_plugin('configurable_worker')
111111
self.assertEqual(plugin.get_crazy_runtime_option(), 16)
112112

113+
def test_plugin_opts_from_stoq_cfg(self):
114+
s = Stoq(base_dir=utils.get_data_dir())
115+
plugin = s.load_plugin('configurable_worker')
116+
self.assertEqual(
117+
plugin.config.getboolean('options', 'worker_test_option_bool'), True
118+
)
119+
self.assertEqual(
120+
plugin.config.get('options', 'worker_test_option_str'),
121+
'Worker Testy McTest Face',
122+
)
123+
self.assertEqual(plugin.config.getint('options', 'worker_test_option_int'), 10)
124+
plugin = s.load_plugin('dummy_connector')
125+
self.assertEqual(
126+
plugin.config.getboolean('options', 'connector_test_option_bool'), False
127+
)
128+
self.assertEqual(
129+
plugin.config.get('options', 'Connector_test_option_str'),
130+
'Connector Testy McTest Face',
131+
)
132+
self.assertEqual(
133+
plugin.config.getint('options', 'connector_test_option_int'), 5
134+
)
135+
136+
def test_plugin_opts_precedence(self):
137+
s = Stoq(
138+
base_dir=utils.get_data_dir(),
139+
plugin_opts={
140+
'configurable_worker': {
141+
'worker_test_option_bool': False,
142+
'worker_test_option_str': 'Test string',
143+
'worker_test_option_int': 20,
144+
}
145+
},
146+
)
147+
plugin = s.load_plugin('configurable_worker')
148+
self.assertEqual(
149+
plugin.config.getboolean('options', 'worker_test_option_bool'), False
150+
)
151+
self.assertEqual(
152+
plugin.config.get('options', 'worker_test_option_str'), 'Test string'
153+
)
154+
self.assertEqual(plugin.config.getint('options', 'worker_test_option_int'), 20)
155+
113156
def test_min_stoq_version(self):
114157
pm = StoqPluginManager([utils.get_invalid_plugins_dir()])
115158
# We have to override the fact that all log calls are disabled in setUp()
@@ -135,7 +178,6 @@ def test_plugin_override(self):
135178
worker.PLUGINS2_DUP_MARKER
136179

137180

138-
139181
class ExampleExternalPlugin(WorkerPlugin):
140182
# Intentionally override this method to not require the config argument
141183
def __init__(self):

0 commit comments

Comments
 (0)