358 lines
14 KiB
Python
Executable File
358 lines
14 KiB
Python
Executable File
#!/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("<<<inotify:sep(9)>>>\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()
|