Coverage for mfplugin/utils.py: 75%
204 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-13 15:57 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-13 15:57 +0000
1import re
2import os
3import json
4import importlib
5from mfutil import BashWrapperException, BashWrapper, get_ipv4_for_hostname, \
6 mkdir_p_or_die
8__pdoc__ = {
9 "PluginEnvContextManager": False
10}
11MFMODULE = os.environ.get('MFMODULE', 'GENERIC')
12MFMODULE_RUNTIME_HOME = os.environ.get("MFMODULE_RUNTIME_HOME", "/tmp")
13MFMODULE_LOWERCASE = os.environ.get('MFMODULE_LOWERCASE', 'generic')
14PLUGIN_NAME_REGEXP = "^[A-Za-z0-9_-]+$"
17class PluginEnvContextManager(object):
19 __env_dict = None
20 __saved_environ = None
22 def __init__(self, env_dict):
23 self.__env_dict = env_dict
25 def __enter__(self):
26 self.__saved_environ = dict(os.environ)
27 for key, value in self.__env_dict.items():
28 os.environ[key] = value
30 def __exit__(self, type, value, traceback):
31 os.environ.clear()
32 os.environ.update(self.__saved_environ)
35def validate_plugin_name(plugin_name):
36 """Validate a plugin name.
38 Args:
39 plugin_name (string): the plugin name to validate.
41 Raises:
42 BadPluginName exception if the plugin_name is incorrect.
43 """
44 if plugin_name.startswith("plugin_"):
45 raise BadPluginName("A plugin name can't start with 'plugin_'")
46 if plugin_name.startswith("__"):
47 raise BadPluginName("A plugin name can't start with '__'")
48 if plugin_name == "base":
49 raise BadPluginName("A plugin name can't be 'base'")
50 if plugin_name == "config":
51 raise BadPluginName("A plugin name can't be 'config'")
52 if not re.match(PLUGIN_NAME_REGEXP, plugin_name):
53 raise BadPluginName("A plugin name must follow %s" %
54 PLUGIN_NAME_REGEXP)
57def plugin_name_to_layerapi2_label(plugin_name):
58 """Get a layerapi2 label from a plugin name.
60 Args:
61 plugin_name (string): the plugin name from which we create the label.
63 Returns:
64 (string): the layerapi2 label.
65 """
66 return "plugin_%s@%s" % (plugin_name, MFMODULE_LOWERCASE)
69def layerapi2_label_to_plugin_name(label):
70 """Get the plugin name from the layerapi2 label.
72 Args:
73 label (string): the label from which we extract the plugin name.
75 Returns:
76 (string): the plugin name.
77 """
78 if not label.startswith("plugin_"):
79 raise BadPlugin("bad layerapi2_label: %s => is it really a plugin? "
80 "(it must start with 'plugin_')" % label)
81 if not label.endswith("@%s" % MFMODULE_LOWERCASE):
82 raise BadPlugin("bad layerapi2_label: %s => is it really a plugin? "
83 "(it must end with '@%s')" %
84 (label, MFMODULE_LOWERCASE))
85 return label[7:].split('@')[0]
88def layerapi2_label_file_to_plugin_name(llf_path):
89 """Get the plugin name from the layerapi2 label file.
91 Args:
92 llf_path (string): the layerapi2 label file path from which
93 we extract the label.
95 Returns:
96 (string): the plugin name.
97 """
98 try:
99 with open(llf_path, 'r') as f:
100 c = f.read().strip()
101 except Exception:
102 raise BadPlugin("can't read %s file" % llf_path)
103 return layerapi2_label_to_plugin_name(c)
106def layerapi2_label_to_plugin_home(plugins_base_dir, label):
107 """Find the plugin home corresponding to the given layerapi2 label.
109 We search in plugins_base_dir for a directory (not recursively) with
110 the corresponding label value.
112 If we found nothing, None is returned.
114 Args:
115 plugins_base_dir (string): plugins base dir to search.
116 label (string): the label to search.
118 Returns:
119 (string): plugin home (absolute directory path) or None.
121 """
122 for d in os.listdir(plugins_base_dir):
123 if d == "base":
124 continue
125 fd = os.path.abspath(os.path.join(plugins_base_dir, d))
126 if not os.path.isdir(fd):
127 continue
128 llf = os.path.join(fd, ".layerapi2_label")
129 if not os.path.isfile(llf):
130 continue
131 try:
132 with open(llf, 'r') as f:
133 c = f.read().strip()
134 except Exception:
135 continue
136 if c == label:
137 return fd
138 # not found
139 return None
142def inside_a_plugin_env():
143 """Return True if we are inside a plugin_env.
145 Returns:
146 (boolean): True if we are inside a plugin_env, False else
147 """
148 return ("%s_CURRENT_PLUGIN_NAME" % MFMODULE) in os.environ
151def validate_configparser(v, cpobj, schema, public=False):
152 document = {}
153 for section in cpobj.sections():
154 document[section] = {}
155 for key in cpobj.options(section):
156 if not public or not key.startswith('_'):
157 document[section][key] = cpobj.get(section, key)
158 return v.validate(document, schema)
161def cerberus_errors_to_human_string(v_errors):
162 errors = ""
163 for section in v_errors.keys():
164 for error in v_errors[section]:
165 if errors == "":
166 errors = "\n"
167 if type(error) is str:
168 errors = errors + "- [section: %s] %s\n" % (section, error)
169 continue
170 for key in error.keys():
171 for error2 in error[key]:
172 errors = errors + \
173 "- [section: %s][key: %s] %s\n" % (section, key,
174 error2)
175 return errors
178class MFPluginException(BashWrapperException):
179 """Base mfplugin Exception class."""
181 def __init__(self, msg=None, validation_errors=None,
182 original_exception=None, **kwargs):
183 if msg is not None:
184 self.message = msg
185 else:
186 self.message = "mfplugin exception!"
187 if original_exception is not None:
188 BashWrapperException.__init__(
189 self,
190 self.message + (": %s" % original_exception),
191 **kwargs)
192 else:
193 BashWrapperException.__init__(self, self.message, **kwargs)
194 self.original_exception = original_exception
197class BadPlugin(MFPluginException):
198 """Exception raised when a plugin is badly constructed."""
200 def __init__(self, msg=None, validation_errors=None, **kwargs):
201 if msg is not None:
202 self.message = msg
203 else:
204 self.message = "bad plugin!"
205 MFPluginException.__init__(self, self.message, **kwargs)
206 self.validation_errors = validation_errors
208 def __repr__(self):
209 if self.validation_errors is None:
210 return MFPluginException.__repr__(self)
211 return "%s exception with message: %s and validation errors: %s" % \
212 (self.__class__.__name__, self.message, self.validation_errors)
214 def __str__(self):
215 return self.__repr__()
218class BadPluginConfiguration(BadPlugin):
219 """Exception raised when a plugin has a bad configuration."""
221 pass
224class BadPluginName(BadPlugin):
225 """Exception raised when a plugin has an invalid name."""
227 pass
230class NotInstalledPlugin(MFPluginException):
231 """Exception raised when a plugin is not installed."""
233 pass
236class AlreadyInstalledPlugin(MFPluginException):
237 """Exception raised when a plugin is already installed."""
239 pass
242class CantInstallPlugin(MFPluginException):
243 """Exception raised when we can't install a plugin."""
245 pass
248class CantUninstallPlugin(MFPluginException):
249 """Exception raised when we can't uninstall a plugin."""
251 pass
254class CantBuildPlugin(MFPluginException):
255 """Exception raised when we can't build a plugin."""
257 pass
260class BadPluginFile(MFPluginException):
261 """Exception raised when a plugin file is bad."""
263 pass
266def get_default_plugins_base_dir():
267 """Return the default plugins base directory path.
269 This value correspond to the content of MFMODULE_PLUGINS_BASE_DIR env var
270 or ${RUNTIME_HOME}/var/plugins (if not set).
272 Returns:
273 (string): the default plugins base directory path.
275 """
276 if "MFMODULE_PLUGINS_BASE_DIR" in os.environ:
277 return os.environ.get("MFMODULE_PLUGINS_BASE_DIR")
278 return os.path.join(MFMODULE_RUNTIME_HOME, "var", "plugins")
281def _touch_conf_monitor_control_file():
282 BashWrapper("touch %s/var/conf_monitor" % MFMODULE_RUNTIME_HOME)
285def resolve(val):
286 if val.lower() == "null" or val.startswith("/"):
287 # If it's "null" or linux socket
288 return val
289 return get_ipv4_for_hostname(val)
292def to_bool(strng):
293 if isinstance(strng, bool):
294 return strng
295 try:
296 return strng.lower() in ('1', 'true', 'yes', 'y', 't')
297 except Exception:
298 return False
301def to_int(strng):
302 try:
303 return int(strng)
304 except Exception:
305 return 0
308def null_to_empty(value):
309 if value == "null":
310 return ""
311 return value
314def get_plugin_lock_path():
315 lock_dir = os.path.join(MFMODULE_RUNTIME_HOME, 'tmp')
316 lock_path = os.path.join(lock_dir, "plugin_management_lock")
317 if not os.path.isdir(lock_dir):
318 mkdir_p_or_die(lock_dir)
319 return lock_path
322def get_current_envs(plugin_name, plugin_home):
323 plugin_label = plugin_name_to_layerapi2_label(plugin_name)
324 return {
325 "%s_CURRENT_PLUGIN_NAME" % MFMODULE: plugin_name,
326 "%s_CURRENT_PLUGIN_DIR" % MFMODULE: plugin_home,
327 "%s_CURRENT_PLUGIN_LABEL" % MFMODULE: plugin_label
328 }
331def get_class_from_fqn(class_fqn):
332 class_name = class_fqn.split('.')[-1]
333 module_path = ".".join(class_fqn.split('.')[0:-1])
334 if module_path == "":
335 raise Exception("incorrect class_fqn: %s" % class_fqn)
336 mod = importlib.import_module(module_path)
337 return getattr(mod, class_name)
340def __get_class(class_arg, env, default):
341 if class_arg is not None:
342 return class_arg
343 if env in os.environ:
344 class_fqn = os.environ[env]
345 return get_class_from_fqn(class_fqn)
346 return default
349def get_configuration_class(configuration_class_arg, default):
350 return __get_class(configuration_class_arg, "MFPLUGIN_CONFIGURATION_CLASS",
351 default)
354def get_app_class(app_class_arg, default):
355 return __get_class(app_class_arg, "MFPLUGIN_APP_CLASS",
356 default)
359def get_extra_daemon_class(extra_daemon_class_arg, default):
360 return __get_class(extra_daemon_class_arg, "MFPLUGIN_EXTRA_DAEMON_CLASS",
361 default)
364def is_jsonable(x):
365 try:
366 json.dumps(x)
367 return True
368 except Exception:
369 return False
372def get_nice_dump(val):
373 def default(o):
374 return f"<<non-serializable: {type(o).__qualname__}"
375 return json.dumps(val, indent=4, default=default)
378def get_configuration_path(plugin_home):
379 return os.path.join(plugin_home, "config.ini")
382def get_configuration_paths(plugin_name, plugin_home):
383 return [
384 get_configuration_path(plugin_home),
385 "%s/config/plugins/%s.ini" % (MFMODULE_RUNTIME_HOME, plugin_name),
386 "/etc/metwork.config.d/%s/plugins/%s.ini" %
387 (MFMODULE_LOWERCASE, plugin_name),
388 ]
391NON_REQUIRED_BOOLEAN = {
392 "required": False,
393 "type": "boolean",
394 "coerce": to_bool
395}
396NON_REQUIRED_BOOLEAN_DEFAULT_FALSE = {
397 **NON_REQUIRED_BOOLEAN,
398 "default": False
399}
400NON_REQUIRED_BOOLEAN_DEFAULT_TRUE = {
401 **NON_REQUIRED_BOOLEAN,
402 "default": True
403}
404NON_REQUIRED_INTEGER = {
405 "required": False,
406 "type": "integer",
407 "coerce": to_int
408}
409NON_REQUIRED_INTEGER_DEFAULT_0 = {
410 **NON_REQUIRED_INTEGER,
411 "default": 0
412}
413NON_REQUIRED_STRING = {
414 "required": False,
415 "type": "string",
416 "coerce": null_to_empty,
417 "default": ""
418}
419NON_REQUIRED_STRING_DEFAULT_EMPTY = {
420 **NON_REQUIRED_STRING,
421 "default": ""
422}
423NON_REQUIRED_STRING_DEFAULT_1 = {
424 **NON_REQUIRED_STRING,
425 "default": "1"
426}