Licenses: Updated the list of licenses and added a PDF containing all license texts
[check_mk.git] / cmk_base / parent_scan.py
blobcba42d6744084b4c9b126d2af129ba119a0c873c
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
28 import sys
29 import socket
30 import subprocess
31 import time
32 import pprint
34 import six
36 import cmk.utils.tty as tty
37 import cmk.utils.paths
38 import cmk.utils.debug
39 from cmk.utils.exceptions import MKGeneralException
41 import cmk_base.utils
42 import cmk_base.console as console
43 import cmk_base.config as config
44 import cmk_base.ip_lookup as ip_lookup
47 def do_scan_parents(hosts):
48 if not hosts:
49 hosts = [
50 h for h in config.all_active_realhosts()
51 if config.in_binary_hostlist(h, config.scanparent_hosts)
54 parent_hosts = []
55 parent_ips = {}
56 parent_rules = []
57 gateway_hosts = set([])
59 if config.max_num_processes < 1:
60 config.max_num_processes = 1
62 outfilename = cmk.utils.paths.check_mk_config_dir + "/parents.mk"
64 if not traceroute_available():
65 raise MKGeneralException('The program "traceroute" was not found.\n'
66 'The parent scan needs this program.\n'
67 'Please install it and try again.')
69 if os.path.exists(outfilename):
70 first_line = file(outfilename, "r").readline()
71 if not first_line.startswith('# Automatically created by --scan-parents at'):
72 raise MKGeneralException("conf.d/parents.mk seems to be created manually.\n\n"
73 "The --scan-parents function would overwrite this file.\n"
74 "Please rename it to keep the configuration or delete "
75 "the file and try again.")
77 console.output("Scanning for parents (%d processes)..." % config.max_num_processes)
78 while len(hosts) > 0:
79 chunk = []
80 while len(chunk) < config.max_num_processes and len(hosts) > 0:
81 host = hosts[0]
82 del hosts[0]
83 # skip hosts that already have a parent
84 if config.parents_of(host):
85 console.verbose("(manual parent) ")
86 continue
87 chunk.append(host)
89 gws = scan_parents_of(chunk)
91 for host, (gw, _unused_state, _unused_ping_fails, _unused_message) in zip(chunk, gws):
92 if gw:
93 gateway, gateway_ip, dns_name = gw
94 if not gateway: # create artificial host
95 if dns_name:
96 gateway = dns_name
97 else:
98 gateway = "gw-%s" % (gateway_ip.replace(".", "-"))
99 if gateway not in gateway_hosts:
100 gateway_hosts.add(gateway)
101 parent_hosts.append("%s|parent|ping" % gateway)
102 parent_ips[gateway] = gateway_ip
103 if config.monitoring_host:
104 parent_rules.append((config.monitoring_host,
105 [gateway])) # make Nagios a parent of gw
106 parent_rules.append((gateway, [host]))
107 elif host != config.monitoring_host and config.monitoring_host:
108 # make monitoring host the parent of all hosts without real parent
109 parent_rules.append((config.monitoring_host, [host]))
111 with file(outfilename, "w") as out:
112 out.write("# Automatically created by --scan-parents at %s\n\n" % time.asctime())
113 out.write("# Do not edit this file. If you want to convert an\n")
114 out.write("# artificial gateway host into a permanent one, then\n")
115 out.write("# move its definition into another *.mk file\n")
117 out.write("# Parents which are not listed in your all_hosts:\n")
118 out.write("all_hosts += %s\n\n" % pprint.pformat(parent_hosts))
120 out.write("# IP addresses of parents not listed in all_hosts:\n")
121 out.write("ipaddresses.update(%s)\n\n" % pprint.pformat(parent_ips))
123 out.write("# Parent definitions\n")
124 out.write("parents += %s\n\n" % pprint.pformat(parent_rules))
125 console.output("\nWrote %s\n" % outfilename)
128 def traceroute_available():
129 for path in os.environ['PATH'].split(os.pathsep):
130 f = path + '/traceroute'
131 if os.path.exists(f) and os.access(f, os.X_OK):
132 return f
135 def scan_parents_of(hosts, silent=False, settings=None):
136 if settings is None:
137 settings = {}
139 if config.monitoring_host:
140 nagios_ip = ip_lookup.lookup_ipv4_address(config.monitoring_host)
141 else:
142 nagios_ip = None
144 os.putenv("LANG", "")
145 os.putenv("LC_ALL", "")
147 # Start processes in parallel
148 procs = []
149 for host in hosts:
150 console.verbose("%s " % host)
151 try:
152 ip = ip_lookup.lookup_ipv4_address(host)
153 command = [
154 "traceroute", "-w",
155 "%d" % settings.get("timeout", 8), "-q",
156 "%d" % settings.get("probes", 2), "-m",
157 "%d" % settings.get("max_ttl", 10), "-n", ip
159 console.vverbose("Running '%s'\n" % subprocess.list2cmdline(command))
161 procs.append((host, ip,
162 subprocess.Popen(
163 command,
164 stdout=subprocess.PIPE,
165 stderr=subprocess.STDOUT,
166 close_fds=True)))
167 except Exception as e:
168 if cmk.utils.debug.enabled():
169 raise
170 procs.append((host, None, "ERROR: %s" % e))
172 # Output marks with status of each single scan
173 def dot(color, dot='o'):
174 if not silent:
175 console.output(tty.bold + color + dot + tty.normal)
177 # Now all run and we begin to read the answers. For each host
178 # we add a triple to gateways: the gateway, a scan state and a diagnostic output
179 gateways = []
180 for host, ip, proc_or_error in procs:
181 if isinstance(proc_or_error, six.string_types):
182 lines = [proc_or_error]
183 exitstatus = 1
184 else:
185 exitstatus = proc_or_error.wait()
186 lines = [l.strip() for l in proc_or_error.stdout.readlines()]
188 if exitstatus:
189 dot(tty.red, '*')
190 gateways.append((None, "failed", 0,
191 "Traceroute failed with exit code %d" % (exitstatus & 255)))
192 continue
194 if len(lines) == 1 and lines[0].startswith("ERROR:"):
195 message = lines[0][6:].strip()
196 console.verbose("%s: %s\n", host, message, stream=sys.stderr)
197 dot(tty.red, "D")
198 gateways.append((None, "dnserror", 0, message))
199 continue
201 elif len(lines) == 0:
202 if cmk.utils.debug.enabled():
203 raise MKGeneralException(
204 "Cannot execute %s. Is traceroute installed? Are you root?" % command)
205 else:
206 dot(tty.red, '!')
207 continue
209 elif len(lines) < 2:
210 if not silent:
211 console.error("%s: %s\n" % (host, ' '.join(lines)))
212 gateways.append((None, "garbled", 0,
213 "The output of traceroute seem truncated:\n%s" % ("".join(lines))))
214 dot(tty.blue)
215 continue
217 # Parse output of traceroute:
218 # traceroute to 8.8.8.8 (8.8.8.8), 30 hops max, 40 byte packets
219 # 1 * * *
220 # 2 10.0.0.254 0.417 ms 0.459 ms 0.670 ms
221 # 3 172.16.0.254 0.967 ms 1.031 ms 1.544 ms
222 # 4 217.0.116.201 23.118 ms 25.153 ms 26.959 ms
223 # 5 217.0.76.134 32.103 ms 32.491 ms 32.337 ms
224 # 6 217.239.41.106 32.856 ms 35.279 ms 36.170 ms
225 # 7 74.125.50.149 45.068 ms 44.991 ms *
226 # 8 * 66.249.94.86 41.052 ms 66.249.94.88 40.795 ms
227 # 9 209.85.248.59 43.739 ms 41.106 ms 216.239.46.240 43.208 ms
228 # 10 216.239.48.53 45.608 ms 47.121 ms 64.233.174.29 43.126 ms
229 # 11 209.85.255.245 49.265 ms 40.470 ms 39.870 ms
230 # 12 8.8.8.8 28.339 ms 28.566 ms 28.791 ms
231 routes = []
232 for line in lines[1:]:
233 parts = line.split()
234 route = parts[1]
235 if route.count('.') == 3:
236 routes.append(route)
237 elif route == '*':
238 routes.append(None) # No answer from this router
239 else:
240 if not silent:
241 console.error("%s: invalid output line from traceroute: '%s'\n" % (host, line))
243 if len(routes) == 0:
244 error = "incomplete output from traceroute. No routes found."
245 console.error("%s: %s\n" % (host, error))
246 gateways.append((None, "garbled", 0, error))
247 dot(tty.red)
248 continue
250 # Only one entry -> host is directly reachable and gets nagios as parent -
251 # if nagios is not the parent itself. Problem here: How can we determine
252 # if the host in question is the monitoring host? The user must configure
253 # this in monitoring_host.
254 elif len(routes) == 1:
255 if ip == nagios_ip:
256 gateways.append((None, "root", 0, "")) # We are the root-monitoring host
257 dot(tty.white, 'N')
258 elif config.monitoring_host:
259 gateways.append(((config.monitoring_host, nagios_ip, None), "direct", 0, ""))
260 dot(tty.cyan, 'L')
261 else:
262 gateways.append((None, "direct", 0, ""))
263 continue
265 # Try far most route which is not identical with host itself
266 ping_probes = settings.get("ping_probes", 5)
267 skipped_gateways = 0
268 route = None
269 for r in routes[::-1]:
270 if not r or (r == ip):
271 continue
272 # Do (optional) PING check in order to determine if that
273 # gateway can be monitored via the standard host check
274 if ping_probes:
275 if not gateway_reachable_via_ping(r, ping_probes):
276 console.verbose("(not using %s, not reachable)\n", r, stream=sys.stderr)
277 skipped_gateways += 1
278 continue
279 route = r
280 break
281 if not route:
282 error = "No usable routing information"
283 if not silent:
284 console.error("%s: %s\n" % (host, error))
285 gateways.append((None, "notfound", 0, error))
286 dot(tty.blue)
287 continue
289 # TTLs already have been filtered out)
290 gateway_ip = route
291 gateway = _ip_to_hostname(route)
292 if gateway:
293 console.verbose("%s(%s) ", gateway, gateway_ip)
294 else:
295 console.verbose("%s ", gateway_ip)
297 # Try to find DNS name of host via reverse DNS lookup
298 dns_name = _ip_to_dnsname(gateway_ip)
299 gateways.append(((gateway, gateway_ip, dns_name), "gateway", skipped_gateways, ""))
300 dot(tty.green, 'G')
301 return gateways
304 def gateway_reachable_via_ping(ip, probes):
305 return subprocess.call(
306 ["ping", "-q", "-i", "0.2", "-l", "3", "-c",
307 "%d" % probes, "-W", "5", ip],
308 stdout=open(os.devnull, "w"),
309 stderr=subprocess.STDOUT,
310 close_fds=True) == 0
313 # find hostname belonging to an ip address. We must not use
314 # reverse DNS but the Check_MK mechanisms, since we do not
315 # want to find the DNS name but the name of a matching host
316 # from all_hosts
317 def _ip_to_hostname(ip):
318 if not cmk_base.config_cache.exists("ip_to_hostname"):
319 cache = cmk_base.config_cache.get_dict("ip_to_hostname")
321 for host in config.all_active_realhosts():
322 try:
323 cache[ip_lookup.lookup_ipv4_address(host)] = host
324 except:
325 pass
326 else:
327 cache = cmk_base.config_cache.get_dict("ip_to_hostname")
329 return cache.get(ip)
332 def _ip_to_dnsname(ip):
333 try:
334 return socket.gethostbyaddr(ip)[0]
335 except:
336 return None