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; only version 2 of the License is applicable.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 import BaseHTTPServer
19 import ConfigParser
20 import SocketServer
21 import base64
22 import libxml2
23 import mimetools
24 import os
25 import random
26 import shlex
27 import signal
28 import socket
29 import string
30 import subprocess
31 import sys
32 import time
33 import urllib2
35 def debug(level, verb, string):
36 if level <= verb:
37 print "%s: %s" % (level, string)
40 ##############################################################################
42 class ExecTimeoutError(Exception):
43 pass
45 ##############################################################################
47 def available_encodings():
48 return ['base64', 'plain',]
51 def check_encoding(enc):
52 if enc in available_encodings():
53 return True
54 else:
55 return False
58 def decode(data, encoding):
59 if encoding == 'plain':
60 return data
61 else:
62 return base64.b64decode(data)
65 def encode(data, encoding=None):
66 if encoding == 'plain':
67 return data
68 else:
69 return base64.b64encode(data)
72 ##############################################################################
74 def read_inifile(inifile):
75 config = ConfigParser.RawConfigParser()
76 config.optionxform = str # We need case-sensitive options
77 ini_list = config.read(inifile)
79 if ini_list:
80 return config
81 else:
82 return False
85 ##############################################################################
87 def exec_timeout_handler(signum, frame):
88 raise ExecTimeoutError
90 def exec_check(host_name, service_descr, cmdline, cmdprefix='', timeout=None, timeout_returncode=2):
91 cmdarray = shlex.split(cmdline)
93 check = {}
94 check['host_name'] = host_name
95 check['service_description'] = service_descr
97 if len(cmdarray) == 0:
98 check['output'] = 'No command line specified!'
99 check['returncode'] = 127
100 return check
102 check['commandline'] = cmdline
103 check['command'] = cmdarray[0].split(os.path.sep)[-1]
105 if cmdprefix:
106 check['fullcommandline'] = cmdprefix + ' ' + cmdline
107 cmdarray = shlex.split(cmdprefix) + cmdarray
108 else:
109 check['fullcommandline'] = cmdline
111 if timeout:
112 signal.signal(signal.SIGALRM, exec_timeout_handler)
113 signal.alarm(timeout)
115 try:
116 cmd = subprocess.Popen(cmdarray, stdout=subprocess.PIPE)
117 check['output'] = cmd.communicate()[0].rstrip()
118 check['returncode'] = cmd.returncode
119 except OSError:
120 check['output'] = 'Could not execute "%s"' % cmdline
121 check['returncode'] = 127
122 except ExecTimeoutError:
123 check['output'] = 'Plugin timed out after %s seconds' % timeout
124 check['returncode'] = timeout_returncode
126 if timeout:
127 signal.alarm(0)
128 try:
129 if sys.version_info >= (2, 6):
130 cmd.terminate()
131 else:
132 os.kill(cmd.pid, 15)
133 except OSError:
134 pass
136 check['timestamp'] = str(long(time.time()))
137 return check
140 ##############################################################################
142 def conf2dict(config, opt_host=None, opt_service=None):
143 checks = []
145 # Read "plugin_timeout" from "[nagixsc]", default "None" (no timeout)
146 try:
147 timeout = config.getint('nagixsc','plugin_timeout')
148 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
149 timeout = None
151 # Read "plugin_timeout_returncode" from "[nagixsc]", default "2" (CRITICAL)
152 try:
153 timeout_returncode = config.getint('nagixsc','plugin_timeout_returncode')
154 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
155 timeout_returncode = 2
157 # Read "add_pnp4nagios_template_hint" from "[nagixsc]", default "False"
158 try:
159 add_pnp4nagios_template_hint = config.getboolean('nagixsc','add_pnp4nagios_template_hint')
160 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
161 add_pnp4nagios_template_hint = False
163 # Read "command_prefix" from "[nagixsc]", default "" (empty string)
164 try:
165 cmdprefix_conffile = config.get('nagixsc','command_prefix')
166 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
167 cmdprefix_conffile = ''
169 # Sections are Hosts (not 'nagixsc'), options in sections are Services
170 hosts = config.sections()
171 if 'nagixsc' in hosts:
172 hosts.remove('nagixsc')
174 # Filter out host/section if it exists
175 if opt_host:
176 if opt_host in hosts:
177 hosts = [opt_host,]
178 else:
179 hosts = []
181 for host in hosts:
182 # Overwrite section/host name with '_host_name'
183 if config.has_option(host,'_host_name'):
184 host_name = config.get(host,'_host_name')
185 else:
186 host_name = host
189 services = config.options(host)
190 # Look for host/section specific "command_prefix"
191 if '_command_prefix' in services:
192 cmdprefix = config.get(host, '_command_prefix')
193 else:
194 cmdprefix = cmdprefix_conffile
196 # Look for host check
197 if '_host_check' in services and not opt_service:
198 cmdline = config.get(host, '_host_check')
199 check = exec_check(host_name, None, cmdline, cmdprefix, timeout, timeout_returncode)
200 if add_pnp4nagios_template_hint and '|' in check['output']:
201 check['output'] += ' [%s]' % check['command']
202 checks.append(check)
205 # Filter out service if given in cmd line options
206 if opt_service:
207 if opt_service in services:
208 services = [opt_service,]
209 else:
210 services = []
212 for service in services:
213 # If option starts with '_' it may be a NagixSC option in the future
214 if service[0] != '_':
215 cmdline = config.get(host, service)
217 check = exec_check(host_name, service, cmdline, cmdprefix, timeout, timeout_returncode)
218 if add_pnp4nagios_template_hint and '|' in check['output']:
219 check['output'] += ' [%s]' % check['command']
220 checks.append(check)
222 return checks
225 ##############################################################################
227 def dict2out_passive(checks, xmltimestamp, opt_pipe, opt_verb=0):
228 FORMAT_HOST = '[%s] PROCESS_HOST_CHECK_RESULT;%s;%s;%s'
229 FORMAT_SERVICE = '[%s] PROCESS_SERVICE_CHECK_RESULT;%s;%s;%s;%s'
230 count_services = 0
231 now = str(long(time.time()))
233 # Prepare
234 if opt_verb <= 2:
235 pipe = open(opt_pipe, "w")
236 else:
237 pipe = None
239 # Output
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 if check['service_description'] == None or check['service_description'] == '':
248 # Host check
249 line = FORMAT_HOST % (timestamp, check['host_name'], check['returncode'], check['output'].replace('\n', '\\n'))
250 else:
251 # Service check
252 line = FORMAT_SERVICE % (timestamp, check['host_name'], check['service_description'], check['returncode'], check['output'].replace('\n', '\\n'))
254 if pipe:
255 pipe.write(line + '\n')
256 debug(2, opt_verb, line)
258 # Close
259 if pipe:
260 pipe.close()
261 else:
262 print "Passive check results NOT written to Nagios pipe due to -vvv!"
264 return count_services
267 def dict2out_checkresult(checks, xmltimestamp, opt_checkresultdir, opt_verb=0):
268 count_services = 0
269 count_failed = 0
270 list_failed = []
271 chars = string.letters + string.digits
272 ctimestamp = time.ctime()
273 random.seed()
275 for check in checks:
276 count_services += 1
277 if check.has_key('timestamp'):
278 timestamp = check['timestamp']
279 else:
280 timestamp = xmltimestamp
282 filename = os.path.join(opt_checkresultdir, 'c' + ''.join([random.choice(chars) for i in range(6)]))
283 try:
284 crfile = open(filename, "w")
285 if check['service_description'] == None or check['service_description'] == '':
286 # Host check
287 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') ) )
288 else:
289 # Service check
290 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') ) )
291 crfile.close()
293 # Create OK file
294 open(filename + '.ok', 'w').close()
295 except:
296 count_failed += 1
297 list_failed.append([filename, check['host_name'], check['service_description']])
299 return (count_services, count_failed, list_failed)
302 ##############################################################################
304 def read_xml(options):
305 if options.url != None:
307 if options.httpuser and options.httppasswd:
308 passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
309 passman.add_password(None, options.url, options.httpuser, options.httppasswd)
310 authhandler = urllib2.HTTPBasicAuthHandler(passman)
311 opener = urllib2.build_opener(authhandler)
312 urllib2.install_opener(opener)
314 try:
315 response = urllib2.urlopen(options.url)
316 except urllib2.HTTPError, error:
317 print error
318 sys.exit(0)
319 except urllib2.URLError, error:
320 print error.reason[1]
321 sys.exit(0)
323 doc = libxml2.parseDoc(response.read())
324 response.close()
326 else:
327 doc = libxml2.parseFile(options.file)
329 return doc
332 def read_xml_from_string(content):
333 return libxml2.parseDoc(content)
336 def write_xml(xmldoc, outfile, httpuser=None, httppasswd=None):
337 if outfile.startswith('http'):
338 (headers, body) = encode_multipart(xmldoc, httpuser, httppasswd)
339 response = urllib2.urlopen(urllib2.Request(outfile, body, headers)).read()
340 return response
342 elif outfile == '-':
343 xmldoc.saveFormatFile('-', format=1)
344 return None
346 else:
347 xmldoc.saveFile(outfile)
348 return None
351 def write_xml_or_die(xmldoc, outfile, httpuser=None, httppasswd=None):
352 try:
353 response = write_xml(xmldoc, outfile, httpuser, httppasswd)
354 except urllib2.HTTPError, error:
355 print error
356 sys.exit(11)
357 except urllib2.URLError, error:
358 print error.reason[1]
359 sys.exit(12)
361 return response
364 ##############################################################################
366 def xml_check_version(xmldoc):
367 # FIXME: Check XML structure
368 try:
369 xmlnagixsc = xmldoc.xpathNewContext().xpathEval('/nagixsc')[0]
370 except:
371 return (False, 'Not a Nag(IX)SC XML file!')
373 try:
374 if xmlnagixsc.prop('version') != "1.0":
375 return (False, 'Wrong version (found "%s", need "1.0") of XML file!' % xmlnagixsc.prop('version'))
376 except:
377 return (False, 'No version information found in XML file!')
379 return (True, 'XML seems to be ok')
382 def xml_get_timestamp(xmldoc):
383 try:
384 timestamp = int(xmldoc.xpathNewContext().xpathEval('/nagixsc/timestamp')[0].get_content())
385 except:
386 return False
388 return timestamp
391 def xml_to_dict(xmldoc, verb=0, hostfilter=None, servicefilter=None):
392 checks = []
393 now = long(time.time())
394 filetimestamp = reset_future_timestamp(xml_get_timestamp(xmldoc), now)
396 if hostfilter:
397 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host[name="%s"] | /nagixsc/host[name="%s"]' % (hostfilter, encode(hostfilter)))
398 else:
399 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host')
401 for host in hosts:
402 xmlhostname = host.xpathEval('name')[0]
403 hostname = decode(xmlhostname.get_content(), xmlhostname.prop('encoding'))
404 debug(2, verb, 'Found host "%s"' % hostname)
406 # Look for Host check result
407 if host.xpathEval('returncode'):
408 retcode = host.xpathEval('returncode')[0].get_content()
409 else:
410 retcode = None
412 if host.xpathEval('output'):
413 xmloutput = host.xpathEval('output')[0]
414 output = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
415 else:
416 output = None
418 if host.xpathEval('timestamp'):
419 timestamp = reset_future_timestamp(int(host.xpathEval('timestamp')[0].get_content()), now)
420 else:
421 timestamp = filetimestamp
423 # Append only if no service filter
424 if not servicefilter and retcode and output:
425 checks.append({'host_name':hostname, 'service_description':None, 'returncode':retcode, 'output':output, 'timestamp':timestamp})
428 # Look for service filter
429 if servicefilter:
430 services = host.xpathEval('service[description="%s"] | service[description="%s"]' % (servicefilter, encode(servicefilter)))
431 else:
432 services = host.xpathEval('service')
434 # Loop over services in host
435 for service in services:
436 service_dict = {}
438 xmldescr = service.xpathEval('description')[0]
439 xmloutput = service.xpathEval('output')[0]
441 srvdescr = decode(xmldescr.get_content(), xmldescr.prop('encoding'))
442 retcode = service.xpathEval('returncode')[0].get_content()
443 output = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
445 try:
446 timestamp = reset_future_timestamp(int(service.xpathEval('timestamp')[0].get_content()), now)
447 except:
448 timestamp = filetimestamp
450 debug(2, verb, 'Found service "%s"' % srvdescr)
452 service_dict = {'host_name':hostname, 'service_description':srvdescr, 'returncode':retcode, 'output':output, 'timestamp':timestamp}
453 checks.append(service_dict)
455 debug(1, verb, 'Host: "%s" - Service: "%s" - RetCode: "%s" - Output: "%s"' % (hostname, srvdescr, retcode, output) )
457 return checks
460 def xml_from_dict(checks, encoding='base64'):
461 lasthost = None
463 db = [(check['host_name'], check) for check in checks]
464 db.sort()
466 xmldoc = libxml2.newDoc('1.0')
467 xmlroot = xmldoc.newChild(None, 'nagixsc', None)
468 xmlroot.setProp('version', '1.0')
469 xmltimestamp = xmlroot.newChild(None, 'timestamp', str(long(time.time())))
471 for entry in db:
472 check = entry[1]
474 if check['host_name'] != lasthost:
475 xmlhost = xmlroot.newChild(None, 'host', None)
476 xmlhostname = xmlhost.newChild(None, 'name', encode(check['host_name'], encoding))
477 lasthost = check['host_name']
479 if check['service_description'] == '' or check['service_description'] == None:
480 # Host check result
481 xmlreturncode = xmlhost.newChild(None, 'returncode', str(check['returncode']))
482 xmloutput = xmlhost.newChild(None, 'output', encode(check['output'], encoding))
483 xmloutput.setProp('encoding', encoding)
484 if check.has_key('timestamp'):
485 xmltimestamp = xmlhost.newChild(None, 'timestamp', str(check['timestamp']))
486 else:
487 # Service check result
488 xmlservice = xmlhost.newChild(None, 'service', None)
489 xmlname = xmlservice.newChild(None, 'description', encode(check['service_description'], encoding))
490 xmlname.setProp('encoding', encoding)
491 xmlreturncode = xmlservice.newChild(None, 'returncode', str(check['returncode']))
492 xmloutput = xmlservice.newChild(None, 'output', encode(check['output'], encoding))
493 xmloutput.setProp('encoding', encoding)
494 if check.has_key('timestamp'):
495 xmltimestamp = xmlservice.newChild(None, 'timestamp', str(check['timestamp']))
497 return xmldoc
500 def xml_merge(xmldocs):
501 checks = []
502 for xmldoc in xmldocs:
503 checks.extend(xml_to_dict(xmldoc))
504 newxmldoc = xml_from_dict(checks)
505 return newxmldoc
508 def check_mark_outdated(check, now, maxtimediff, markold):
509 timedelta = now - check['timestamp']
510 if timedelta > maxtimediff:
511 check['output'] = 'Nag(ix)SC: Check result is %s(>%s) seconds old - %s' % (timedelta, maxtimediff, check['output'])
512 if markold:
513 check['returncode'] = 3
514 return check
517 def reset_future_timestamp(timestamp, now):
518 if timestamp <= now:
519 return timestamp
520 else:
521 return now
523 ##############################################################################
525 def encode_multipart(xmldoc, httpuser=None, httppasswd=None):
526 BOUNDARY = mimetools.choose_boundary()
527 CRLF = '\r\n'
528 L = []
529 L.append('--' + BOUNDARY)
530 L.append('Content-Disposition: form-data; name="xmlfile"; filename="xmlfile"')
531 L.append('Content-Type: application/xml')
532 L.append('')
533 L.append(xmldoc.serialize())
534 L.append('--' + BOUNDARY + '--')
535 L.append('')
536 body = CRLF.join(L)
537 content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
538 headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
540 if httpuser and httppasswd:
541 headers['Authorization'] = 'Basic %s' % base64.b64encode(':'.join([httpuser, httppasswd]))
543 return (headers, body)
545 ##############################################################################
547 def daemonize(pidfile=None, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
548 # 1st fork
549 try:
550 pid = os.fork()
551 if pid > 0:
552 sys.exit(0)
553 except OSError, e:
554 sys.stderr.write("1st fork failed: (%d) %s\n" % (e.errno, e.strerror))
555 sys.exit(1)
556 # Prepare 2nd fork
557 os.chdir("/")
558 os.umask(0)
559 os.setsid( )
560 # 2nd fork
561 try:
562 pid = os.fork()
563 if pid > 0:
564 sys.exit(0)
565 except OSError, e:
566 sys.stderr.write("2nd fork failed: (%d) %s\n" % (e.errno, e.strerror))
567 sys.exit(1)
569 # Try to write PID file
570 if pidfile:
571 pid = str(os.getpid())
572 try:
573 file(pidfile, 'w+').write('%s\n' % pid)
574 except IOError:
575 sys.stderr.write("Could not write PID file, exiting...\n")
576 sys.exit(1)
578 # Redirect stdin, stdout, stderr
579 sys.stdout.flush()
580 sys.stderr.flush()
581 si = file(stdin, 'r')
582 so = file(stdout, 'a+')
583 se = file(stderr, 'a+', 0)
584 os.dup2(si.fileno(), sys.stdin.fileno())
585 os.dup2(so.fileno(), sys.stdout.fileno())
586 os.dup2(se.fileno(), sys.stderr.fileno())
588 return
590 ##############################################################################
592 if 'ForkingMixIn' in SocketServer.__dict__:
593 MixInClass = SocketServer.ForkingMixIn
594 else:
595 MixInClass = SocketServer.ThreadingMixIn
597 class MyHTTPServer(MixInClass, BaseHTTPServer.HTTPServer):
598 def __init__(self, server_address, HandlerClass, ssl=False, sslpemfile=None):
599 SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
601 if ssl:
602 try:
603 import ssl
604 self.socket = ssl.wrap_socket(socket.socket(self.address_family, self.socket_type), keyfile=sslpemfile, certfile=sslpemfile)
606 except:
608 try:
609 from OpenSSL import SSL
610 except:
611 print 'No Python SSL or OpenSSL wrapper/bindings found!'
612 sys.exit(127)
614 context = SSL.Context(SSL.SSLv23_METHOD)
615 context.use_privatekey_file (sslpemfile)
616 context.use_certificate_file(sslpemfile)
617 self.socket = SSL.Connection(context, socket.socket(self.address_family, self.socket_type))
619 else:
620 self.socket = socket.socket(self.address_family, self.socket_type)
622 self.server_bind()
623 self.server_activate()
626 class MyHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
627 def setup(self):
628 self.connection = self.request
629 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
630 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
632 ##############################################################################
634 def prepare_socket(socket_path):
635 try:
636 if socket_path.startswith('/'):
637 s_family=socket.AF_UNIX
638 s_sockaddr = socket_path
639 elif socket_path.startswith('unix:'):
640 s_family=socket.AF_UNIX
641 s_sockaddr = socket_path[5:]
642 elif socket_path.find(':') >= 0:
643 s_port = socket_path.split(':')[-1]
644 s_host = ':'.join(socket_path.split(':')[:-1])
645 if s_host.startswith('[') and s_host.endswith(']'):
646 s_host = s_host[1:-1]
647 (s_family, s_socktype, s_proto, s_canonname, s_sockaddr) = socket.getaddrinfo(s_host, s_port, 0, socket.SOCK_STREAM)[0]
648 else:
649 return None
650 except:
651 return None
653 return (s_family, s_sockaddr)
656 def read_socket(s_opts, commands):
657 # print '%20s => %s %s' % (sock, s_family, s_sockaddr)
659 s = socket.socket(s_opts[0], socket.SOCK_STREAM)
660 s.connect(s_opts[1])
661 for line in commands:
662 if not line.endswith('\n'):
663 line += '\n'
664 s.send(line)
665 s.shutdown(socket.SHUT_WR)
667 answer = ''
668 try:
669 while True:
670 s.settimeout(10)
671 data = s.recv(32768)
672 if data:
673 answer += data
674 else:
675 break
676 except socket.timeout:
677 return ''
679 return answer
682 def livestatus2dict(s_opts, host=None, service=None):
683 checks = []
685 # Get host information only if NO service specified
686 if not service:
687 commands = []
688 commands.append('GET hosts\n')
689 commands.append('Columns: name state plugin_output long_plugin_output last_check\n')
690 if host:
691 commands.append('Filter: name = %s' % host)
692 answer = read_socket(s_opts, commands)
694 for line in answer.split('\n')[:-1]:
695 line = line.split(';')
696 checks.append({'host_name':line[0], 'service_description':None, 'returncode':line[1], 'output':'\n'.join([line[2], line[3]]).rstrip(), 'timestamp':str(line[4])})
698 # Get service information(s)
699 commands = []
700 commands.append('GET services\n')
701 commands.append('Columns: host_name description state plugin_output long_plugin_output last_check\n')
702 if host:
703 commands.append('Filter: host_name = %s' % host)
704 if service:
705 commands.append('Filter: description = %s' % service)
707 answer = read_socket(s_opts, commands)
709 for line in answer.split('\n')[:-1]:
710 line = line.split(';')
711 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])})
714 return checks
715 ##############################################################################