]> arthur.barton.de Git - netdata.git/blobdiff - plugins.d/python.d.plugin
ab-debian 0.20170327.01-0ab1, upstream v1.6.0-42-gaa6b96fc
[netdata.git] / plugins.d / python.d.plugin
index 5e720a904b233713395fdff6d780546392bb8e45..efa62cbc5a34affbd86651147a50e08871ff7e0f 100755 (executable)
-#!/usr/bin/python3 -u
+#!/usr/bin/env bash
+'''':; exec "$(command -v python || command -v python3 || command -v python2 || echo "ERROR python IS NOT AVAILABLE IN THIS SYSTEM")" "$0" "$@" # '''
+# -*- coding: utf-8 -*-
+
+# Description: netdata python modules supervisor
+# Author: Pawel Krupa (paulfantom)
 
 import os
 import sys
 import time
-#import configparser
-#import importlib.machinery
+import threading
+from re import sub
+
+# -----------------------------------------------------------------------------
+# globals & environment setup
+# https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
+MODULE_EXTENSION = ".chart.py"
+BASE_CONFIG = {'update_every': os.getenv('NETDATA_UPDATE_EVERY', 1),
+               'priority': 90000,
+               'retries': 10}
+
+MODULES_DIR = os.path.abspath(os.getenv('NETDATA_PLUGINS_DIR',
+                                        os.path.dirname(__file__)) + "/../python.d") + "/"
+CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
+# directories should end with '/'
+if CONFIG_DIR[-1] != "/":
+    CONFIG_DIR += "/"
+sys.path.append(MODULES_DIR + "python_modules")
+
+PROGRAM = os.path.basename(__file__).replace(".plugin", "")
+DEBUG_FLAG = False
+TRACE_FLAG = False
+OVERRIDE_UPDATE_EVERY = False
+
+# -----------------------------------------------------------------------------
+# custom, third party and version specific python modules management
+import msg
+
 try:
-    assert sys.version_info >= (3,1)
-    import configparser
+    assert sys.version_info >= (3, 1)
     import importlib.machinery
-except AssertionError:
-    sys.stderr.write('Not supported python version. Needed python >= 3.1\n')
-    sys.stdout.write('DISABLE\n')
-    sys.exit(1)
+    PY_VERSION = 3
+    # change this hack below if we want PY_VERSION to be used in modules
+    # import builtins
+    # builtins.PY_VERSION = 3
+    msg.info('Using python v3')
+except (AssertionError, ImportError):
+    try:
+        import imp
+
+        # change this hack below if we want PY_VERSION to be used in modules
+        # import __builtin__
+        # __builtin__.PY_VERSION = 2
+        PY_VERSION = 2
+        msg.info('Using python v2')
+    except ImportError:
+        msg.fatal('Cannot start. No importlib.machinery on python3 or lack of imp on python2')
+# try:
+#     import yaml
+# except ImportError:
+#     msg.fatal('Cannot find yaml library')
+try:
+    if PY_VERSION == 3:
+        import pyyaml3 as yaml
+    else:
+        import pyyaml2 as yaml
+except ImportError:
+    msg.fatal('Cannot find yaml library')
+
+try:
+    from collections import OrderedDict
+    ORDERED = True
+    DICT = OrderedDict
+    msg.info('YAML output is ordered')
+except ImportError:
+    try:
+        from ordereddict import OrderedDict
+        ORDERED = True
+        DICT = OrderedDict
+        msg.info('YAML output is ordered')
+    except ImportError:
+        ORDERED = False
+        DICT = dict
+        msg.info('YAML output is unordered')
+if ORDERED:
+    def ordered_load(stream, Loader=yaml.Loader, object_pairs_hook=OrderedDict):
+        class OrderedLoader(Loader):
+            pass
+
+        def construct_mapping(loader, node):
+            loader.flatten_mapping(node)
+            return object_pairs_hook(loader.construct_pairs(node))
+        OrderedLoader.add_constructor(
+            yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
+            construct_mapping)
+        return yaml.load(stream, OrderedLoader)
 
 
 class PythonCharts(object):
