from __future__ import absolute_import from builtins import object import daemon import pid as pidfile import os import signal import sys import logging import vaping import vaping.config import vaping.io from vaping import plugin class PluginContext(object): """ Context to pass to plugins for getting extra information """ def __init__(self, config): # probably should be a deep copy for security from plugins self.__config = config.copy() @property def config(self): """ config """ return self.__config class Vaping(object): """ Vaping daemon class """ def __init__(self, config=None, config_dir=None): """ must either pass config as a dict or vaping.config.Config or config_dir as a path to where the config dir is located """ if config: if isinstance(config, dict): self.config = vaping.Config(data=config) else: if not config.meta: raise ValueError("config was not specified or empty") self.config = config elif config_dir: self.config = vaping.Config(read=config_dir) else: raise ValueError("config was not specified or empty") self.joins = [] self._logger = None self.plugin_context = PluginContext(self.config) vcfg = self.config.get('vaping', None) if not vcfg: vcfg = dict() # get either home_dir from config, or use config_dir self.home_dir = vcfg.get('home_dir', None) if not self.home_dir: self.home_dir = self.config.meta['config_dir'] if not os.path.exists(self.home_dir): raise ValueError("home directory '{}' does not exist".format(self.home_dir)) if not os.access(self.home_dir, os.W_OK): raise ValueError("home directory '{}' is not writable".format(self.home_dir)) # change to home for working dir os.chdir(self.home_dir) # instantiate all defined plugins # TODO remove and let them lazy init? plugins = self.config.get('plugins', None) if not plugins: raise ValueError('no plugins specified') plugin.instantiate(self.config['plugins'], self.plugin_context) # check that probes don't name clash with plugins for probe in self.config.get('probes', []): if plugin.exists(probe["name"]): raise ValueError("probes may not share names with plugins ({})".format(probe["name"])) # TODO move to daemon pidname = vcfg.get('pidfile', 'vaping.pid') self.pidfile = pidfile.PidFile(pidname=pidname, piddir=self.home_dir) @property def log(self): """ logger instance """ if not self._logger: self._logger = logging.getLogger(__name__) return self._logger def _exec(self, detach=True): """ daemonize and exec main() """ kwargs = { 'pidfile': self.pidfile, 'working_directory': self.home_dir, } # FIXME - doesn't work if not detach: kwargs.update({ 'detach_process': False, 'files_preserve': [0,1,2], 'stdout': sys.stdout, 'stderr': sys.stderr, }) ctx = daemon.DaemonContext(**kwargs) with ctx: self._main() def _main(self): """ process """ probes = self.config.get('probes', None) if not probes: raise ValueError('no probes specified') for probe_config in self.config['probes']: probe = plugin.get_probe(probe_config, self.plugin_context) # FIXME - needs to check for output defined in plugin if 'output' not in probe_config: raise ValueError("no output specified") # get all output targets and start / join them for output_name in probe_config['output']: output = plugin.get_output(output_name, self.plugin_context) if not output.started: output.start() self.joins.append(output) probe._emit.append(output) probe.start() self.joins.append(probe) vaping.io.joinall(self.joins) return 0 def start(self): """ start daemon """ self._exec() def stop(self): """ stop daemon """ try: with self.pidfile: self.log.error("failed to stop, missing pid file or not running") except pidfile.PidFileError: # this isn't exposed in pidfile :o with open(self.pidfile.filename) as fobj: pid = int(fobj.readline().rstrip()) if not pid: self.log.error("failed to read pid from file") self.log.info("killing %d", pid) os.kill(pid, signal.SIGTERM) def run(self): """ run daemon """ # FIXME - not detaching doesn't work, just run directly for now # self._exec(detach=False) try: with self.pidfile: return self._main() except pidfile.PidFileError: # this isn't exposed in pidfile :o self.log.error("failed to get pid lock, already running?") return 1 finally: # call on_stop to let them clean up for mod in self.joins: self.log.debug("stopping %s", mod.name) mod.on_stop()