1 import BaseHTTPServer
2 import ConfigParser
3 import SocketServer
4 import base64
5 import datetime
6 import libxml2
7 import mimetools
8 import os
9 import random
10 import shlex
11 import signal
12 import socket
13 import string
14 import subprocess
15 import sys
16 import urllib2
18 def debug(level, verb, string):
19 if level <= verb:
20 print "%s: %s" % (level, string)
23 ##############################################################################
25 class ExecTimeoutError(Exception):
26 pass
28 ##############################################################################
30 def available_encodings():
31 return ['base64', 'plain',]
34 def check_encoding(enc):
35 if enc in available_encodings():
36 return True
37 else:
38 return False
41 def decode(data, encoding):
42 if encoding == 'plain':
43 return data
44 else:
45 return base64.b64decode(data)
48 def encode(data, encoding=None):
49 if encoding == 'plain':
50 return data
51 else:
52 return base64.b64encode(data)
55 ##############################################################################
57 def read_inifile(inifile):
58 config = ConfigParser.RawConfigParser()
59 config.optionxform = str # We need case-sensitive options
60 ini_list = config.read(inifile)
62 if ini_list:
63 return config
64 else:
65 return False
68 ##############################################################################
70 def exec_timeout_handler(signum, frame):
71 raise ExecTimeoutError
73 def exec_check(host_name, service_descr, cmdline, timeout=None, timeout_returncode=2):
74 cmdarray = shlex.split(cmdline)
76 check = {}
77 check['host_name'] = host_name
78 check['service_description'] = service_descr
80 if len(cmdarray) == 0:
81 check['output'] = 'No command line specified!'
82 check['returncode'] = 127
83 return check
85 check['commandline'] = cmdline
86 check['command'] = cmdarray[0].split(os.path.sep)[-1]
88 if timeout:
89 signal.signal(signal.SIGALRM, exec_timeout_handler)
90 signal.alarm(timeout)
92 try:
93 cmd = subprocess.Popen(cmdarray, stdout=subprocess.PIPE)
94 check['output'] = cmd.communicate()[0].rstrip()
95 check['returncode'] = cmd.returncode
96 except OSError:
97 check['output'] = 'Could not execute "%s"' % cmdline
98 check['returncode'] = 127
99 except ExecTimeoutError:
100 check['output'] = 'Plugin timed out after %s seconds' % timeout
101 check['returncode'] = timeout_returncode
103 if timeout:
104 signal.alarm(0)
105 try:
106 if sys.version_info >= (2, 6):
107 cmd.terminate()
108 else:
109 os.kill(cmd.pid, 15)
110 except OSError:
111 pass
113 check['timestamp'] = datetime.datetime.now().strftime('%s')
114 return check
117 ##############################################################################
119 def conf2dict(config, opt_host=None, opt_service=None):
120 checks = []
122 # Read "plugin_timeout" from "[nagixsc]", default "None" (no timeout)
123 try:
124 timeout = config.getint('nagixsc','plugin_timeout')
125 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
126 timeout = None
128 # Read "plugin_timeout_returncode" from "[nagixsc]", default "2" (CRITICAL)
129 try:
130 timeout_returncode = config.getint('nagixsc','plugin_timeout_returncode')
131 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
132 timeout_returncode = 2
134 # Read "add_pnp4nagios_template_hint" from "[nagixsc]", default "False"
135 try:
136 add_pnp4nagios_template_hint = config.getboolean('nagixsc','add_pnp4nagios_template_hint')
137 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
138 add_pnp4nagios_template_hint = False
140 # Sections are Hosts (not 'nagixsc'), options in sections are Services
141 hosts = config.sections()
142 if 'nagixsc' in hosts:
143 hosts.remove('nagixsc')
145 # Filter out host/section if it exists
146 if opt_host:
147 if opt_host in hosts:
148 hosts = [opt_host,]
149 else:
150 hosts = []
152 for host in hosts:
153 # Overwrite section/host name with '_host_name'
154 if config.has_option(host,'_host_name'):
155 host_name = config.get(host,'_host_name')
156 else:
157 host_name = host
160 services = config.options(host)
161 # Look for host check
162 if '_host_check' in services and not opt_service:
163 cmdline = config.get(host, '_host_check')
164 check = exec_check(host_name, None, cmdline, timeout, timeout_returncode)
165 if add_pnp4nagios_template_hint and '|' in check['output']:
166 check['output'] += ' [%s]' % check['command']
167 checks.append(check)
170 # Filter out service if given in cmd line options
171 if opt_service:
172 if opt_service in services:
173 services = [opt_service,]
174 else:
175 services = []
177 for service in services:
178 # If option starts with '_' it may be a NagixSC option in the future
179 if service[0] != '_':
180 cmdline = config.get(host, service)
182 check = exec_check(host_name, service, cmdline, timeout, timeout_returncode)
183 if add_pnp4nagios_template_hint and '|' in check['output']:
184 check['output'] += ' [%s]' % check['command']
185 checks.append(check)
187 return checks
190 ##############################################################################
192 def dict2out_passive(checks, xmltimestamp, opt_pipe, opt_verb=0):
193 FORMAT_HOST = '[%s] PROCESS_HOST_CHECK_RESULT;%s;%s;%s'
194 FORMAT_SERVICE = '[%s] PROCESS_SERVICE_CHECK_RESULT;%s;%s;%s;%s'
195 count_services = 0
196 now = datetime.datetime.now().strftime('%s')
198 # Prepare
199 if opt_verb <= 2:
200 pipe = open(opt_pipe, "w")
201 else:
202 pipe = None
204 # Output
205 for check in checks:
206 count_services += 1
207 if check.has_key('timestamp'):
208 timestamp = check['timestamp']
209 else:
210 timestamp = xmltimestamp
212 if check['service_description'] == None or check['service_description'] == '':
213 # Host check
214 line = FORMAT_HOST % (timestamp, check['host_name'], check['returncode'], check['output'].replace('\n', '\\n'))
215 else:
216 # Service check
217 line = FORMAT_SERVICE % (timestamp, check['host_name'], check['service_description'], check['returncode'], check['output'].replace('\n', '\\n'))
219 if pipe:
220 pipe.write(line + '\n')
221 debug(2, opt_verb, line)
223 # Close
224 if pipe:
225 pipe.close()
226 else:
227 print "Passive check results NOT written to Nagios pipe due to -vvv!"
229 return count_services
232 def dict2out_checkresult(checks, xmltimestamp, opt_checkresultdir, opt_verb=0):
233 count_services = 0
234 count_failed = 0
235 list_failed = []
236 chars = string.letters + string.digits
237 ctimestamp = datetime.datetime.now().ctime()
238 random.seed()
240 for check in checks:
241 count_services += 1
242 if check.has_key('timestamp'):
243 timestamp = check['timestamp']
244 else:
245 timestamp = xmltimestamp
247 filename = os.path.join(opt_checkresultdir, 'c' + ''.join([random.choice(chars) for i in range(6)]))
248 try:
249 crfile = open(filename, "w")
250 if check['service_description'] == None or check['service_description'] == '':
251 # Host check
252 crfile.write('### Active Check Result File ###\nfile_time=%s\n\n### Nagios Service Check Result ###\n# Time: %s\nhost_name=%s\ncheck_type=0\ncheck_options=0\nscheduled_check=1\nreschedule_check=1\nlatency=0.0\nstart_time=%s.00\nfinish_time=%s.05\nearly_timeout=0\nexited_ok=1\nreturn_code=%s\noutput=%s\n' % (timestamp, ctimestamp, check['host_name'], timestamp, timestamp, check['returncode'], check['output'].replace('\n', '\\n') ) )
253 else:
254 # Service check
255 crfile.write('### Active Check Result File ###\nfile_time=%s\n\n### Nagios Service Check Result ###\n# Time: %s\nhost_name=%s\nservice_description=%s\ncheck_type=0\ncheck_options=0\nscheduled_check=1\nreschedule_check=1\nlatency=0.0\nstart_time=%s.00\nfinish_time=%s.05\nearly_timeout=0\nexited_ok=1\nreturn_code=%s\noutput=%s\n' % (timestamp, ctimestamp, check['host_name'], check['service_description'], timestamp, timestamp, check['returncode'], check['output'].replace('\n', '\\n') ) )
256 crfile.close()
258 # Create OK file
259 open(filename + '.ok', 'w').close()
260 except:
261 count_failed += 1
262 list_failed.append([filename, check['host_name'], check['service_description']])
264 return (count_services, count_failed, list_failed)
267 ##############################################################################
269 def read_xml(options):
270 if options.url != None:
272 if options.httpuser and options.httppasswd:
273 passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
274 passman.add_password(None, options.url, options.httpuser, options.httppasswd)
275 authhandler = urllib2.HTTPBasicAuthHandler(passman)
276 opener = urllib2.build_opener(authhandler)
277 urllib2.install_opener(opener)
279 try:
280 response = urllib2.urlopen(options.url)
281 except urllib2.HTTPError, error:
282 print error
283 sys.exit(0)
284 except urllib2.URLError, error:
285 print error.reason[1]
286 sys.exit(0)
288 doc = libxml2.parseDoc(response.read())
289 response.close()
291 else:
292 doc = libxml2.parseFile(options.file)
294 return doc
297 def read_xml_from_string(content):
298 return libxml2.parseDoc(content)
301 def write_xml(xmldoc, outfile, httpuser=None, httppasswd=None):
302 if outfile.startswith('http'):
303 (headers, body) = encode_multipart(xmldoc, httpuser, httppasswd)
304 response = urllib2.urlopen(urllib2.Request(outfile, body, headers)).read()
305 return response
307 elif outfile == '-':
308 xmldoc.saveFormatFile('-', format=1)
309 return None
311 else:
312 xmldoc.saveFile(outfile)
313 return None
316 def write_xml_or_die(xmldoc, outfile, httpuser=None, httppasswd=None):
317 try:
318 response = write_xml(xmldoc, outfile, httpuser, httppasswd)
319 except urllib2.HTTPError, error:
320 print error
321 sys.exit(11)
322 except urllib2.URLError, error:
323 print error.reason[1]
324 sys.exit(12)
326 return response
329 ##############################################################################
331 def xml_check_version(xmldoc):
332 # FIXME: Check XML structure
333 try:
334 xmlnagixsc = xmldoc.xpathNewContext().xpathEval('/nagixsc')[0]
335 except:
336 return (False, 'Not a Nag(IX)SC XML file!')
338 try:
339 if xmlnagixsc.prop('version') != "1.0":
340 return (False, 'Wrong version (found "%s", need "1.0") of XML file!' % xmlnagixsc.prop('version'))
341 except:
342 return (False, 'No version information found in XML file!')
344 return (True, 'XML seems to be ok')
347 def xml_get_timestamp(xmldoc):
348 try:
349 timestamp = int(xmldoc.xpathNewContext().xpathEval('/nagixsc/timestamp')[0].get_content())
350 except:
351 return False
353 return timestamp
356 def xml_to_dict(xmldoc, verb=0, hostfilter=None, servicefilter=None):
357 checks = []
358 now = int(datetime.datetime.now().strftime('%s'))
359 filetimestamp = reset_future_timestamp(xml_get_timestamp(xmldoc), now)
361 if hostfilter:
362 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host[name="%s"] | /nagixsc/host[name="%s"]' % (hostfilter, encode(hostfilter)))
363 else:
364 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host')
366 for host in hosts:
367 xmlhostname = host.xpathEval('name')[0]
368 hostname = decode(xmlhostname.get_content(), xmlhostname.prop('encoding'))
369 debug(2, verb, 'Found host "%s"' % hostname)
371 # Look for Host check result
372 if host.xpathEval('returncode'):
373 retcode = host.xpathEval('returncode')[0].get_content()
374 else:
375 retcode = None
377 if host.xpathEval('output'):
378 xmloutput = host.xpathEval('output')[0]
379 output = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
380 else:
381 output = None
383 if host.xpathEval('timestamp'):
384 timestamp = reset_future_timestamp(int(host.xpathEval('timestamp')[0].get_content()), now)
385 else:
386 timestamp = filetimestamp
388 # Append only if no service filter
389 if not servicefilter and retcode and output:
390 checks.append({'host_name':hostname, 'service_description':None, 'returncode':retcode, 'output':output, 'timestamp':timestamp})
393 # Look for service filter
394 if servicefilter:
395 services = host.xpathEval('service[description="%s"] | service[description="%s"]' % (servicefilter, encode(servicefilter)))
396 else:
397 services = host.xpathEval('service')
399 # Loop over services in host
400 for service in services:
401 service_dict = {}
403 xmldescr = service.xpathEval('description')[0]
404 xmloutput = service.xpathEval('output')[0]
406 srvdescr = decode(xmldescr.get_content(), xmldescr.prop('encoding'))
407 retcode = service.xpathEval('returncode')[0].get_content()
408 output = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
410 try:
411 timestamp = reset_future_timestamp(int(service.xpathEval('timestamp')[0].get_content()), now)
412 except:
413 timestamp = filetimestamp
415 debug(2, verb, 'Found service "%s"' % srvdescr)
417 service_dict = {'host_name':hostname, 'service_description':srvdescr, 'returncode':retcode, 'output':output, 'timestamp':timestamp}
418 checks.append(service_dict)
420 debug(1, verb, 'Host: "%s" - Service: "%s" - RetCode: "%s" - Output: "%s"' % (hostname, srvdescr, retcode, output) )
422 return checks
425 def xml_from_dict(checks, encoding='base64'):
426 lasthost = None
428 db = [(check['host_name'], check) for check in checks]
429 db.sort()
431 xmldoc = libxml2.newDoc('1.0')
432 xmlroot = xmldoc.newChild(None, 'nagixsc', None)
433 xmlroot.setProp('version', '1.0')
434 xmltimestamp = xmlroot.newChild(None, 'timestamp', datetime.datetime.now().strftime('%s'))
436 for entry in db:
437 check = entry[1]
439 if check['host_name'] != lasthost:
440 xmlhost = xmlroot.newChild(None, 'host', None)
441 xmlhostname = xmlhost.newChild(None, 'name', encode(check['host_name'], encoding))
442 lasthost = check['host_name']
444 if check['service_description'] == '' or check['service_description'] == None:
445 # Host check result
446 xmlreturncode = xmlhost.newChild(None, 'returncode', str(check['returncode']))
447 xmloutput = xmlhost.newChild(None, 'output', encode(check['output'], encoding))
448 xmloutput.setProp('encoding', encoding)
449 if check.has_key('timestamp'):
450 xmltimestamp = xmlhost.newChild(None, 'timestamp', str(check['timestamp']))
451 else:
452 # Service check result
453 xmlservice = xmlhost.newChild(None, 'service', None)
454 xmlname = xmlservice.newChild(None, 'description', encode(check['service_description'], encoding))
455 xmlname.setProp('encoding', encoding)
456 xmlreturncode = xmlservice.newChild(None, 'returncode', str(check['returncode']))
457 xmloutput = xmlservice.newChild(None, 'output', encode(check['output'], encoding))
458 xmloutput.setProp('encoding', encoding)
459 if check.has_key('timestamp'):
460 xmltimestamp = xmlservice.newChild(None, 'timestamp', str(check['timestamp']))
462 return xmldoc
465 def xml_merge(xmldocs):
466 checks = []
467 for xmldoc in xmldocs:
468 checks.extend(xml_to_dict(xmldoc))
469 newxmldoc = xml_from_dict(checks)
470 return newxmldoc
473 def check_mark_outdated(check, now, maxtimediff, markold):
474 timedelta = now - check['timestamp']
475 if timedelta > maxtimediff:
476 check['output'] = 'Nag(ix)SC: Check result is %s(>%s) seconds old - %s' % (timedelta, maxtimediff, check['output'])
477 if markold:
478 check['returncode'] = 3
479 return check
482 def reset_future_timestamp(timestamp, now):
483 if timestamp <= now:
484 return timestamp
485 else:
486 return now
488 ##############################################################################
490 def encode_multipart(xmldoc, httpuser=None, httppasswd=None):
491 BOUNDARY = mimetools.choose_boundary()
492 CRLF = '\r\n'
493 L = []
494 L.append('--' + BOUNDARY)
495 L.append('Content-Disposition: form-data; name="xmlfile"; filename="xmlfile"')
496 L.append('Content-Type: application/xml')
497 L.append('')
498 L.append(xmldoc.serialize())
499 L.append('--' + BOUNDARY + '--')
500 L.append('')
501 body = CRLF.join(L)
502 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
503 headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
505 if httpuser and httppasswd:
506 headers['Authorization'] = 'Basic %s' % base64.b64encode(':'.join([httpuser, httppasswd]))
508 return (headers, body)
510 ##############################################################################
512 def daemonize(pidfile=None, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
513 # 1st fork
514 try:
515 pid = os.fork()
516 if pid > 0:
517 sys.exit(0)
518 except OSError, e:
519 sys.stderr.write("1st fork failed: (%d) %s\n" % (e.errno, e.strerror))
520 sys.exit(1)
521 # Prepare 2nd fork
522 os.chdir("/")
523 os.umask(0)
524 os.setsid( )
525 # 2nd fork
526 try:
527 pid = os.fork()
528 if pid > 0:
529 sys.exit(0)
530 except OSError, e:
531 sys.stderr.write("2nd fork failed: (%d) %s\n" % (e.errno, e.strerror))
532 sys.exit(1)
534 # Try to write PID file
535 if pidfile:
536 pid = str(os.getpid())
537 try:
538 file(pidfile, 'w+').write('%s\n' % pid)
539 except IOError:
540 sys.stderr.write("Could not write PID file, exiting...\n")
541 sys.exit(1)
543 # Redirect stdin, stdout, stderr
544 sys.stdout.flush()
545 sys.stderr.flush()
546 si = file(stdin, 'r')
547 so = file(stdout, 'a+')
548 se = file(stderr, 'a+', 0)
549 os.dup2(si.fileno(), sys.stdin.fileno())
550 os.dup2(so.fileno(), sys.stdout.fileno())
551 os.dup2(se.fileno(), sys.stderr.fileno())
553 return
555 ##############################################################################
557 class MyHTTPServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer):
558 def __init__(self, server_address, HandlerClass, ssl=False, sslpemfile=None):
559 if ssl:
560 # FIXME: SSL is in Py2.6
561 try:
562 from OpenSSL import SSL
563 except:
564 print 'No Python OpenSSL wrapper/bindings found!'
565 sys.exit(127)
567 SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
568 context = SSL.Context(SSL.SSLv23_METHOD)
569 context.use_privatekey_file (sslpemfile)
570 context.use_certificate_file(sslpemfile)
571 self.socket = SSL.Connection(context, socket.socket(self.address_family, self.socket_type))
572 else:
573 SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
574 self.socket = socket.socket(self.address_family, self.socket_type)
576 self.server_bind()
577 self.server_activate()
580 class MyHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
581 def setup(self):
582 self.connection = self.request
583 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
584 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
586 ##############################################################################
588 def prepare_socket(socket_path):
589 try:
590 if socket_path.startswith('/'):
591 s_family=socket.AF_UNIX
592 s_sockaddr = socket_path
593 elif socket_path.startswith('unix:'):
594 s_family=socket.AF_UNIX
595 s_sockaddr = socket_path[5:]
596 elif socket_path.find(':') >= 0:
597 s_port = socket_path.split(':')[-1]
598 s_host = ':'.join(socket_path.split(':')[:-1])
599 if s_host.startswith('[') and s_host.endswith(']'):
600 s_host = s_host[1:-1]
601 (s_family, s_socktype, s_proto, s_canonname, s_sockaddr) = socket.getaddrinfo(s_host, s_port, 0, socket.SOCK_STREAM)[0]
602 else:
603 return None
604 except:
605 return None
607 return (s_family, s_sockaddr)
610 def read_socket(s_opts, commands):
611 # print '%20s => %s %s' % (sock, s_family, s_sockaddr)
613 s = socket.socket(s_opts[0], socket.SOCK_STREAM)
614 s.connect(s_opts[1])
615 for line in commands:
616 if not line.endswith('\n'):
617 line += '\n'
618 s.send(line)
619 s.shutdown(socket.SHUT_WR)
621 answer = ''
622 try:
623 while True:
624 s.settimeout(10)
625 data = s.recv(32768)
626 if data:
627 answer += data
628 else:
629 break
630 except socket.timeout:
631 return ''
633 return answer
636 def livestatus2dict(s_opts, host=None, service=None):
637 checks = []
639 # Get host information only if NO service specified
640 if not service:
641 commands = []
642 commands.append('GET hosts\n')
643 commands.append('Columns: name state plugin_output long_plugin_output last_check\n')
644 if host:
645 commands.append('Filter: name = %s' % host)
646 answer = read_socket(s_opts, commands)
648 for line in answer.split('\n')[:-1]:
649 line = line.split(';')
650 checks.append({'host_name':line[0], 'service_description':None, 'returncode':line[1], 'output':'\n'.join([line[2], line[3]]).rstrip(), 'timestamp':str(line[4])})
652 # Get service information(s)
653 commands = []
654 commands.append('GET services\n')
655 commands.append('Columns: host_name description state plugin_output long_plugin_output last_check\n')
656 if host:
657 commands.append('Filter: host_name = %s' % host)
658 if service:
659 commands.append('Filter: description = %s' % service)
661 answer = read_socket(s_opts, commands)
663 for line in answer.split('\n')[:-1]:
664 line = line.split(';')
665 checks.append({'host_name':line[0], 'service_description':line[1], 'returncode':line[2], 'output':'\n'.join([line[3], line[4]]).rstrip(), 'timestamp':str(line[5])})
668 return checks
669 ##############################################################################