@cryptotaxi247 / netdata-1 / commits / 495b5dba4

feat(python.d): load modules from user plugin directories (NETDATA_USER_PLUGINS_DIRS) (#13214)

Ilya Mashchenko committed Jun 28, 2022 at 10:03 UTC 495b5dba4f27753b156d328502ff6698ba1919db
1 file changed +81 -30
collectors/python.d.plugin/python.d.plugin.in
+81 -30
@@ -31,8 +31,8 @@ import os
31 import pprint
32 import re
33 import sys
34 -import time
34 import threading
35 +import time
36 import types
37
38 try:
@@ -50,6 +50,7 @@ else:
50 ENV_NETDATA_USER_CONFIG_DIR = 'NETDATA_USER_CONFIG_DIR'
51 ENV_NETDATA_STOCK_CONFIG_DIR = 'NETDATA_STOCK_CONFIG_DIR'
52 ENV_NETDATA_PLUGINS_DIR = 'NETDATA_PLUGINS_DIR'
53 +ENV_NETDATA_USER_PLUGINS_DIRS = 'NETDATA_USER_PLUGINS_DIRS'
54 ENV_NETDATA_LIB_DIR = 'NETDATA_LIB_DIR'
55 ENV_NETDATA_UPDATE_EVERY = 'NETDATA_UPDATE_EVERY'
56 ENV_NETDATA_LOCK_DIR = 'NETDATA_LOCK_DIR'
@@ -99,6 +100,9 @@ def dirs():
100 modules_user_config = os.path.join(plugin_user_config, 'python.d')
101 modules_stock_config = os.path.join(plugin_stock_config, 'python.d')
102 modules = os.path.abspath(pluginsd + '/../python.d')
103 + user_modules = [os.path.join(p, 'python.d') for p in
104 + os.getenv(ENV_NETDATA_USER_PLUGINS_DIRS, "").split(" ") if
105 + p]
106
107 Dirs = collections.namedtuple(
108 'Dirs',
@@ -108,6 +112,7 @@ def dirs():
112 'modules_user_config',
113 'modules_stock_config',
114 'modules',
115 + 'user_modules',
116 'var_lib',
117 'locks',
118 ]
@@ -118,6 +123,7 @@ def dirs():
123 modules_user_config,
124 modules_stock_config,
125 modules,
126 + user_modules,
127 var_lib,
128 locks,
129 )
@@ -130,6 +136,28 @@ IS_ATTY = sys.stdout.isatty()
136 MODULE_SUFFIX = '.chart.py'
137
138
139 +def find_available_modules(*directories):
140 + AvailableModule = collections.namedtuple(
141 + 'AvailableModule',
142 + [
143 + 'filepath',
144 + 'name',
145 + ]
146 + )
147 + available = list()
148 + for d in directories:
149 + try:
150 + if not os.path.isdir(d):
151 + continue
152 + files = sorted(os.listdir(d))
153 + except OSError:
154 + continue
155 + modules = [m for m in files if m.endswith(MODULE_SUFFIX)]
156 + available.extend([AvailableModule(os.path.join(d, m), m[:-len(MODULE_SUFFIX)]) for m in modules])
157 +
158 + return available
159 +
160 +
161 def available_modules():
162 obsolete = (
163 'apache_cache', # replaced by web_log
@@ -143,10 +171,17 @@ def available_modules():
171 'unbound', # rewritten in Go
172 )
173
146 - files = sorted(os.listdir(DIRS.modules))
147 - modules = [m[:-len(MODULE_SUFFIX)] for m in files if m.endswith(MODULE_SUFFIX)]
148 - avail = [m for m in modules if m not in obsolete]
149 - return tuple(avail)
174 + stock = [m for m in find_available_modules(DIRS.modules) if m.name not in obsolete]
175 + user = find_available_modules(*DIRS.user_modules)
176 +
177 + available, seen = list(), set()
178 + for m in user + stock:
179 + if m.name in seen:
180 + continue
181 + seen.add(m.name)
182 + available.append(m)
183 +
184 + return available
185
186
187 AVAILABLE_MODULES = available_modules()
@@ -176,9 +211,8 @@ def multi_path_find(name, *paths):
211 return str()
212
213
179 -def load_module(name):
180 - abs_path = os.path.join(DIRS.modules, '{0}{1}'.format(name, MODULE_SUFFIX))
181 - module = SourceFileLoader('pythond_' + name, abs_path)
214 +def load_module(name, filepath):
215 + module = SourceFileLoader('pythond_' + name, filepath)
216 if isinstance(module, types.ModuleType):
217 return module
218 return module.load_module()
@@ -331,12 +365,13 @@ class Job(threading.Thread):
365
366
367 class ModuleSrc:
334 - def __init__(self, name):
335 - self.name = name
368 + def __init__(self, m):
369 + self.name = m.name
370 + self.filepath = m.filepath
371 self.src = None
372
373 def load(self):
339 - self.src = load_module(self.name)
374 + self.src = load_module(self.name, self.filepath)
375
376 def get(self, key):
377 return getattr(self.src, key, None)
@@ -553,37 +588,38 @@ class Plugin:
588 builder.min_update_every = self.min_update_every
589
590 jobs = list()
556 - for mod_name in self.modules_to_run:
557 - if not self.config.is_module_enabled(mod_name):
558 - self.log.info("[{0}] is disabled in the configuration file, skipping it".format(mod_name))
591 + for m in self.modules_to_run:
592 + if not self.config.is_module_enabled(m.name):
593 + self.log.info("[{0}] is disabled in the configuration file, skipping it".format(m.name))
594 continue
595
561 - src = ModuleSrc(mod_name)
596 + src = ModuleSrc(m)
597 try:
598 src.load()
599 except Exception as error:
565 - self.log.warning("[{0}] error on loading source : {1}, skipping it".format(mod_name, repr(error)))
600 + self.log.warning("[{0}] error on loading source : {1}, skipping it".format(m.name, repr(error)))
601 continue
602 + self.log.debug("[{0}] loaded module source : '{1}'".format(m.name, m.filepath))
603
604 if not (src.service() and callable(src.service())):
569 - self.log.warning("[{0}] has no callable Service object, skipping it".format(mod_name))
605 + self.log.warning("[{0}] has no callable Service object, skipping it".format(m.name))
606 continue
607
572 - if src.is_disabled_by_default() and not self.config.is_module_explicitly_enabled(mod_name):
573 - self.log.info("[{0}] is disabled by default, skipping it".format(mod_name))
608 + if src.is_disabled_by_default() and not self.config.is_module_explicitly_enabled(m.name):
609 + self.log.info("[{0}] is disabled by default, skipping it".format(m.name))
610 continue
611
612 builder.module_defaults = src.defaults()
577 - configs = builder.build(mod_name)
613 + configs = builder.build(m.name)
614 if not configs:
579 - self.log.info("[{0}] has no job configs, skipping it".format(mod_name))
615 + self.log.info("[{0}] has no job configs, skipping it".format(m.name))
616 continue
617
618 for config in configs:
619 config['job_name'] = re.sub(r'\s+', '_', config['job_name'])
620 config['override_name'] = re.sub(r'\s+', '_', config.pop('name'))
621
586 - job = Job(src.service(), mod_name, config)
622 + job = Job(src.service(), m.name, config)
623
624 was_previously_active = job_statuses and job_statuses.has(job.module_name, job.real_name)
625 if was_previously_active and job.autodetection_retry == 0:
@@ -811,6 +847,20 @@ def disable():
847 exit(0)
848
849
850 +def get_modules_to_run(cmd):
851 + if not cmd.modules_to_run:
852 + return AVAILABLE_MODULES
853 +
854 + modules_to_run, seen = list(), set()
855 + for m in AVAILABLE_MODULES:
856 + if m.name not in cmd.modules_to_run or m.name in seen:
857 + continue
858 + seen.add(m.name)
859 + modules_to_run.append(m)
860 +
861 + return modules_to_run
862 +
863 +
864 def main():
865 cmd = parse_command_line()
866 log = PythonDLogger()
@@ -822,21 +872,22 @@ def main():
872
873 log.info('using python v{0}'.format(PY_VERSION[0]))
874
825 - unknown = set(cmd.modules_to_run) - set(AVAILABLE_MODULES)
875 + if DIRS.locks and not cmd.nolock:
876 + registry = FileLockRegistry(DIRS.locks)
877 + else:
878 + registry = DummyRegistry()
879 +
880 + unique_avail_module_names = set([m.name for m in AVAILABLE_MODULES])
881 + unknown = set(cmd.modules_to_run) - unique_avail_module_names
882 if unknown:
883 log.error('unknown modules : {0}'.format(sorted(list(unknown))))
828 - guessed = guess_module(AVAILABLE_MODULES, *cmd.modules_to_run)
884 + guessed = guess_module(unique_avail_module_names, *cmd.modules_to_run)
885 if guessed:
886 log.info('probably you meant : \n{0}'.format(pprint.pformat(guessed, width=1)))
887 return
888
833 - if DIRS.locks and not cmd.nolock:
834 - registry = FileLockRegistry(DIRS.locks)
835 - else:
836 - registry = DummyRegistry()
837 -
889 p = Plugin(
839 - cmd.modules_to_run or AVAILABLE_MODULES,
890 + get_modules_to_run(cmd),
891 cmd.update_every,
892 registry,
893 )