1 # Nag(ix)SC -- __init__.py
2 #
3 # Copyright (C) 2009-2010 Sven Velt <sv@teamix.net>
4 #
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 import BaseHTTPServer
20 import ConfigParser
21 import SocketServer
22 import base64
23 import libxml2
24 import mimetools
25 import os
26 import random
27 import shlex
28 import signal
29 import socket
30 import string
31 import subprocess
32 import sys
33 import time
34 import urllib2
36 def debug(level, verb, string):
37 if level <= verb:
38 print "%s: %s" % (level, string)
41 ##############################################################################
43 class ExecTimeoutError(Exception):
44 pass
46 ##############################################################################
48 def available_encodings():
49 return ['base64', 'plain',]
52 def check_encoding(enc):
53 if enc in available_encodings():
54 return True
55 else:
56 return False
59 def decode(data, encoding):
60 if encoding == 'plain':
61 return data
62 else:
63 return base64.b64decode(data)
66 def encode(data, encoding=None):
67 if encoding == 'plain':
68 return data
69 else:
70 return base64.b64encode(data)
73 ##############################################################################
75 def read_inifile(inifile):
76 config = ConfigParser.RawConfigParser()
77 config.optionxform = str # We need case-sensitive options
78 ini_list = config.read(inifile)
80 if ini_list:
81 return config
82 else:
83 return False
86 ##############################################################################
88 def exec_timeout_handler(signum, frame):
89 raise ExecTimeoutError
91 def exec_check(host_name, service_descr, cmdline, cmdprefix='', timeout=None, timeout_returncode=2):
92 cmdarray = shlex.split(cmdline)
94 check = {}
95 check['host_name'] = host_name
96 check['service_description'] = service_descr
98 if len(cmdarray) == 0:
99 check['output'] = 'No command line specified!'
100 check['returncode'] = 127
101 return check
103 check['commandline'] = cmdline
104 check['command'] = cmdarray[0].split(os.path.sep)[-1]
106 if cmdprefix:
107 check['fullcommandline'] = cmdprefix + ' ' + cmdline
108 cmdarray = shlex.split(cmdprefix) + cmdarray
109 else:
110 check['fullcommandline'] = cmdline
112 if timeout:
113 signal.signal(signal.SIGALRM, exec_timeout_handler)
114 signal.alarm(timeout)
116 try:
117 cmd = subprocess.Popen(cmdarray, stdout=subprocess.PIPE)
118 check['output'] = cmd.communicate()[0].rstrip()
119 check['returncode'] = cmd.returncode
120 except OSError:
121 check['output'] = 'Could not execute "%s"' % cmdline
122 check['returncode'] = 127
123 except ExecTimeoutError:
124 check['output'] = 'Plugin timed out after %s seconds' % timeout
125 check['returncode'] = timeout_returncode
127 if timeout:
128 signal.alarm(0)
129 try:
130 if sys.version_info >= (2, 6):
131 cmd.terminate()
132 else:
133 os.kill(cmd.pid, 15)
134 except OSError:
135 pass
137 check['timestamp'] = str(long(time.time()))
138 return check
141 ##############################################################################
143 def conf2dict(config, opt_host=None, opt_service=None):
144 checks = []
146 # Read "plugin_timeout" from "[nagixsc]", default "None" (no timeout)
147 try:
148 timeout = config.getint('nagixsc','plugin_timeout')
149 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
150 timeout = None
152 # Read "plugin_timeout_returncode" from "[nagixsc]", default "2" (CRITICAL)
153 try:
154 timeout_returncode = config.getint('nagixsc','plugin_timeout_returncode')
155 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
156 timeout_returncode = 2
158 # Read "add_pnp4nagios_template_hint" from "[nagixsc]", default "False"
159 try:
160 add_pnp4nagios_template_hint = config.getboolean('nagixsc','add_pnp4nagios_template_hint')
161 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
162 add_pnp4nagios_template_hint = False
164 # Read "command_prefix" from "[nagixsc]", default "" (empty string)
165 try:
166 cmdprefix_conffile = config.get('nagixsc','command_prefix')
167 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
168 cmdprefix_conffile = ''
170 # Sections are Hosts (not 'nagixsc'), options in sections are Services
171 hosts = config.sections()
172 if 'nagixsc' in hosts:
173 hosts.remove('nagixsc')
175 # Filter out host/section if it exists
176 if opt_host:
177 if opt_host in hosts:
178 hosts = [opt_host,]
179 else:
180 hosts = []
182 for host in hosts:
183 # Overwrite section/host name with '_host_name'
184 if config.has_option(host,'_host_name'):
185 host_name = config.get(host,'_host_name')
186 else:
187 host_name = host
190 services = config.options(host)
191 # Look for host/section specific "command_prefix"
192 if '_command_prefix' in services:
193 cmdprefix = config.get(host, '_command_prefix')
194 else:
195 cmdprefix = cmdprefix_conffile
197 # Look for host check
198 if '_host_check' in services and not opt_service:
199 cmdline = config.get(host, '_host_check')
200 check = exec_check(host_name, None, cmdline, cmdprefix, timeout, timeout_returncode)
201 if add_pnp4nagios_template_hint and '|' in check['output']:
202 check['output'] += ' [%s]' % check['command']
203 checks.append(check)
206 # Filter out service if given in cmd line options
207 if opt_service:
208 if opt_service in services:
209 services = [opt_service,]
210 else:
211 services = []
213 for service in services:
214 # If option starts with '_' it may be a NagixSC option in the future
215 if service[0] != '_':
216 cmdline = config.get(host, service)
218 check = exec_check(host_name, service, cmdline, cmdprefix, timeout, timeout_returncode)
219 if add_pnp4nagios_template_hint and '|' in check['output']:
220 check['output'] += ' [%s]' % check['command']
221 checks.append(check)
223 return checks
226 ##############################################################################
228 def dict2out_passive(checks, xmltimestamp, opt_pipe, opt_verb=0):
229 FORMAT_HOST = '[%s] PROCESS_HOST_CHECK_RESULT;%s;%s;%s'
230 FORMAT_SERVICE = '[%s] PROCESS_SERVICE_CHECK_RESULT;%s;%s;%s;%s'
231 count_services = 0
232 now = str(long(time.time()))
234 # Prepare
235 if opt_verb <= 2:
236 pipe = open(opt_pipe, "w")
237 else:
238 pipe = None
240 # Output
241 for check in checks:
242 count_services += 1
243 if check.has_key('timestamp'):
244 timestamp = check['timestamp']
245 else:
246 timestamp = xmltimestamp
248 if check['service_description'] == None or check['service_description'] == '':
249 # Host check
250 line = FORMAT_HOST % (timestamp, check['host_name'], check['returncode'], check['output'].replace('\n', '\\n'))
251 else:
252 # Service check
253 line = FORMAT_SERVICE % (timestamp, check['host_name'], check['service_description'], check['returncode'], check['output'].replace('\n', '\\n'))
255 if pipe:
256 pipe.write(line + '\n')
257 debug(2, opt_verb, line)
259 # Close
260 if pipe:
261 pipe.close()
262 else:
263 print "Passive check results NOT written to Nagios pipe due to -vvv!"
265 return count_services
268 def dict2out_checkresult(checks, xmltimestamp, opt_checkresultdir, opt_verb=0):
269 count_services = 0
270 count_failed = 0
271 list_failed = []
272 chars = string.letters + string.digits
273 ctimestamp = time.ctime()
274 random.seed()
276 for check in checks:
277 count_services += 1
278 if check.has_key('timestamp'):
279 timestamp = check['timestamp']
280 else:
281 timestamp = xmltimestamp
283 filename = os.path.join(opt_checkresultdir, 'c' + ''.join([random.choice(chars) for i in range(6)]))
284 try:
285 crfile = open(filename, "w")
286 if check['service_description'] == None or check['service_description'] == '':
287 # Host check
288 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') ) )
289 else:
290 # Service check
291 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') ) )
292 crfile.close()
294 # Create OK file
295 open(filename + '.ok', 'w').close()
296 except:
297 count_failed += 1
298 list_failed.append([filename, check['host_name'], check['service_description']])
300 return (count_services, count_failed, list_failed)
303 ##############################################################################
305 def read_xml(options):
306 if options.url != None:
308 if options.httpuser and options.httppasswd:
309 passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
310 passman.add_password(None, options.url, options.httpuser, options.httppasswd)
311 authhandler = urllib2.HTTPBasicAuthHandler(passman)
312 opener = urllib2.build_opener(authhandler)
313 urllib2.install_opener(opener)
315 try:
316 response = urllib2.urlopen(options.url)
317 except urllib2.HTTPError, error:
318 print error
319 sys.exit(0)
320 except urllib2.URLError, error:
321 print error.reason[1]
322 sys.exit(0)
324 doc = libxml2.parseDoc(response.read())
325 response.close()
327 else:
328 doc = libxml2.parseFile(options.file)
330 return doc
333 def read_xml_from_string(content):
334 return libxml2.parseDoc(content)
337 def write_xml(xmldoc, outfile, httpuser=None, httppasswd=None):
338 if outfile.startswith('http'):
339 (headers, body) = encode_multipart(xmldoc, httpuser, httppasswd)
340 response = urllib2.urlopen(urllib2.Request(outfile, body, headers)).read()
341 return response
343 elif outfile == '-':
344 xmldoc.saveFormatFile('-', format=1)
345 return None
347 else:
348 xmldoc.saveFile(outfile)
349 return None
352 def write_xml_or_die(xmldoc, outfile, httpuser=None, httppasswd=None):
353 try:
354 response = write_xml(xmldoc, outfile, httpuser, httppasswd)
355 except urllib2.HTTPError, error:
356 print error
357 sys.exit(11)
358 except urllib2.URLError, error:
359 print error.reason[1]
360 sys.exit(12)
362 return response
365 ##############################################################################
367 def xml_check_version(xmldoc):
368 # FIXME: Check XML structure
369 try:
370 xmlnagixsc = xmldoc.xpathNewContext().xpathEval('/nagixsc')[0]
371 except:
372 return (False, 'Not a Nag(IX)SC XML file!')
374 try:
375 if xmlnagixsc.prop('version') != "1.0":
376 return (False, 'Wrong version (found "%s", need "1.0") of XML file!' % xmlnagixsc.prop('version'))
377 except:
378 return (False, 'No version information found in XML file!')
380 return (True, 'XML seems to be ok')
383 def xml_get_timestamp(xmldoc):
384 try:
385 timestamp = int(xmldoc.xpathNewContext().xpathEval('/nagixsc/timestamp')[0].get_content())
386 except:
387 return False
389 return timestamp
392 def xml_to_dict(xmldoc, verb=0, hostfilter=None, servicefilter=None):
393 checks = []
394 now = long(time.time())
395 filetimestamp = reset_future_timestamp(xml_get_timestamp(xmldoc), now)
397 if hostfilter:
398 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host[name="%s"] | /nagixsc/host[name="%s"]' % (hostfilter, encode(hostfilter)))
399 else:
400 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host')
402 for host in hosts:
403 xmlhostname = host.xpathEval('name')[0]
404 hostname = decode(xmlhostname.get_content(), xmlhostname.prop('encoding'))
405 debug(2, verb, 'Found host "%s"' % hostname)
407 # Look for Host check result
408 if host.xpathEval('returncode'):
409 retcode = host.xpathEval('returncode')[0].get_content()
410 else:
411 retcode = None
413 if host.xpathEval('output'):
414 xmloutput = host.xpathEval('output')[0]
415 output = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
416 else:
417 output = None
419 if host.xpathEval('timestamp'):
420 timestamp = reset_future_timestamp(int(host.xpathEval('timestamp')[0].get_content()), now)
421 else:
422 timestamp = filetimestamp
424 # Append only if no service filter
425 if not servicefilter and retcode and output:
426 checks.append({'host_name':hostname, 'service_description':None, 'returncode':retcode, 'output':output, 'timestamp':timestamp})
429 # Look for service filter
430 if servicefilter:
431 services = host.xpathEval('service[description="%s"] | service[description="%s"]' % (servicefilter, encode(servicefilter)))
432 else:
433 services = host.xpathEval('service')
435 # Loop over services in host
436 for service in services:
437 service_dict = {}
439 xmldescr = service.xpathEval('description')[0]
440 xmloutput = service.xpathEval('output')[0]
442 srvdescr = decode(xmldescr.get_content(), xmldescr.prop('encoding'))
443 retcode = service.xpathEval('returncode')[0].get_content()
444 output = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
446 try:
447 timestamp = reset_future_timestamp(int(service.xpathEval('timestamp')[0].get_content()), now)
448 except:
449 timestamp = filetimestamp
451 debug(2, verb, 'Found service "%s"' % srvdescr)
453 service_dict = {'host_name':hostname, 'service_description':srvdescr, 'returncode':retcode, 'output':output, 'timestamp':timestamp}
454 checks.append(service_dict)
456 debug(1, verb, 'Host: "%s" - Service: "%s" - RetCode: "%s" - Output: "%s"' % (hostname, srvdescr, retcode, output) )
458 return checks
461 def xml_from_dict(checks, encoding='base64'):
462 lasthost = None
464 db = [(check['host_name'], check) for check in checks]
465 db.sort()
467 xmldoc = libxml2.newDoc('1.0')
468 xmlroot = xmldoc.newChild(None, 'nagixsc', None)
469 xmlroot.setProp('version', '1.0')
470 xmltimestamp = xmlroot.newChild(None, 'timestamp', str(long(time.time())))
472 for entry in db:
473 check = entry[1]
475 if check['host_name'] != lasthost:
476 xmlhost = xmlroot.newChild(None, 'host', None)
477 xmlhostname = xmlhost.newChild(None, 'name', encode(check['host_name'], encoding))
478 lasthost = check['host_name']
480 if check['service_description'] == '' or check['service_description'] == None:
481 # Host check result
482 xmlreturncode = xmlhost.newChild(None, 'returncode', str(check['returncode']))
483 xmloutput = xmlhost.newChild(None, 'output', encode(check['output'], encoding))
484 xmloutput.setProp('encoding', encoding)
485 if check.has_key('timestamp'):
486 xmltimestamp = xmlhost.newChild(None, 'timestamp', str(check['timestamp']))
487 else:
488 # Service check result
489 xmlservice = xmlhost.newChild(None, 'service', None)
490 xmlname = xmlservice.newChild(None, 'description', encode(check['service_description'], encoding))
491 xmlname.setProp('encoding', encoding)
492 xmlreturncode = xmlservice.newChild(None, 'returncode', str(check['returncode']))
493 xmloutput = xmlservice.newChild(None, 'output', encode(check['output'], encoding))
494 xmloutput.setProp('encoding', encoding)
495 if check.has_key('timestamp'):
496 xmltimestamp = xmlservice.newChild(None, 'timestamp', str(check['timestamp']))
498 return xmldoc
501 def xml_merge(xmldocs):
502 checks = []
503 for xmldoc in xmldocs:
504 checks.extend(xml_to_dict(xmldoc))
505 newxmldoc = xml_from_dict(checks)
506 return newxmldoc
509 def check_mark_outdated(check, now, maxtimediff, markold):
510 timedelta = now - check['timestamp']
511 if timedelta > maxtimediff:
512 check['output'] = 'Nag(ix)SC: Check result is %s(>%s) seconds old - %s' % (timedelta, maxtimediff, check['output'])
513 if markold:
514 check['returncode'] = 3
515 return check
518 def reset_future_timestamp(timestamp, now):
519 if timestamp <= now:
520 return timestamp
521 else:
522 return now
524 ##############################################################################
526 def encode_multipart(xmldoc, httpuser=None, httppasswd=None):
527 BOUNDARY = mimetools.choose_boundary()
528 CRLF = '\r\n'
529 L = []
530 L.append('--' + BOUNDARY)
531 L.append('Content-Disposition: form-data; name="xmlfile"; filename="xmlfile"')
532 L.append('Content-Type: application/xml')
533 L.append('')
534 L.append(xmldoc.serialize())
535 L.append('--' + BOUNDARY + '--')
536 L.append('')
537 body = CRLF.join(L)
538 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
539 headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
541 if httpuser and httppasswd:
542 headers['Authorization'] = 'Basic %s' % base64.b64encode(':'.join([httpuser, httppasswd]))
544 return (headers, body)
546 ##############################################################################
548 def daemonize(pidfile=None, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
549 # 1st fork
550 try:
551 pid = os.fork()
552 if pid > 0:
553 sys.exit(0)
554 except OSError, e:
555 sys.stderr.write("1st fork failed: (%d) %s\n" % (e.errno, e.strerror))
556 sys.exit(1)
557 # Prepare 2nd fork
558 os.chdir("/")
559 os.umask(0)
560 os.setsid( )
561 # 2nd fork
562 try:
563 pid = os.fork()
564 if pid > 0:
565 sys.exit(0)
566 except OSError, e:
567 sys.stderr.write("2nd fork failed: (%d) %s\n" % (e.errno, e.strerror))
568 sys.exit(1)
570 # Try to write PID file
571 if pidfile:
572 pid = str(os.getpid())
573 try:
574 file(pidfile, 'w+').write('%s\n' % pid)
575 except IOError:
576 sys.stderr.write("Could not write PID file, exiting...\n")
577 sys.exit(1)
579 # Redirect stdin, stdout, stderr
580 sys.stdout.flush()
581 sys.stderr.flush()
582 si = file(stdin, 'r')
583 so = file(stdout, 'a+')
584 se = file(stderr, 'a+', 0)
585 os.dup2(si.fileno(), sys.stdin.fileno())
586 os.dup2(so.fileno(), sys.stdout.fileno())
587 os.dup2(se.fileno(), sys.stderr.fileno())
589 return
591 ##############################################################################
593 if 'ForkingMixIn' in SocketServer.__dict__:
594 MixInClass = SocketServer.ForkingMixIn
595 else:
596 MixInClass = SocketServer.ThreadingMixIn
598 class MyHTTPServer(MixInClass, BaseHTTPServer.HTTPServer):
599 def __init__(self, server_address, HandlerClass, ssl=False, sslpemfile=None):
600 SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
602 if ssl:
603 try:
604 import ssl
605 self.socket = ssl.wrap_socket(socket.socket(self.address_family, self.socket_type), keyfile=sslpemfile, certfile=sslpemfile)
607 except:
609 try:
610 from OpenSSL import SSL
611 except:
612 print 'No Python SSL or OpenSSL wrapper/bindings found!'
613 sys.exit(127)
615 context = SSL.Context(SSL.SSLv23_METHOD)
616 context.use_privatekey_file (sslpemfile)
617 context.use_certificate_file(sslpemfile)
618 self.socket = SSL.Connection(context, socket.socket(self.address_family, self.socket_type))
620 else:
621 self.socket = socket.socket(self.address_family, self.socket_type)
623 self.server_bind()
624 self.server_activate()
627 class MyHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
628 def setup(self):
629 self.connection = self.request
630 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
631 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
633 ##############################################################################
635 def prepare_socket(socket_path):
636 try:
637 if socket_path.startswith('/'):
638 s_family=socket.AF_UNIX
639 s_sockaddr = socket_path
640 elif socket_path.startswith('unix:'):
641 s_family=socket.AF_UNIX
642 s_sockaddr = socket_path[5:]
643 elif socket_path.find(':') >= 0:
644 s_port = socket_path.split(':')[-1]
645 s_host = ':'.join(socket_path.split(':')[:-1])
646 if s_host.startswith('[') and s_host.endswith(']'):
647 s_host = s_host[1:-1]
648 (s_family, s_socktype, s_proto, s_canonname, s_sockaddr) = socket.getaddrinfo(s_host, s_port, 0, socket.SOCK_STREAM)[0]
649 else:
650 return None
651 except:
652 return None
654 return (s_family, s_sockaddr)
657 def read_socket(s_opts, commands):
658 # print '%20s => %s %s' % (sock, s_family, s_sockaddr)
660 s = socket.socket(s_opts[0], socket.SOCK_STREAM)
661 s.connect(s_opts[1])
662 for line in commands:
663 if not line.endswith('\n'):
664 line += '\n'
665 s.send(line)
666 s.shutdown(socket.SHUT_WR)
668 answer = ''
669 try:
670 while True:
671 s.settimeout(10)
672 data = s.recv(32768)
673 if data:
674 answer += data
675 else:
676 break
677 except socket.timeout:
678 return ''
680 return answer
683 def livestatus2dict(s_opts, host=None, service=None):
684 checks = []
686 # Get host information only if NO service specified
687 if not service:
688 commands = []
689 commands.append('GET hosts\n')
690 commands.append('Columns: name state plugin_output long_plugin_output last_check\n')
691 if host:
692 commands.append('Filter: name = %s' % host)
693 answer = read_socket(s_opts, commands)
695 for line in answer.split('\n')[:-1]:
696 line = line.split(';')
697 checks.append({'host_name':line[0], 'service_description':None, 'returncode':line[1], 'output':'\n'.join([line[2], line[3]]).rstrip(), 'timestamp':str(line[4])})
699 # Get service information(s)
700 commands = []
701 commands.append('GET services\n')
702 commands.append('Columns: host_name description state plugin_output long_plugin_output last_check\n')
703 if host:
704 commands.append('Filter: host_name = %s' % host)
705 if service:
706 commands.append('Filter: description = %s' % service)
708 answer = read_socket(s_opts, commands)
710 for line in answer.split('\n')[:-1]:
711 line = line.split(';')
712 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])})
715 return checks
716 ##############################################################################