-    
+    """
+    Main class used to control every python module.
+    """
+
     def __init__(self,
-                 interval=None,
-                 modules=[],
+                 modules=None,
                  modules_path='../python.d/',
-                 modules_configs='../conf.d/'):
+                 modules_configs='../conf.d/',
+                 modules_disabled=None,
+                 modules_enabled=None,
+                 default_run=None):
+        """
+        :param modules: list
+        :param modules_path: str
+        :param modules_configs: str
+        :param modules_disabled: list
+        :param modules_enabled: list
+        :param default_run: bool
+        """
+
+        if modules is None:
+            modules = []
+        if modules_disabled is None:
+            modules_disabled = []
+
         self.first_run = True
-        if interval is None:
-            interval = 1
-        self.default_priority = 90000
-        # check if plugin directory exists
-        if not os.path.isdir(modules_path):
-            debug("cannot find charts directory ", modules_path)
-            sys.stdout.write("DISABLE\n")
-            sys.exit(1)
+        # set configuration directory
         self.configs = modules_configs
 
-        self.modules = []
-        if len(modules) > 0:
-            for m in modules:
-                mod = self.import_plugin(modules_path + m + ".chart.py")
-                if mod is not None:
-                    self.modules.append(mod)
-            if len(self.modules) == 0:
-                debug("cannot find provided module(s)", modules_path)
-                sys.stdout.write("DISABLE\n")
-                sys.exit(1)
-        else:
-            self.load_modules(modules_path)
-            if len(self.modules) == 0:
-                debug("cannot find modules directory", modules_path)
-                sys.stdout.write("DISABLE\n")
-                sys.exit(1)
-
-        self.load_configs()
-
-        # set timetable dict (last execution, next execution and frequency)
-        # set priorities
-        self.timetable = {}
-        for m in self.modules:
-            try:
-                m.priority = int(m.priority)
-            except (AttributeError,ValueError):
-                m.priority = self.default_priority
-             
-            if interval is None:
-                try:
-                    interval = int(m.update_every)
-                except (AttributeError, ValueError):
-                    interval = 1
-            
-            now = time.time()
-            self.timetable[m.__name__] = {'last' : now,
-                                          'next' : now - (now % interval) + interval,
-                                          'freq' : interval}
-
-    def import_plugin(self, path, name=None):
+        # load modules
+        loaded_modules = self._load_modules(modules_path, modules, modules_disabled, modules_enabled, default_run)
+
+        # load configuration files
+        configured_modules = self._load_configs(loaded_modules)
+
+        # good economy and prosperity:
+        self.jobs = self._create_jobs(configured_modules)  # type <list>
+
+        # enable timetable override like `python.d.plugin mysql debug 1`
+        if DEBUG_FLAG and OVERRIDE_UPDATE_EVERY:
+            for job in self.jobs:
+                job.create_timetable(BASE_CONFIG['update_every'])
+
+    @staticmethod
+    def _import_module(path, name=None):
+        """
+        Try to import module using only its path.
+        :param path: str
+        :param name: str
+        :return: object
+        """
+
         if name is None:
             name = path.split('/')[-1]
-            if name[-9:] != ".chart.py":
+            if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
                 return None
-            name = name[:-9]
+            name = name[:-len(MODULE_EXTENSION)]
         try:
-            return importlib.machinery.SourceFileLoader(name, path).load_module()
+            if PY_VERSION == 3:
+                return importlib.machinery.SourceFileLoader(name, path).load_module()
+            else:
+                return imp.load_source(name, path)
         except Exception as e:
-            debug(str(e))
+            msg.error("Problem loading", name, str(e))
             return None
 
