Code

Fix: (Now real) random filenames in checkresultdir
[nagixsc.git] / nagixsc / __init__.py
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         if timeout:
86                 signal.signal(signal.SIGALRM, exec_timeout_handler)
87                 signal.alarm(timeout)
89         try:
90                 cmd = subprocess.Popen(cmdarray, stdout=subprocess.PIPE)
91                 check['output'] = cmd.communicate()[0].rstrip()
92                 check['returncode'] = cmd.returncode
93         except OSError:
94                 check['output'] = 'Could not execute "%s"' % cmdline
95                 check['returncode'] = 127
96         except ExecTimeoutError:
97                 check['output'] = 'Plugin timed out after %s seconds' % timeout
98                 check['returncode'] = timeout_returncode
100         if timeout:
101                 signal.alarm(0)
102                 try:
103                         if sys.version_info >= (2, 6):
104                                 cmd.terminate()
105                         else:
106                                 os.kill(cmd.pid, 15)
107                 except OSError:
108                         pass
110         check['timestamp'] = datetime.datetime.now().strftime('%s')
111         return check
114 ##############################################################################
116 def conf2dict(config, opt_host=None, opt_service=None):
117         checks = []
119         # Read "plugin_timeout" from "[nagixsc]", default "None" (no timeout)
120         try:
121                 timeout = config.getint('nagixsc','plugin_timeout')
122         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
123                 timeout = None
125         # Read "plugin_timeout_returncode" from "[nagixsc]", default "2" (CRITICAL)
126         try:
127                 timeout_returncode = config.getint('nagixsc','plugin_timeout_returncode')
128         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
129                 timeout_returncode = 2
131         # Sections are Hosts (not 'nagixsc'), options in sections are Services
132         hosts = config.sections()
133         if 'nagixsc' in hosts:
134                 hosts.remove('nagixsc')
136         # Filter out host/section if it exists
137         if opt_host:
138                 if opt_host in hosts:
139                         hosts = [opt_host,]
140                 else:
141                         hosts = []
143         for host in hosts:
144                 # Overwrite section/host name with '_host_name'
145                 if config.has_option(host,'_host_name'):
146                         host_name = config.get(host,'_host_name')
147                 else:
148                         host_name = host
151                 services = config.options(host)
152                 # Look for host check
153                 if '_host_check' in services and not opt_service:
154                         cmdline = config.get(host, '_host_check')
155                         check = exec_check(host_name, None, cmdline, timeout, timeout_returncode)
156                         checks.append(check)
159                 # Filter out service if given in cmd line options
160                 if opt_service:
161                         if opt_service in services:
162                                 services = [opt_service,]
163                         else:
164                                 services = []
166                 for service in services:
167                         # If option starts with '_' it may be a NagixSC option in the future
168                         if service[0] != '_':
169                                 cmdline = config.get(host, service)
171                                 check = exec_check(host_name, service, cmdline, timeout, timeout_returncode)
172                                 checks.append(check)
174         return checks
177 ##############################################################################
179 def dict2out_passive(checks, xmltimestamp, opt_pipe, opt_verb=0):
180         FORMAT_HOST = '[%s] PROCESS_HOST_CHECK_RESULT;%s;%s;%s'
181         FORMAT_SERVICE = '[%s] PROCESS_SERVICE_CHECK_RESULT;%s;%s;%s;%s'
182         count_services = 0
183         now = datetime.datetime.now().strftime('%s')
185         # Prepare
186         if opt_verb <= 2:
187                 pipe = open(opt_pipe, "w")
188         else:
189                 pipe = None
191         # Output
192         for check in checks:
193                 count_services += 1
194                 if check.has_key('timestamp'):
195                         timestamp = check['timestamp']
196                 else:
197                         timestamp = xmltimestamp
198                 count_services += 1
200                 if check['service_description'] == None or check['service_description'] == '':
201                         # Host check
202                         line = FORMAT_HOST % (now, check['host_name'], check['returncode'], check['output'].replace('\n', '\\n'))
203                 else:
204                         # Service check
205                         line =  FORMAT_SERVICE % (now, check['host_name'], check['service_description'], check['returncode'], check['output'].replace('\n', '\\n'))
207                 if pipe:
208                         pipe.write(line + '\n')
209                 debug(2, opt_verb, line)
211         # Close
212         if pipe:
213                 pipe.close()
214         else:
215                 print "Passive check results NOT written to Nagios pipe due to -vvv!"
217         return count_services
220 def dict2out_checkresult(checks, xmltimestamp, opt_checkresultdir, opt_verb=0):
221         count_services = 0
222         count_failed = 0
223         list_failed = []
224         chars = string.letters + string.digits
225         ctimestamp = datetime.datetime.now().ctime()
226         random.seed()
228         for check in checks:
229                 count_services += 1
230                 if check.has_key('timestamp'):
231                         timestamp = check['timestamp']
232                 else:
233                         timestamp = xmltimestamp
235                 filename = os.path.join(opt_checkresultdir, 'c' + ''.join([random.choice(chars) for i in range(6)]))
236                 try:
237                         crfile = open(filename, "w")
238                         if check['service_description'] == None or check['service_description'] == '':
239                                 # Host check
240                                 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') ) )
241                         else:
242                                 # Service check
243                                 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') ) )
244                         crfile.close()
246                         # Create OK file
247                         open(filename + '.ok', 'w').close()
248                 except:
249                         count_failed += 1
250                         list_failed.append([filename, check['host_name'], check['service_description']])
252         return (count_services, count_failed, list_failed)
255 ##############################################################################
257 def read_xml(options):
258         if options.url != None:
260                 if options.httpuser and options.httppasswd:
261                         passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
262                         passman.add_password(None, options.url, options.httpuser, options.httppasswd)
263                         authhandler = urllib2.HTTPBasicAuthHandler(passman)
264                         opener = urllib2.build_opener(authhandler)
265                         urllib2.install_opener(opener)
267                 try:
268                         response = urllib2.urlopen(options.url)
269                 except urllib2.HTTPError, error:
270                         print error
271                         sys.exit(0)
272                 except urllib2.URLError, error:
273                         print error.reason[1]
274                         sys.exit(0)
276                 doc = libxml2.parseDoc(response.read())
277                 response.close()
279         else:
280                 doc = libxml2.parseFile(options.file)
282         return doc
285 def read_xml_from_string(content):
286         return libxml2.parseDoc(content)
289 def write_xml(xmldoc, outfile, httpuser=None, httppasswd=None):
290         if outfile.startswith('http'):
291                 (headers, body) = encode_multipart(xmldoc, httpuser, httppasswd)
293                 try:
294                         response = urllib2.urlopen(urllib2.Request(outfile, body, headers)).read()
295                 except urllib2.HTTPError, error:
296                         print error
297                         sys.exit(11)
298                 except urllib2.URLError, error:
299                         print error.reason[1]
300                         sys.exit(12)
302                 print response
304         elif outfile == '-':
305                 xmldoc.saveFormatFile('-', format=1)
307         else:
308                 xmldoc.saveFile(outfile)
311 ##############################################################################
313 def xml_check_version(xmldoc):
314         # FIXME: Check XML structure
315         try:
316                 xmlnagixsc = xmldoc.xpathNewContext().xpathEval('/nagixsc')[0]
317         except:
318                 return (False, 'Not a Nag(IX)SC XML file!')
320         try:
321                 if xmlnagixsc.prop('version') != "1.0":
322                         return (False, 'Wrong version (found "%s", need "1.0") of XML file!' % xmlnagixsc.prop('version'))
323         except:
324                 return (False, 'No version information found in XML file!')
326         return (True, 'XML seems to be ok')
329 def xml_get_timestamp(xmldoc):
330         try:
331                 timestamp = int(xmldoc.xpathNewContext().xpathEval('/nagixsc/timestamp')[0].get_content())
332         except:
333                 return False
335         return timestamp
338 def xml_to_dict(xmldoc, verb=0, hostfilter=None, servicefilter=None):
339         checks = []
340         now = int(datetime.datetime.now().strftime('%s'))
341         filetimestamp = reset_future_timestamp(xml_get_timestamp(xmldoc), now)
343         if hostfilter:
344                 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host[name="%s"] | /nagixsc/host[name="%s"]' % (hostfilter, encode(hostfilter)))
345         else:
346                 hosts = xmldoc.xpathNewContext().xpathEval('/nagixsc/host')
348         for host in hosts:
349                 xmlhostname = host.xpathEval('name')[0]
350                 hostname = decode(xmlhostname.get_content(), xmlhostname.prop('encoding'))
351                 debug(2, verb, 'Found host "%s"' % hostname)
353                 # Look for Host check result
354                 if host.xpathEval('returncode'):
355                         retcode   = host.xpathEval('returncode')[0].get_content()
356                 else:
357                         retcode   = None
359                 if host.xpathEval('output'):
360                         xmloutput = host.xpathEval('output')[0]
361                         output    = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
362                 else:
363                         output    = None
365                 if host.xpathEval('timestamp'):
366                         timestamp = reset_future_timestamp(int(host.xpathEval('timestamp')[0].get_content()), now)
367                 else:
368                         timestamp = filetimestamp
370                 # Append only if no service filter
371                 if not servicefilter and retcode and output:
372                         checks.append({'host_name':hostname, 'service_description':None, 'returncode':retcode, 'output':output, 'timestamp':timestamp})
375                 # Look for service filter
376                 if servicefilter:
377                         services = host.xpathEval('service[description="%s"] | service[description="%s"]' % (servicefilter, encode(servicefilter)))
378                 else:
379                         services = host.xpathEval('service')
381                 # Loop over services in host
382                 for service in services:
383                         service_dict = {}
385                         xmldescr  = service.xpathEval('description')[0]
386                         xmloutput = service.xpathEval('output')[0]
388                         srvdescr = decode(xmldescr.get_content(), xmldescr.prop('encoding'))
389                         retcode  = service.xpathEval('returncode')[0].get_content()
390                         output   = decode(xmloutput.get_content(), xmloutput.prop('encoding')).rstrip()
392                         try:
393                                 timestamp = reset_future_timestamp(int(service.xpathEval('timestamp')[0].get_content()), now)
394                         except:
395                                 timestamp = filetimestamp
397                         debug(2, verb, 'Found service "%s"' % srvdescr)
399                         service_dict = {'host_name':hostname, 'service_description':srvdescr, 'returncode':retcode, 'output':output, 'timestamp':timestamp}
400                         checks.append(service_dict)
402                         debug(1, verb, 'Host: "%s" - Service: "%s" - RetCode: "%s" - Output: "%s"' % (hostname, srvdescr, retcode, output) )
404         return checks
407 def xml_from_dict(checks, encoding='base64'):
408         lasthost = None
410         db = [(check['host_name'], check) for check in checks]
411         db.sort()
413         xmldoc = libxml2.newDoc('1.0')
414         xmlroot = xmldoc.newChild(None, 'nagixsc', None)
415         xmlroot.setProp('version', '1.0')
416         xmltimestamp = xmlroot.newChild(None, 'timestamp', datetime.datetime.now().strftime('%s'))
418         for entry in db:
419                 check = entry[1]
421                 if check['host_name'] != lasthost:
422                         xmlhost = xmlroot.newChild(None, 'host', None)
423                         xmlhostname = xmlhost.newChild(None, 'name', encode(check['host_name'], encoding))
424                         lasthost = check['host_name']
426                 if check['service_description'] == '' or check['service_description'] == None:
427                         # Host check result
428                         xmlreturncode = xmlhost.newChild(None, 'returncode', str(check['returncode']))
429                         xmloutput     = xmlhost.newChild(None, 'output', encode(check['output'], encoding))
430                         xmloutput.setProp('encoding', encoding)
431                         if check.has_key('timestamp'):
432                                 xmltimestamp  = xmlhost.newChild(None, 'timestamp', str(check['timestamp']))
433                 else:
434                         # Service check result
435                         xmlservice    = xmlhost.newChild(None, 'service', None)
436                         xmlname       = xmlservice.newChild(None, 'description', encode(check['service_description'], encoding))
437                         xmlname.setProp('encoding', encoding)
438                         xmlreturncode = xmlservice.newChild(None, 'returncode', str(check['returncode']))
439                         xmloutput     = xmlservice.newChild(None, 'output', encode(check['output'], encoding))
440                         xmloutput.setProp('encoding', encoding)
441                         if check.has_key('timestamp'):
442                                 xmltimestamp  = xmlservice.newChild(None, 'timestamp', str(check['timestamp']))
444         return xmldoc
447 def xml_merge(xmldocs):
448         checks = []
449         for xmldoc in xmldocs:
450                 checks.extend(xml_to_dict(xmldoc))
451         newxmldoc = xml_from_dict(checks)
452         return newxmldoc
455 def check_mark_outdated(check, now, maxtimediff, markold):
456         timedelta = now - check['timestamp']
457         if timedelta > maxtimediff:
458                 check['output'] = 'Nag(ix)SC: Check result is %s(>%s) seconds old - %s' % (timedelta, maxtimediff, check['output'])
459                 if markold:
460                         check['returncode'] = 3
461         return check
464 def reset_future_timestamp(timestamp, now):
465         if timestamp <= now:
466                 return timestamp
467         else:
468                 return now
470 ##############################################################################
472 def encode_multipart(xmldoc, httpuser=None, httppasswd=None):
473         BOUNDARY = mimetools.choose_boundary()
474         CRLF = '\r\n'
475         L = []
476         L.append('--' + BOUNDARY)
477         L.append('Content-Disposition: form-data; name="xmlfile"; filename="xmlfile"')
478         L.append('Content-Type: application/xml')
479         L.append('')
480         L.append(xmldoc.serialize())
481         L.append('--' + BOUNDARY + '--')
482         L.append('')
483         body = CRLF.join(L)
484         content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
485         headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
487         if httpuser and httppasswd:
488                 headers['Authorization'] = 'Basic %s' % base64.b64encode(':'.join([httpuser, httppasswd]))
490         return (headers, body)
492 ##############################################################################
494 def daemonize(pidfile=None, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
495         # 1st fork
496         try:
497                 pid = os.fork()
498                 if pid > 0:
499                         sys.exit(0)
500         except OSError, e:
501                 sys.stderr.write("1st fork failed: (%d) %s\n" % (e.errno, e.strerror))
502                 sys.exit(1)
503         # Prepare 2nd fork
504         os.chdir("/")
505         os.umask(0)
506         os.setsid( )
507         # 2nd fork
508         try:
509                 pid = os.fork()
510                 if pid > 0:
511                         sys.exit(0)
512         except OSError, e:
513                 sys.stderr.write("2nd fork failed: (%d) %s\n" % (e.errno, e.strerror))
514                 sys.exit(1)
516         # Try to write PID file
517         if pidfile:
518                 pid = str(os.getpid())
519                 try:
520                         file(pidfile, 'w+').write('%s\n' % pid)
521                 except IOError:
522                         sys.stderr.write("Could not write PID file, exiting...\n")
523                         sys.exit(1)
525         # Redirect stdin, stdout, stderr
526         sys.stdout.flush()
527         sys.stderr.flush()
528         si = file(stdin, 'r')
529         so = file(stdout, 'a+')
530         se = file(stderr, 'a+', 0)
531         os.dup2(si.fileno(), sys.stdin.fileno())
532         os.dup2(so.fileno(), sys.stdout.fileno())
533         os.dup2(se.fileno(), sys.stderr.fileno())
535         return
537 ##############################################################################
539 class MyHTTPServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer):
540         def __init__(self, server_address, HandlerClass, ssl=False, sslpemfile=None):
541                 if ssl:
542                         # FIXME: SSL is in Py2.6
543                         try:
544                                 from OpenSSL import SSL
545                         except:
546                                 print 'No Python OpenSSL wrapper/bindings found!'
547                                 sys.exit(127)
549                         SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
550                         context = SSL.Context(SSL.SSLv23_METHOD)
551                         context.use_privatekey_file (sslpemfile)
552                         context.use_certificate_file(sslpemfile)
553                         self.socket = SSL.Connection(context, socket.socket(self.address_family, self.socket_type))
554                 else:
555                         SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
556                         self.socket = socket.socket(self.address_family, self.socket_type)
558                 self.server_bind()
559                 self.server_activate()
562 class MyHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
563         def setup(self):
564                 self.connection = self.request
565                 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
566                 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
568 ##############################################################################
570 def prepare_socket(socket_path):
571         try:
572                 if socket_path.startswith('/'):
573                         s_family=socket.AF_UNIX
574                         s_sockaddr = socket_path
575                 elif socket_path.startswith('unix:'):
576                         s_family=socket.AF_UNIX
577                         s_sockaddr = socket_path[5:]
578                 elif socket_path.find(':') >= 0:
579                         s_port = socket_path.split(':')[-1]
580                         s_host = ':'.join(socket_path.split(':')[:-1])
581                         if s_host.startswith('[') and s_host.endswith(']'):
582                                 s_host = s_host[1:-1]
583                         (s_family, s_socktype, s_proto, s_canonname, s_sockaddr) = socket.getaddrinfo(s_host, s_port, 0, socket.SOCK_STREAM)[0]
584                 else:
585                         return None
586         except:
587                 return None
589         return (s_family, s_sockaddr)
592 def read_socket(s_opts, commands):
593         # print '%20s => %s %s' % (sock, s_family, s_sockaddr)
595         s = socket.socket(s_opts[0], socket.SOCK_STREAM)
596         s.connect(s_opts[1])
597         for line in commands:
598                 if not line.endswith('\n'):
599                         line += '\n'
600                 s.send(line)
601         s.shutdown(socket.SHUT_WR)
603         answer = ''
604         try:
605                 while True:
606                         s.settimeout(10)
607                         data = s.recv(32768)
608                         if data:
609                                 answer += data
610                         else:
611                                 break
612         except socket.timeout:
613                 return ''
615         return answer
618 def livestatus2dict(s_opts, host=None, service=None):
619         checks = []
621         # Get host information only if NO service specified
622         if not service:
623                 commands = []
624                 commands.append('GET hosts\n')
625                 commands.append('Columns: name state plugin_output long_plugin_output last_check\n')
626                 if host:
627                         commands.append('Filter: name = %s' % host)
628                 answer = read_socket(s_opts, commands)
630                 for line in answer.split('\n')[:-1]:
631                         line = line.split(';')
632                         checks.append({'host_name':line[0], 'service_description':None, 'returncode':line[1], 'output':'\n'.join([line[2], line[3]]).rstrip(), 'timestamp':str(line[4])})
634         # Get service information(s)
635         commands = []
636         commands.append('GET services\n')
637         commands.append('Columns: host_name description state plugin_output long_plugin_output last_check\n')
638         if host:
639                 commands.append('Filter: host_name = %s' % host)
640         if service:
641                 commands.append('Filter: description = %s' % service)
643         answer = read_socket(s_opts, commands)
645         for line in answer.split('\n')[:-1]:
646                 line = line.split(';')
647                 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])})
648                                 
650         return checks
651 ##############################################################################