#!/usr/bin/python # -*- encoding: utf-8; py-indent-offset: 4 -*- # +------------------------------------------------------------------+ # | ____ _ _ __ __ _ __ | # | / ___| |__ ___ ___| | __ | \/ | |/ / | # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / | # | | |___| | | | __/ (__| < | | | | . \ | # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ | # | | # | Copyright Mathias Kettner 2016 mk@mathias-kettner.de | # +------------------------------------------------------------------+ # # This file is part of Check_MK. # The official homepage is at http://mathias-kettner.de/check_mk. # # check_mk is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation in version 2. check_mk is distributed # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with- # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. See the GNU General Public License for more de- # tails. You should have received a copy of the GNU General Public # License along with GNU Make; see the file COPYING. If not, write # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, # Boston, MA 02110-1301 USA. import os import sys import time import signal import ConfigParser try: # TODO: We should probably ship this package. import pyinotify # pylint: disable=import-error except: sys.stderr.write("Error: Python plugin pyinotify is not installed") sys.exit(1) def usage(): sys.stdout.write("Usage: mk_inotify [-g]\n") sys.stdout.write(" -g: run in foreground\n\n") # Available options: # -g: run in foreground opt_foreground = False if len(sys.argv) == 2 and sys.argv[1] == "-g": opt_foreground = True mk_confdir = os.getenv("MK_CONFDIR") or "/etc/check_mk" mk_vardir = os.getenv("MK_VARDIR") or "/var/lib/check_mk_agent" config_filename = mk_confdir + "/mk_inotify.cfg" configured_paths = mk_vardir + "/mk_inotify.configured" pid_filename = mk_vardir + "/mk_inotify.pid" config = ConfigParser.SafeConfigParser({}) if not os.path.exists(config_filename): sys.exit(0) config_mtime = os.stat(config_filename).st_mtime config.read(config_filename) # Configurable in Agent Bakery heartbeat_timeout = config.getint("global", "heartbeat_timeout") write_interval = config.getint("global", "write_interval") max_messages_per_interval = config.getint("global", "max_messages_per_interval") stats_retention = config.getint("global", "stats_retention") config.remove_section("global") def output_data(): sys.stdout.write("<<>>\n") if os.path.exists(configured_paths): sys.stdout.write(file(configured_paths).read()) now = time.time() for dirpath, _unused_dirnames, filenames in os.walk(mk_vardir): for filename in filenames: if filename.startswith("mk_inotify.stats"): try: the_file = "%s/%s" % (dirpath, filename) filetime = os.stat(the_file).st_mtime file_age = now - filetime if file_age > 5: sys.stdout.write(file(the_file).read()) if file_age > stats_retention: os.unlink(the_file) except: pass break # Check if another mk_inotify process is already running if os.path.exists(pid_filename): pid = file(pid_filename).read() proc_cmdline = "/proc/%s/cmdline" % pid if os.path.exists(proc_cmdline): cmdline = file(proc_cmdline).read() cmdline_tokens = cmdline.split("\0") if "mk_inotify" in cmdline_tokens[1]: # Another mk_notify process is already running.. # Simply output the current statistics and exit output_data() # The pidfile is also the heartbeat file for the running process os.utime(pid_filename, None) sys.exit(0) # .--Fork----------------------------------------------------------------. # | _____ _ | # | | ___|__ _ __| | __ | # | | |_ / _ \| '__| |/ / | # | | _| (_) | | | < | # | |_| \___/|_| |_|\_\ | # | | # +----------------------------------------------------------------------+ # Reaching this point means that no mk_inotify is currently running if not opt_foreground: try: pid = os.fork() if pid > 0: sys.exit(0) # Decouple from parent environment os.chdir("/") os.umask(0) os.setsid() # Close all fd for fd in range(0, 256): try: os.close(fd) except OSError: pass except Exception, e: sys.stderr.write("Error forking mk_inotify: %s" % e) # Save pid of working process. file(pid_filename, "w").write("%d" % os.getpid()) #. # .--Main----------------------------------------------------------------. # | __ __ _ | # | | \/ | __ _(_)_ __ | # | | |\/| |/ _` | | '_ \ | # | | | | | (_| | | | | | | # | |_| |_|\__,_|_|_| |_| | # | | # +----------------------------------------------------------------------+ folder_configs = {} # Computed configuration output = [] # Data to be written to disk def get_watched_files(): files = set([]) for folder, attributes in folder_configs.items(): for filenames in attributes["monitor_files"].values(): for filename in filenames: files.add("configured\tfile\t%s/%s" % (folder, filename)) if attributes.get("monitor_all"): files.add("configured\tfolder\t%s" % (folder)) return files def wakeup_handler(signum, frame): global output if output: if opt_foreground: sys.stdout.write("%s\n" % "\n".join(output)) sys.stdout.write("%s\n" % "\n".join(get_watched_files())) else: filename = "mk_inotify.stats.%d" % time.time() file("%s/%s" % (mk_vardir, filename), "w").write("\n".join(output)+"\n") output = [] # Check if configuration has changed -> restart if (config_mtime != os.stat(config_filename).st_mtime): os.execv(__file__, sys.argv) # Exit on various instances if not opt_foreground: if not os.path.exists(pid_filename): # pidfile is missing sys.exit(0) if time.time() - os.stat(pid_filename).st_mtime > heartbeat_timeout: # heartbeat timeout sys.exit(0) if os.getpid() != int(file(pid_filename).read()): # pidfile differs sys.exit(0) update_watched_folders() signal.alarm(write_interval) def do_output(what, event): if event.dir: return # Only monitor files if len(output) > max_messages_per_interval: last_message = "warning\tMaximum messages reached: %d per %d seconds" % \ (max_messages_per_interval, write_interval) if output[-1] != last_message: output.append(last_message) return path = event.path path_config = folder_configs.get(path) if not path_config: return # shouldn't happen, maybe on subfolders (not supported) filename = os.path.basename(event.pathname) if what in path_config["monitor_all"] or\ filename in path_config["monitor_files"].get(what, []): line = "%d\t%s\t%s" % (time.time(), what, event.pathname) if map_events[what][1]: # Check if filestats are enabled try: stats = os.stat(event.pathname) line += "\t%d\t%d" % (stats.st_size, stats.st_mtime) except Exception: pass output.append(line) if opt_foreground: sys.stdout.write("%s\n" % line) map_events = { # Mode Mask Report_filestats (currently unused) "access" : (pyinotify.IN_ACCESS, False), # pylint: disable=no-member "open" : (pyinotify.IN_OPEN, False), # pylint: disable=no-member "create" : (pyinotify.IN_CREATE, False), # pylint: disable=no-member "delete" : (pyinotify.IN_DELETE, False), # pylint: disable=no-member "modify" : (pyinotify.IN_MODIFY, False), # pylint: disable=no-member "movedto" : (pyinotify.IN_MOVED_TO, False), # pylint: disable=no-member "movedfrom": (pyinotify.IN_MOVED_FROM, False), # pylint: disable=no-member "moveself" : (pyinotify.IN_MOVE_SELF, False), # pylint: disable=no-member } class NotifyEventHandler(pyinotify.ProcessEvent): def process_IN_MOVED_TO(self, event): do_output("movedto", event) def process_IN_MOVED_FROM(self, event): do_output("movedfrom", event) def process_IN_MOVE_SELF(self, event): do_output("moveself", event) # def process_IN_CLOSE_NOWRITE(self, event): # print "CLOSE_NOWRITE event:", event.pathname # # def process_IN_CLOSE_WRITE(self, event): # print "CLOSE_WRITE event:", event.pathname def process_IN_CREATE(self, event): do_output("create", event) def process_IN_DELETE(self, event): do_output("delete", event) def process_IN_MODIFY(self, event): do_output("modify", event) def process_IN_OPEN(self, event): do_output("open", event) # Watch manager wm = pyinotify.WatchManager() def update_watched_folders(): for folder, attributes in folder_configs.items(): if attributes.get("watch_descriptor"): if not wm.get_path(attributes["watch_descriptor"].get(folder)): del attributes["watch_descriptor"] else: if os.path.exists(folder): new_wd = wm.add_watch(folder, attributes["mask"], rec=True) if new_wd.get(folder) > 0: attributes["watch_descriptor"] = new_wd def main(): # Read config for section in config.sections(): section_tokens = section.split("|") folder = section_tokens[0] folder_configs.setdefault(folder, {"add_modes": {}, "del_modes": {}, "all_add_modes": set([]), "all_del_modes": set([])}) files = None if len(section_tokens) > 1: files = set(section_tokens[1:]) add_modes = set([]) del_modes = set([]) for key, value in config.items(section): if key in map_events: if value == "1": add_modes.add(key) else: del_modes.add(key) if files: for mode in add_modes: folder_configs[folder]["add_modes"].setdefault(mode, set([])) folder_configs[folder]["add_modes"][mode].update(files) for mode in del_modes: folder_configs[folder]["del_modes"].setdefault(mode, set([])) folder_configs[folder]["del_modes"][mode].update(files) else: folder_configs[folder]["all_add_modes"].update(add_modes) folder_configs[folder]["all_del_modes"].update(del_modes) # Evaluate config for folder, attributes in folder_configs.items(): required_modes = set([]) for mode in attributes["add_modes"].keys(): if mode not in attributes["all_del_modes"]: required_modes.add(mode) files_to_monitor = {} skip_modes = set([]) for mode in required_modes: files_to_monitor.setdefault(mode, set([])) files_to_monitor[mode].update(attributes["add_modes"][mode]) files_to_monitor[mode] -= attributes["del_modes"].get(mode, set([])) if not files_to_monitor[mode]: skip_modes.add(mode) attributes["monitor_files"] = files_to_monitor attributes["monitor_all"] = attributes["all_add_modes"] - attributes["all_del_modes"] attributes["modes"] = required_modes - skip_modes # Determine mask attributes["mask"] = 0 for mode in attributes["modes"]: attributes["mask"] |= map_events[mode][0] for mode in attributes["monitor_all"]: attributes["mask"] |= map_events[mode][0] update_watched_folders() if opt_foreground: import pprint sys.stdout.write(pprint.pformat(folder_configs)) # Save monitored file/folder information specified in mk_inotify.cfg file(configured_paths, "w").write("\n".join(get_watched_files())+"\n") # Event handler eh = NotifyEventHandler() notifier = pyinotify.Notifier(wm, eh) # Wake up every few seconds, check heartbeat and write data to disk signal.signal(signal.SIGALRM, wakeup_handler) signal.alarm(write_interval) notifier.loop() if __name__ == '__main__': main()