-    def load_modules(self, path):
-        names = os.listdir(path)
-        for mod in names:
-            m = self.import_plugin(path + mod)
-            if m is not None:
-                debug("loading chart: '" + path + mod + "'")
-                self.modules.append(m)
-
-    def load_configs(self):
-        for m in self.modules:
-            configfile = self.configs + m.__name__ + ".conf"
+    def _load_modules(self, path, modules, disabled, enabled, default_run):
+        """
+        Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
+        :param path: str
+        :param modules: list
+        :param disabled: list
+        :return: list
+        """
+
+        # check if plugin directory exists
+        if not os.path.isdir(path):
+            msg.fatal("cannot find charts directory ", path)
+
+        # load modules
+        loaded = []
+        if len(modules) > 0:
+            for m in modules:
+                if m in disabled:
+                    continue
+                mod = self._import_module(path + m + MODULE_EXTENSION)
+                if mod is not None:
+                    loaded.append(mod)
+                else:  # exit if plugin is not found
+                    msg.fatal('no modules found.')
+        else:
+            # scan directory specified in path and load all modules from there
+            if default_run is False:
+                names = [module for module in os.listdir(path) if module[:-9] in enabled]
+            else:
+                names = os.listdir(path)
+            for mod in names:
+                if mod.replace(MODULE_EXTENSION, "") in disabled:
+                    msg.error(mod + ": disabled module ", mod.replace(MODULE_EXTENSION, ""))
+                    continue
+                m = self._import_module(path + mod)
+                if m is not None:
+                    msg.debug(mod + ": loading module '" + path + mod + "'")
+                    loaded.append(m)
+        return loaded
+
+    def _load_configs(self, modules):
+        """
+        Append configuration in list named `config` to every module.
+        For multi-job modules `config` list is created in _parse_config,
+        otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
+        :param modules: list
+        :return: list
+        """
+        for mod in modules:
+            configfile = self.configs + mod.__name__ + ".conf"
             if os.path.isfile(configfile):
-                debug("loading chart options: '" + configfile + "'")
-                for k, v in read_config(configfile).items():
-                    setattr(m, k, v)
+                msg.debug(mod.__name__ + ": loading module configuration: '" + configfile + "'")
+                try:
+                    if not hasattr(mod, 'config'):
+                        mod.config = {}
+                    setattr(mod,
+                            'config',
+                            self._parse_config(mod, read_config(configfile)))
+                except Exception as e:
+                    msg.error(mod.__name__ + ": cannot parse configuration file '" + configfile + "':", str(e))
             else:
