Refactoring: Changed all check parameters starting with a 'p' to the new rulespec...
[check_mk.git] / scripts / autodetect.py
blob9f8b822c186f5e291e4b6e603cd581faf18d280b
1 #!/usr/bin/env python
2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
9 # | |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 import os, sys, stat
29 opt_debug = "-d" in sys.argv or "--debug" in sys.argv
31 # The following settings are tried to be autodetected
32 target_values = {
33 'apache_config_dir': "Configuration directory of Apache",
34 'cgiurl': "URL of Nagios CGI programs",
35 'check_icmp_path': "Path to check_icmp Plugin",
36 'htdocsdir': "Directory of Nagios' static web pages",
37 'htpasswd_file': "File of Nagios' HTTP users and passwords",
38 'livestatus_in_nagioscfg': "Wether nagios.cfg loads livestatus module",
39 'nagconfdir': "Directory of Nagios objects (see cfg_dir)",
40 'nagiosaddconf': "Snippet to add to nagios.cfg",
41 'nagios_auth_name': "HTTP Basic AuthName for Nagios",
42 'nagios_binary': "Absolute path to Nagios binary itself",
43 'nagios_version': "Nagios version",
44 'nagios_config_file': "Absolute path to nagios.cfg",
45 'nagios_startscript': "Nagios startskript (usually in /etc/init.d)",
46 'nagios_status_file': "Absolute path to Nagios' status.dat",
47 'nagiosurl': "Base-URL of Nagios web pages",
48 'nagiosuser': "System user running the Nagios process",
49 'nagpipe': "Absolute path to Nagios' command pipe (nagios.cmd)",
50 'check_result_path': "Absolute path to Nagios' checkresults directory",
51 'pnp_url': "URL of PNP4Nagios",
52 'pnpconffile': "PNP4Nagios configuration file for its PHP pages",
53 'pnphtdocsdir': "PNP4Nagios www document root directory",
54 'pnptemplates': "directory of PHP templates for PNP4Nagios",
55 'rrddir': "Base directory where RRDs are stored",
56 'wwwgroup': "Common group of Nagios and Apache",
57 'wwwuser': "System user apache runs with",
60 # Ich suche nach Prozessen mit folgenden Kriterien:
61 # - Der Text 'nagios' im Namen
62 # - eines der Worte muss '-d' oder '--daemon' sein.
63 # - Das letzte Wort ist der Name einer existierenden Datei
64 # Beispiel:
65 # /usr/sbin/nagios3 -d /etc/nagios3/nagios.cfg
68 class Sorry(Exception):
69 def __init__(self, reason):
70 self.reason = reason
71 super(Sorry, self).__init__(reason)
74 def find_pid_and_configfile():
75 procs = os.popen("ps ax -o pid,ppid,user,command").readlines()
76 pids = []
77 for line in procs:
78 if line.find('nagios.cfg') >= 0 or line.find('icinga.cfg') >= 0:
79 pids.append(line.split()[0])
81 for line in procs:
82 if line.find('nagios.cfg') >= 0 or line.find('icinga.cfg') >= 0:
83 try:
84 words = line.split()
85 if '-d' in words or '--daemon' in words:
86 pid = words[0]
87 ppid = words[1]
88 user = words[2]
89 configfile = words[-1]
90 if ppid in pids:
91 continue # this is not the main thread. It has
92 # another process as parent!
93 if os.path.exists(configfile):
94 return int(pid), user, configfile
95 except Exception as e:
96 if opt_debug:
97 raise
99 raise Sorry("Cannot find Nagios/Icinga process. Is it running?")
102 def find_apache_properties(nagiosuser, nagios_htdocs_dir):
103 wwwuser = None
105 # Search in running processes for Apache
106 for line in os.popen("ps ax -o pid,user,command").readlines():
107 parts = line.split()
108 if len(parts) >= 3 and \
109 (parts[2].endswith("/apache2") or \
110 parts[2].endswith("/httpd") or \
111 parts[2].endswith("/httpd2-prefork") or \
112 parts[2].endswith("/httpd2-worker")
113 ) and \
114 parts[1] != 'root':
115 wwwuser = parts[1]
116 apache_pid = int(parts[0])
117 break
118 if not wwwuser:
119 raise Exception("Cannot find Apache process. Is it running?")
121 def scan_apacheconf(apache_conffile):
122 confdirs = []
123 if apache_conffile[0] != '/':
124 apache_conffile = httpd_root + "/" + apache_conffile
125 confdirs = []
126 for line in file(apache_conffile):
127 parts = line.strip().split()
128 if len(parts) == 2 and parts[0].lower() == "include":
129 if parts[1].endswith("/") or parts[1].endswith("/*.conf"):
130 confdir = "/".join(parts[1].split("/")[:-1])
131 if confdir[0] != "/":
132 confdir = httpd_root + "/" + confdir
133 if not os.path.exists(confdir):
134 continue
135 confdirs.append(confdir) # put at front of list
136 else:
137 try:
138 confdirs += scan_apacheconf(parts[1]) # recursive scan
139 except:
140 pass
141 return confdirs
143 # Find binary
144 try:
145 nagios_htpasswd_file = None
146 nagios_auth_name = None
147 apache_binary = process_executable(apache_pid)
148 apache_conffile = None
149 apache_confdir = None
150 httpd_root = ""
151 for line in os.popen("%s -V 2>&1" % apache_binary): # nosec
152 parts = line.split()
153 if parts[0] == "-D" and len(parts) > 1:
154 p = parts[1].split("=")
155 if len(p) == 2 and p[0] == "SERVER_CONFIG_FILE":
156 apache_conffile = p[1].replace('"', "")
157 elif len(p) == 2 and p[0] == "HTTPD_ROOT":
158 httpd_root = p[1].replace('"', "")
159 if apache_conffile:
160 confdirs = scan_apacheconf(apache_conffile)
162 if len(confdirs) > 0:
163 apache_confdir = confdirs[0]
165 for dir in confdirs:
166 if dir.endswith("/conf.d"):
167 apache_confdir = dir
169 # Search for Nagios configuration. We are interested
170 # in the authentication configuration. Most Nagios
171 # installations use HTTP basic auth with a htpasswd
172 # user file. We want to use that user file for the
173 # check_mk web pages.
175 def remove_quotes(text):
176 try:
177 if text[0] in '"\'':
178 text = text[1:]
179 if text[-1] in '"\'':
180 text = text[:-1]
181 except:
182 pass
183 return text
185 auth_files = []
186 auth_names = []
187 try:
188 for confdir in confdirs:
189 for fn in os.listdir(confdir):
190 file_good = False
191 conffile = file(confdir + "/" + fn)
192 try:
193 new_auth_names = []
194 new_auth_files = []
195 for line in conffile:
196 if nagios_htdocs_dir in line:
197 file_good = True
198 parts = line.split()
199 if len(parts) == 2 and parts[0].lower() == "authuserfile":
200 path = remove_quotes(parts[1])
201 if os.path.exists(path):
202 new_auth_files.append(path)
203 if len(parts) > 1 and parts[0].lower() == "authname":
204 parts = line.split(None, 1)
205 new_auth_names.append(remove_quotes(parts[1].strip()))
206 try:
207 if len(parts) > 1 and parts[0].lower().startswith(
208 "<directory") and "pnp4nagios" in line:
209 cleanedup = line.replace("<", "").replace(">", "").replace(
210 '"', "")
211 cleanedup = cleanedup[9:]
212 dir = cleanedup.strip()
213 if os.path.exists(dir) and os.path.exists(
214 dir + "/application/config/config.php"):
215 result['pnphtdocsdir'] = dir
216 result['pnptemplates'] = dir + "/templates"
217 except Exception as e:
218 pass
219 if file_good:
220 auth_names += new_auth_names
221 auth_files += new_auth_files
223 except Exception as e:
224 pass
225 if len(auth_files) > 0:
226 nagios_htpasswd_file = auth_files[0]
227 if len(auth_names) > 0:
228 nagios_auth_name = auth_names[0]
229 except:
230 if opt_debug:
231 raise
233 except:
234 if opt_debug:
235 raise
236 apache_confdir = None
237 nagios_htpasswd_file = None
239 www_groups = os.popen("id -nG " + wwwuser).read().split() # nosec
240 nagios_groups = os.popen("id -nG " + nagiosuser).read().split() # nosec
241 common_groups = [g for g in www_groups if g in nagios_groups]
242 if len(common_groups) > 1:
243 if 'nagios' in common_groups:
244 common_group = 'nagios'
245 elif 'icinga' in common_groups:
246 common_group = 'icinga'
247 else:
248 common_group = common_groups[0]
249 elif len(common_groups) == 1:
250 common_group = common_groups[0]
251 else:
252 common_group = None
254 return wwwuser, common_group, apache_confdir, nagios_htpasswd_file, nagios_auth_name
257 def process_environment(pid):
258 # Umgebung des Prozesses bestimmen. Ich brauch das nicht,
259 # aber der folgende Code ist einfach cool, oder?
260 try:
261 env = {}
262 for line in file("/proc/%d/environ" % pid).read().split("\0"):
263 if '=' in line:
264 var, value = entry.split('=', 1)
265 env[var] = value
266 return env
267 except:
268 raise Sorry("Cannot get environment of process %d. Aren't you root?" % pid)
271 def process_executable(pid):
272 try:
273 return os.readlink("/proc/%d/exe" % pid).split(" ", 1)[0]
274 except:
275 raise Sorry("Cannot get executable of process %d. Aren't you root?" % pid)
278 def open_files(pid):
279 try:
280 # Liste der offenen Dateien. Das ist schon nuetzlicher,
281 # denn hier sieht man z.B. die Commandpipe
282 procpath = "/proc/%d/fd" % pid
283 return [os.readlink(procpath + "/" + entry) for entry in os.listdir(procpath)]
284 except:
285 raise Sorry("Cannot get open files of process %d. Aren't you root?" % pid)
288 def find_pipes(filenames):
289 pipes = []
290 for f in filenames:
291 try:
292 mode = os.stat(f)[stat.ST_MODE]
293 if stat.S_ISFIFO(mode):
294 pipes.append(f)
295 except:
296 pass
297 return pipes
300 def parse_nagios_config(configfile):
301 conf = []
302 for line in file(configfile):
303 line = line.strip()
304 if line == "" or line[0] == '#':
305 continue
306 try:
307 key, value = line.split('=', 1)
308 conf.append((key, value))
309 except:
310 pass # ignore invalid line (as Nagios seems to do)
311 return conf
314 def detect_pnp():
315 global result
316 # Jetzt will ich noch das Verzeichnis fuer die Schablonen
317 # von PNP finden. Ich erkenne es daran, dass es ein Verzeichnis
318 # ist, in dem 'templates' und 'templates.dist' liegen. Dieses
319 # Verzeichnis liegt hoffentlich innerhalb der Webseite von
320 # Nagios selbst. Dieser Pfad ist in cgi.cfg festgelegt. Das ganze
321 # klappt nur bei PNP 0.4
322 if 'pnptemplates' not in result:
323 try:
324 found = []
326 def func(arg, dirname, names):
327 if 'templates' in names and 'templates.dist' in names:
328 found.append(dirname + "/templates")
330 os.path.walk(cgiconf['physical_html_path'], func, None)
331 result['pnptemplates'] = found[0]
332 if 'pnphtdocsdir' not in result:
333 result['pnphtdocsdir'] = result['pnptemplates'].rsplit('/', 1)[0]
334 except:
335 pass
337 # Suche die Konfigurationsdatei von PNP4Nagios. Denn ich will
338 # den Eintrag finden, der auf die RRDs zeigt. Den braucht
339 # check_mk für das direkte Eintragen in die RRD-Datenbanken
340 try:
341 pnppath = os.path.dirname(result['pnptemplates'])
342 index_php = pnppath + "/index.php"
343 for line in file(index_php):
344 line = line.strip()
345 # $config = "/usr/local/nagios/etc/pnp/config";
346 if line.startswith('$config =') and line.endswith('";'):
347 pnpconffile = line.split('"')[1] + ".php"
348 result['pnpconffile'] = pnpconffile
349 result['pnpconfdir'] = pnpconffile.rsplit("/", 1)[0]
350 break
351 except:
352 pass
354 try:
355 # For PNP 0.6
356 if 'pnpconffile' not in result:
357 kohanaconf = result['pnphtdocsdir'] + "/application/config/config.php"
358 if os.path.exists(kohanaconf):
359 for line in file(kohanaconf):
360 line = line.strip()
361 if not line.startswith("#") and "pnp_etc_path" in line:
362 last = line.split('=')[-1].strip()
363 dir = last.replace("'", "").replace(";", "").replace('"', "")
364 if os.path.exists(dir):
365 result['pnpconfdir'] = dir
366 result['pnpconffile'] = dir + "/config.php"
368 except:
369 pass
371 try:
372 for line in file(result['pnpconffile']):
373 line = line.strip()
374 if line.startswith("$conf['rrdbase']") and line.endswith('";'):
375 rrddir = line.split('"')[1]
376 if rrddir.endswith('/'):
377 rrddir = rrddir[:-1]
378 result['rrddir'] = rrddir
379 elif (line.startswith("$conf['base_url']")
380 or line.startswith("$conf['pnp_base']")) \
381 and line.endswith(";"):
382 pnp_url = line.split('"')[1]
383 if not pnp_url.endswith("/"):
384 pnp_url += "/"
385 result["pnp_url"] = pnp_url
386 except:
387 pass
390 def detect_omd():
391 site = os.getenv("OMD_SITE")
392 root = os.getenv("OMD_ROOT")
393 if not site or not root:
394 return None
395 else:
396 return {
397 'apache_config_dir': root + "/etc/apache/conf.d",
398 'cgiurl': "/" + site + "/nagios/cgi-bin/",
399 'check_icmp_path': root + "/lib/nagios/plugins/check_icmp",
400 'htdocsdir': root + "/share/nagios/htdocs",
401 'htpasswd_file': root + "/etc/htpasswd",
402 'livestatus_in_nagioscfg': False,
403 'nagconfdir': root + "/etc/nagios/conf.d",
404 'nagiosaddconf': "",
405 'nagios_auth_name': "OMD Monitoring Site " + site,
406 'nagios_binary': root + "/bin/nagios",
407 'nagios_config_file': root + "/tmp/nagios/nagios.cfg",
408 'nagios_startscript': root + "/etc/init.d/nagios",
409 'nagios_status_file': root + "/var/nagios/status.dat",
410 'nagiosurl': "/" + site + "/nagios/",
411 'nagiosuser': site,
412 'nagpipe': root + "/tmp/run/nagios.cmd",
413 'check_result_path': root + "/tmp/nagios/checkresults",
414 'pnp_url': "/" + site + "/pnp4nagios/",
415 'pnpconffile': root + "/etc/pnp4nagios/config.php",
416 'pnphtdocsdir': root + "/share/pnp4nagios/htdocs",
417 'pnptemplates': root + "/local/share/check_mk/pnp-templates",
418 'rrddir': root + "/var/pnp4nagios/perfdata",
419 'wwwgroup': site,
420 'wwwuser': site,
425 # _ __ ___ __ _(_)_ __
426 # | '_ ` _ \ / _` | | '_ \
427 # | | | | | | (_| | | | | |
428 # |_| |_| |_|\__,_|_|_| |_|
431 try:
432 result = detect_omd()
433 if not result:
434 result = {}
436 pid, nagiosuser, configfile = find_pid_and_configfile()
437 nagios_dir = os.path.dirname(configfile)
438 result['nagios_config_file'] = configfile
439 result['nagiosuser'] = nagiosuser
440 pipes = find_pipes(open_files(pid))
441 if len(pipes) > 0:
442 result['nagpipe'] = pipes[0]
444 # Path to executable
445 result['nagios_binary'] = process_executable(pid)
447 # Nagios version
448 result['nagios_version'] = ""
449 for line in os.popen(result["nagios_binary"] + " --version 2>/dev/null"): # nosec
450 if line.startswith("Nagios Core") or line.startswith("Icinga Core"):
451 result['nagios_version'] = line.split()[2]
453 # Path to startscript
454 for path in ['/etc/init.d/nagios', '/etc/init.d/nagios3', '/etc/init.d/icinga']:
455 if os.path.exists(path):
456 result['nagios_startscript'] = path
457 break
459 nagconf = parse_nagios_config(configfile)
460 nagconf_dict = dict(nagconf)
461 if "check_result_path" in nagconf_dict:
462 result['check_result_path'] = nagconf_dict['check_result_path']
464 try:
465 cgifile = os.path.dirname(configfile) + "/cgi.cfg"
466 cgiconf = dict(parse_nagios_config(cgifile))
467 result['htdocsdir'] = cgiconf['physical_html_path']
468 except:
469 cgiconf = {}
471 # Suche nach cfg_dir Direktiven. Wir suchen
472 # einen flauschigen Platz fuer unsere Konfigdateien
473 cfg_dirs = [value for key, value in nagconf if key == 'cfg_dir']
474 if len(cfg_dirs) > 0:
475 # Wenn es mehrere gibt, bevorzuge ich das, das im gleichen
476 # Verzeichnis, wie die Nagios-Konfigdatei selbst liegt.
477 # Debian legt ein cfg_dir fuer die Plugins an....
478 if len(cfg_dirs) == 1:
479 result['nagconfdir'] = cfg_dirs[0]
480 else:
481 dir = os.path.dirname(configfile)
482 for d in cfg_dirs:
483 if os.path.dirname(d) == dir:
484 result['nagconfdir'] = d
485 break
486 else:
487 result['nagconfdir'] = cfg_dirs[0]
488 else:
489 # Mist. Kein cfg_dir in nagios.cfg. Das ist z.B. bei
490 # der immer noch verbreiteten Defaultkonfig der Fall.
491 # Wir legen einfach selbst eins fest und hängen das
492 # eigenmächtig hinten an die Config an
493 nagconfdir = nagios_dir + "/check_mk.d"
494 result['nagconfdir'] = nagconfdir
495 result['nagiosaddconf'] = "cfg_dir=" + nagconfdir
497 # Find path to status.dat, the Nagios status file. We
498 # need that for the check_mk web pages. Normally the
499 # path is configured in nagios.cfg. If no - we still
500 # have a chance by parsing the output of nagios3stats.
501 nagios_status_file = nagconf_dict.get("status_file")
502 if not nagios_status_file:
503 for stats_name in ["stats", "tats"]:
504 try:
505 stats_bin = result['nagios_binary'] + stats_name
506 for line in os.popen(stats_bin + " 2>/dev/null"): # nosec
507 if line.startswith("Status File:"):
508 parts = line.split()
509 nagios_status_file = parts[-1]
510 break
511 elif line.startswith("Error reading status file"):
512 parts = line.split()
513 nagios_status_file = parts[-1][1:-1]
514 break
515 except:
516 pass
518 if nagios_status_file:
519 result['nagios_status_file'] = nagios_status_file
521 # Ermittle $USER1$ Variablen, da sie in den Plugin-Pfaden
522 # auftauchen koennen.
523 uservars = {}
524 try:
525 for line in file(nagconf_dict['resource_file']):
526 line = line.strip()
527 if line.startswith('$') and '=' in line:
528 varname, value = line.split('=', 1)
529 uservars[varname.strip()] = value.strip()
530 except:
531 pass
533 # Suche nach einem Eintrag zum Laden des livestatus
534 # Moduls. Er darf auch auskommentiert sein. Dann lassen
535 # wir den Benutzer damit in Ruhe
536 found = False
537 for line in file(configfile):
538 if "broker_module=" in line and "/livestatus.o" in line:
539 found = True
540 break
541 result['livestatus_in_nagioscfg'] = found
543 # Jetzt wird's schwieriger: Ich suche nach check_icmp.
544 # Ich will keinen find machen, da das erstens ewig
545 # dauern kann und zweitens eventl. eine falsche Stelle
546 # findet, z.B. innerhalb eines ausgepackten und kompilierten
547 # Quellcodes der nagios-plugins. Daher suche ich in
548 # allen Objektdateien von Nagios nach command_line.
549 # Damit ermittle ich alle Verzeichnisse, in denen Plugins
550 # liegen. Dort suche ich dann nach check_icmp. Zur Sicherheit
551 # suche ich aber auch unter '/usr/lib/nagios' und '/usr/local/nagios/libexec'
552 # und '/usr/local/nagios/plugins'
553 found = []
554 for dir in cfg_dirs:
555 os.path.walk(dir, lambda x, dirname, names: found.append((dirname, names)), None)
556 plugin_paths = []
557 for dirname, names in found:
558 for name in names:
559 if name.endswith(".cfg"):
560 path = dirname + "/" + name
561 try:
562 for line in file(path):
563 if line.strip() == '':
564 continue
565 parts = line.strip().split()
566 if parts[0] == "command_line":
567 path = parts[1]
568 for var, value in uservars.items():
569 path = path.replace(var, value)
570 if path.startswith('/') and path not in plugin_paths:
571 plugin_paths.append(path)
572 except:
573 pass
575 for dir in plugin_paths + \
576 [ '/usr/lib/nagios/plugins',
577 '/usr/lib64/nagios/plugins',
578 '/usr/local/nagios/libexec',
579 '/usr/local/nagios/plugins' ]:
580 try:
581 mode = os.stat(dir)[stat.ST_MODE]
582 if not stat.S_ISDIR(mode):
583 dir = os.path.dirname(dir)
584 filenames = os.listdir(dir)
586 for filename in filenames:
587 if filename == 'check_icmp':
588 result['check_icmp_path'] = dir + '/' + filename
589 break
590 except:
591 pass
593 # Die Basis-Url fuer Nagios ist leider auch nicht immer
594 # gleich
595 try:
596 result['nagiosurl'] = cgiconf['url_html_path']
597 result['cgiurl'] = result['nagiosurl'] + "/cgi-bin"
598 except:
599 pass
601 # Suche eine Gruppe, die Nagios mit dem Apache gemeinsam
602 # hat. Diese brauchen wir z.B. für logwatch
603 try:
604 wwwuser, wwwgroup, apache_confdir, nagios_htpasswd_file, nagios_auth_name = \
605 find_apache_properties(nagiosuser, result['htdocsdir'])
606 if wwwuser:
607 result['wwwuser'] = wwwuser
608 if wwwgroup:
609 result['wwwgroup'] = wwwgroup
610 if apache_confdir:
611 result['apache_config_dir'] = apache_confdir
612 if nagios_htpasswd_file:
613 result['htpasswd_file'] = nagios_htpasswd_file
614 if nagios_auth_name:
615 result['nagios_auth_name'] = nagios_auth_name
616 except Exception as e:
617 sys.stderr.write("\033[1;41;35m Cannot determine Apache properties. \033[0m\n"
618 "Reason: %s\n" % e)
620 detect_pnp()
622 print "# Result of autodetection"
623 for var, value in result.items():
624 print
625 descr = target_values.get(var)
626 if descr:
627 print "# %s" % descr
628 else:
629 print "# (unknown value)"
630 print "%s='%s'" % (var, value)
632 for var, descr in target_values.items():
633 if var not in result:
634 print
635 print "# %s" % descr
636 print "# NOT DETECTED: %s" % var
638 except Sorry as e:
639 sys.stderr.write("\033[1;41;35m Sorry: %s \033[0m\n" % e.reason)
640 sys.exit(1)
642 except Exception as e:
643 if opt_debug:
644 raise
645 else:
646 sys.stderr.write("* Sorry, something unexpected happened: %s\n\n" % e)
647 import traceback
648 traceback.print_exc()
649 sys.exit(1)