-                debug(m.__name__ +
-                      ": configuration file '" +
-                      configfile +
-                      "' not found. Using defaults.")
-
-    def disable_module(self, mod, reason=None):
-        self.modules.remove(mod)
-        del self.timetable[mod.__name__]
+                msg.error(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.")
+                # set config if not found
+                if not hasattr(mod, 'config'):
+                    msg.debug(mod.__name__ + ": setting configuration for only one job")
+                    mod.config = {None: {}}
+                    for var in BASE_CONFIG:
+                        try:
+                            mod.config[None][var] = getattr(mod, var)
+                        except AttributeError:
+                            mod.config[None][var] = BASE_CONFIG[var]
+        return modules
+
+    @staticmethod
+    def _parse_config(module, config):
+        """
+        Parse configuration file or extract configuration from module file.
+        Example of returned dictionary:
+            config = {'name': {
+                            'update_every': 2,
+                            'retries': 3,
+                            'priority': 30000
+                            'other_val': 123}}
+        :param module: object
+        :param config: dict
+        :return: dict
+        """
+        if config is None:
+            config = {}
+        # get default values
+        defaults = {}
+        msg.debug(module.__name__ + ": reading configuration")
+        for key in BASE_CONFIG:
+            try:
+                # get defaults from module config
+                defaults[key] = int(config.pop(key))
+            except (KeyError, ValueError):
+                try:
+                    # get defaults from module source code
+                    defaults[key] = getattr(module, key)
+                except (KeyError, ValueError, AttributeError):
+                    # if above failed, get defaults from global dict
+                    defaults[key] = BASE_CONFIG[key]
+
+        # check if there are dict in config dict
+        many_jobs = False
+        for name in config:
+            if isinstance(config[name], DICT):
+                many_jobs = True
+                break
+
+        # assign variables needed by supervisor to every job configuration
+        if many_jobs:
+            for name in config:
+                for key in defaults:
+                    if key not in config[name]:
+                        config[name][key] = defaults[key]
+        # if only one job is needed, values doesn't have to be in dict (in YAML)
+        else:
+            config = {None: config.copy()}
+            config[None].update(defaults)
+
+        # return dictionary of jobs where every job has BASE_CONFIG variables
+        return config
+
+    @staticmethod
+    def _create_jobs(modules):
+        """
+        Create jobs based on module.config dictionary and module.Service class definition.
+        :param modules: list
+        :return: list
+        """
+        jobs = []
+        for module in modules:
+            for name in module.config:
+                # register a new job
+                conf = module.config[name]
+                try:
+                    job = module.Service(configuration=conf, name=name)
+                except Exception as e:
+                    msg.error(module.__name__ +
+                              ("/" + str(name) if name is not None else "") +
+                              ": cannot start job: '" +
+                              str(e))
+                    return None
+                else:
+                    # set chart_name (needed to plot run time graphs)
+                    job.chart_name = module.__name__
+                    if name is not None:
+                        job.chart_name += "_" + name
+                jobs.append(job)
+                msg.debug(module.__name__ + ("/" + str(name) if name is not None else "") + ": job added")
+
+        return [j for j in jobs if j is not None]
+
+    def _stop(self, job, reason=None):
+        """
+        Stop specified job and remove it from self.jobs list
+        Also notifies user about job failure if DEBUG_FLAG is set
+        :param job: object
+        :param reason: str
+        """
+        prefix = job.__module__
+        if job.name is not None and len(job.name) != 0:
+            prefix += "/" + job.name
+        try:
+            msg.error("DISABLED:", prefix)
+            self.jobs.remove(job)
+        except Exception as e:
+            msg.debug("This shouldn't happen. NO " + prefix + " IN LIST:" + str(self.jobs) + " ERROR: " + str(e))
+
+        # TODO remove section below and remove `reason`.
+        prefix += ": "
         if reason is None:
             return
         elif reason[:3] == "no ":
-            debug("chart '" +
-                  mod.__name__,
-                  "' does not seem to have " +
-                  reason[3:] +
-                  "() function. Disabling it.")
+            msg.error(prefix +
+                      "does not seem to have " +
+                      reason[3:] +
+                      "() function. Disabling it.")
         elif reason[:7] == "failed ":
-            debug("chart '" +
-                  mod.__name__ +
-                  reason[3:] +
-                  "() function. reports failure.")
+            msg.error(prefix +
+                      reason[7:] +
+                      "() function reports failure.")
         elif reason[:13] == "configuration":
-            debug(mod.__name__,
-                  "configuration file '" +
-                  self.configs +
-                  mod.__name__ +
-                  ".conf' not found. Using defaults.")
+            msg.error(prefix +
+                      "configuration file '" +
+                      self.configs +
+                      job.__module__ +
+                      ".conf' not found. Using defaults.")
         elif reason[:11] == "misbehaving":
-            debug(mod.__name__, "is "+reason)
+            msg.error(prefix + "is " + reason)
 
     def check(self):
-        for mod in self.modules:
+        """
+        Tries to execute check() on every job.
+        This cannot fail thus it is catching every exception
+        If job.check() fails job is stopped
+        """
+        i = 0
+        overridden = []
+        msg.debug("all job objects", str(self.jobs))
+        while i < len(self.jobs):
+            job = self.jobs[i]
             try:
-                if not mod.check():
-                    self.disable_module(mod, "failed check")
-            except AttributeError:
-                self.disable_module(mod, "no check")
+                if not job.check():
+                    msg.error(job.chart_name, "check() failed - disabling job")
+                    self._stop(job)
+                else:
+                    msg.info("CHECKED OK:", job.chart_name)
+                    i += 1
+                    try:
+                        if job.override_name is not None:
+                            new_name = job.__module__ + '_' + sub(r'\s+', '_', job.override_name)
+                            if new_name in overridden:
+                                msg.info("DROPPED:", job.name, ", job '" + job.override_name +
+                                         "' is already served by another job.")
+                                self._stop(job)
+                                i -= 1
+                            else:
+                                job.name = job.override_name
+                                msg.info("RENAMED:", new_name, ", from " + job.chart_name)
+                                job.chart_name = new_name
+                                overridden.append(job.chart_name)
+                    except Exception:
+                        pass
+            except AttributeError as e:
+                self._stop(job)
+                msg.error(job.chart_name, "cannot find check() function or it thrown unhandled exception.")
+                msg.debug(str(e))
             except (UnboundLocalError, Exception) as e:
-                self.disable_module(mod, "misbehaving. Reason: " + str(e))
+                msg.error(job.chart_name, str(e))
+                self._stop(job)
+        msg.debug("overridden job names:", str(overridden))
+        msg.debug("all remaining job objects:", str(self.jobs))
 
     def create(self):
-        for mod in self.modules:
+        """
+        Tries to execute create() on every job.
+        This cannot fail thus it is catching every exception.
+        If job.create() fails job is stopped.
+        This is also creating job run time chart.
+        """
+        i = 0
+        while i < len(self.jobs):
+            job = self.jobs[i]
             try:
-                if not mod.create():
-                    self.disable_module(mod, "failed create")
+                if not job.create():
+                    msg.error(job.chart_name, "create function failed.")
+                    self._stop(job)
                 else:
-                    chart = mod.__name__
+                    chart = job.chart_name
                     sys.stdout.write(
                         "CHART netdata.plugin_pythond_" +
                         chart +
                         " '' 'Execution time for " +
                         chart +
                         " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
-                        str(self.timetable[mod.__name__]['freq']) +
+                        str(job.timetable['freq']) +
                         '\n')
                     sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
-                    sys.stdout.flush()
+                    msg.debug("created charts for", job.chart_name)
+                    # sys.stdout.flush()
+                    i += 1
             except AttributeError:
-                self.disable_module(mod, "no create")
+                msg.error(job.chart_name, "cannot find create() function or it thrown unhandled exception.")
+                self._stop(job)
             except (UnboundLocalError, Exception) as e:
-                self.disable_module(mod, "misbehaving. Reason: " + str(e))
-
-    def update_module(self, mod):
-        t_start = time.time()
-        # check if it is time to execute module update() function
-        if self.timetable[mod.__name__]['next'] > t_start:
-            return
-        try:
-            if self.first_run:
-                since_last = 0
-            else:
-                since_last = int((t_start - self.timetable[mod.__name__]['last']) * 1000000)
-            if not mod.update(since_last):
-                self.disable_module(mod, "update failed")
-                return
-        except AttributeError:
-            self.disable_module(mod, "no update")
-            return
-        except (UnboundLocalError, Exception) as e:
-            self.disable_module(mod, "misbehaving. Reason: " + str(e))
-            return
-        t_end = time.time()
-        self.timetable[mod.__name__]['next'] = t_end - (t_end % self.timetable[mod.__name__]['freq']) + self.timetable[mod.__name__]['freq']
-        if self.first_run:
-            dt = 0
-        else:
-            dt = int((t_end - self.timetable[mod.__name__]['last']) * 1000000)
-        sys.stdout.write("BEGIN netdata.plugin_pythond_"+mod.__name__+" "+str(since_last)+'\n')
-        sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
-        sys.stdout.write("END\n")
-        sys.stdout.flush()
-        self.timetable[mod.__name__]['last'] = t_start
-        self.first_run = False
+                msg.error(job.chart_name, str(e))
+                self._stop(job)
 
     def update(self):
-        self.first_run = True
+        """
+        Creates and supervises every job thread.
+        This will stay forever and ever and ever forever and ever it'll be the one...
+        """
+        for job in self.jobs:
+            job.start()
+
         while True:
-            t_begin = time.time()
-            next_runs = []
-            for mod in self.modules:
-                self.update_module(mod)
-                try:
-                    next_runs.append(self.timetable[mod.__name__]['next'])
-                except KeyError:
-                    pass
-            if len(next_runs) == 0:
-                debug("No plugins loaded")
-                sys.stdout.write("DISABLE\n")
-                sys.exit(1)
-            time.sleep(min(next_runs) - time.time())
+            if threading.active_count() <= 1:
+                msg.fatal("no more jobs")
+            time.sleep(1)
 
 
 def read_config(path):
-    config = configparser.ConfigParser()
-    config_str = ""
-    try:
-        with open(path, 'r', encoding="utf_8") as f:
-            config_str = '[config]\n' + f.read()
-    except IsADirectoryError:
-        debug(str(path), "is a directory")
-        return
+    """
+    Read YAML configuration from specified file
+    :param path: str
+    :return: dict
+    """
     try:
-        config.read_string(config_str)
-    except configparser.ParsingError as e:
-        debug("Malformed configuration file: "+str(e))
-        return
-    return dict(config.items('config'))
-
-
-def debug(*args):
-    if not DEBUG_FLAG:
-        return
-    sys.stderr.write(PROGRAM + ":")
-    for i in args:
-        sys.stderr.write(" " + str(i))
-    sys.stderr.write("\n")
-    sys.stderr.flush()
+        with open(path, 'r') as stream:
+            if ORDERED:
+                config = ordered_load(stream, yaml.SafeLoader)
+            else:
+                config = yaml.load(stream)
+    except (OSError, IOError):
+        msg.error(str(path), "is not a valid configuration file")
+        return None
+    except yaml.YAMLError as e:
+        msg.error(str(path), "is malformed:", e)
+        return None
+    return config
 
 
 def parse_cmdline(directory, *commands):
-    # TODO number -> interval
-    global DEBUG_FLAG
-    DEBUG_FLAG = False
-    interval = None
-
+    """
+    Parse parameters from command line.
+    :param directory: str
+    :param commands: list of str
+    :return: dict
+    """
+    global DEBUG_FLAG, TRACE_FLAG
+    global OVERRIDE_UPDATE_EVERY
+    global BASE_CONFIG
+
+    changed_update = False
     mods = []
     for cmd in commands[1:]:
         if cmd == "check":
@@ -252,89 +488,109 @@ def parse_cmdline(directory, *commands):
         elif cmd == "debug" or cmd == "all":
             DEBUG_FLAG = True
             # redirect stderr to stdout?
+        elif cmd == "trace" or cmd == "all":
+            TRACE_FLAG = True
         elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
-            DEBUG_FLAG = True
+            DEBUG_FLAG = True
             mods.append(cmd.replace(".chart.py", ""))
         else:
             try:
-                interval = int(cmd)
+                BASE_CONFIG['update_every'] = int(cmd)
+                changed_update = True
             except ValueError:
                 pass
+    if changed_update and DEBUG_FLAG:
+        OVERRIDE_UPDATE_EVERY = True
+        msg.debug(PROGRAM, "overriding update interval to", str(BASE_CONFIG['update_every']))
 
-    debug("started from", commands[0], "with options:", *commands[1:])
+    msg.debug("started from", commands[0], "with options:", *commands[1:])
 
-    return {'interval': interval,
-            'modules': mods}
+    return mods
 
 
 # if __name__ == '__main__':
 def run():
-    global DEBUG_FLAG, PROGRAM
-    DEBUG_FLAG = True
-    PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
-    # parse env variables
-    # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
-    main_dir = os.getenv('NETDATA_PLUGINS_DIR',
-                         os.path.abspath(__file__).strip("python.d.plugin.py"))
-    config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
-    interval = int(os.getenv('NETDATA_UPDATE_EVERY', 1))
+    """
+    Main program.
+    """
+    global DEBUG_FLAG, TRACE_FLAG, BASE_CONFIG
 
     # read configuration file
-    if config_dir[-1] != '/':
-        configfile = config_dir + '/' + "python.d.conf"
-    else:
-        configfile = config_dir + "python.d.conf"
-
-    try:
-        conf = read_config(configfile)
+    disabled = ['nginx_log', 'gunicorn_log']
+    enabled = list()
+    default_run = True
+    configfile = CONFIG_DIR + "python.d.conf"
+    msg.PROGRAM = PROGRAM
+    msg.info("reading configuration file:", configfile)
+    log_throttle = 200
+    log_interval = 3600
+
+    conf = read_config(configfile)
+    if conf is not None:
         try:
-            if str(conf['enable']) == "no":
-                debug("disabled in configuration file")
-                sys.stdout.write("DISABLE\n")
-                sys.exit(1)
+            # exit the whole plugin when 'enabled: no' is set in 'python.d.conf'
+            if conf['enabled'] is False:
+                msg.fatal('disabled in configuration file.\n')
         except (KeyError, TypeError):
             pass
+
         try:
-            modules_conf = conf['plugins_config_dir']
+            for param in BASE_CONFIG:
+                BASE_CONFIG[param] = conf[param]
         except (KeyError, TypeError):
-            modules_conf = config_dir  # default configuration directory
+            pass  # use default update_every from NETDATA_UPDATE_EVERY
+
         try:
-            modules_dir = conf['plugins_dir']
+            DEBUG_FLAG = conf['debug']
         except (KeyError, TypeError):
-            modules_dir = main_dir.replace("plugins.d", "python.d")
+            pass
+
         try:
-            interval = int(conf['interval'])
+            TRACE_FLAG = conf['trace']
         except (KeyError, TypeError):
-            pass  # default interval
+            pass
+
         try:
-            DEBUG_FLAG = bool(conf['debug'])
+            log_throttle = conf['logs_per_interval']
         except (KeyError, TypeError):
             pass
-    except FileNotFoundError:
-        modules_conf = config_dir
-        modules_dir = main_dir.replace("plugins.d", "python.d")
 
-    # directories should end with '/'
-    if modules_dir[-1] != '/':
-        modules_dir += "/"
-    if modules_conf[-1] != '/':
-        modules_conf += "/"
+        try:
+            log_interval = conf['log_interval']
+        except (KeyError, TypeError):
+            pass
 
-    # parse passed command line arguments
-    out = parse_cmdline(modules_dir, *sys.argv)
-    modules = out['modules']
-#    if out['interval'] is not None:
-#        interval = out['interval']
+        default_run = True if ('default_run' not in conf or conf.get('default_run')) else False
 
-    # configure environement to run modules
-    #sys.path.append(modules_dir+"python_modules") # append path to directory with modules dependencies
+        for k, v in conf.items():
+            if k in ("update_every", "debug", "enabled", "default_run"):
+                continue
+            if default_run:
+                if v is False:
+                    disabled.append(k)
+            else:
+                if v is True:
+                    enabled.append(k)
+    # parse passed command line arguments
+    modules = parse_cmdline(MODULES_DIR, *sys.argv)
+    msg.DEBUG_FLAG = DEBUG_FLAG
+    msg.TRACE_FLAG = TRACE_FLAG
+    msg.LOG_THROTTLE = log_throttle
+    msg.LOG_INTERVAL = log_interval
+    msg.LOG_COUNTER = 0
+    msg.LOG_NEXT_CHECK = 0
+    msg.info("MODULES_DIR='" + MODULES_DIR +
+             "', CONFIG_DIR='" + CONFIG_DIR +
+             "', UPDATE_EVERY=" + str(BASE_CONFIG['update_every']) +
+             ", ONLY_MODULES=" + str(modules))
 
     # run plugins
-#    charts = PythonCharts(interval, modules, modules_dir, modules_conf)
-    charts = PythonCharts(out['interval'], modules, modules_dir, modules_conf)
+    charts = PythonCharts(modules, MODULES_DIR, CONFIG_DIR + "python.d/", disabled, enabled, default_run)
     charts.check()
     charts.create()
     charts.update()
+    msg.fatal("finished")
+
 
 if __name__ == '__main__':
     run()