Code

Added COPYING (GPLv2) and copyright headers to all source files.
[nagixsc.git] / nagixsc / __init__.py
index 54a2125519fb1b8dd86d7c1ded8af7cf140dd38a..030b86da2805ddf724cf9892fb49d4e6a5a2d280 100644 (file)
@@ -1,17 +1,36 @@
+# Nag(ix)SC -- __init__.py
+#
+# Copyright (C) 2009-2010 Sven Velt <sv@teamix.net>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; only version 2 of the License is applicable.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+
 import BaseHTTPServer
 import ConfigParser
 import SocketServer
 import base64
-import datetime
 import libxml2
 import mimetools
 import os
 import random
 import shlex
+import signal
 import socket
 import string
 import subprocess
 import sys
+import time
+import urllib2
 
 def debug(level, verb, string):
        if level <= verb:
@@ -20,6 +39,11 @@ def debug(level, verb, string):
 
 ##############################################################################
 
+class ExecTimeoutError(Exception):
+       pass
+
+##############################################################################
+
 def available_encodings():
        return ['base64', 'plain',]
 
@@ -60,7 +84,10 @@ def read_inifile(inifile):
 
 ##############################################################################
 
-def exec_check(host_name, service_descr, cmdline):
+def exec_timeout_handler(signum, frame):
+       raise ExecTimeoutError
+
+def exec_check(host_name, service_descr, cmdline, cmdprefix='', timeout=None, timeout_returncode=2):
        cmdarray = shlex.split(cmdline)
 
        check = {}
@@ -72,15 +99,41 @@ def exec_check(host_name, service_descr, cmdline):
                check['returncode'] = 127
                return check
 
+       check['commandline'] = cmdline
+       check['command'] = cmdarray[0].split(os.path.sep)[-1]
+
+       if cmdprefix:
+               check['fullcommandline'] = cmdprefix + ' ' + cmdline
+               cmdarray = shlex.split(cmdprefix) + cmdarray
+       else:
+               check['fullcommandline'] = cmdline
+
+       if timeout:
+               signal.signal(signal.SIGALRM, exec_timeout_handler)
+               signal.alarm(timeout)
+
        try:
-               cmd     = subprocess.Popen(cmdarray, stdout=subprocess.PIPE)
+               cmd = subprocess.Popen(cmdarray, stdout=subprocess.PIPE)
                check['output'] = cmd.communicate()[0].rstrip()
                check['returncode'] = cmd.returncode
        except OSError:
                check['output'] = 'Could not execute "%s"' % cmdline
                check['returncode'] = 127
+       except ExecTimeoutError:
+               check['output'] = 'Plugin timed out after %s seconds' % timeout
+               check['returncode'] = timeout_returncode
 
-       check['timestamp'] = datetime.datetime.now().strftime('%s')
+       if timeout:
+               signal.alarm(0)
+               try:
+                       if sys.version_info >= (2, 6):
+                               cmd.terminate()
+                       else:
+                               os.kill(cmd.pid, 15)
+               except OSError:
+                       pass
+
+       check['timestamp'] = str(long(time.time()))
        return check
 
 
@@ -89,6 +142,30 @@ def exec_check(host_name, service_descr, cmdline):
 def conf2dict(config, opt_host=None, opt_service=None):
        checks = []
 
+       # Read "plugin_timeout" from "[nagixsc]", default "None" (no timeout)
+       try:
+               timeout = config.getint('nagixsc','plugin_timeout')
+       except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+               timeout = None
+
+       # Read "plugin_timeout_returncode" from "[nagixsc]", default "2" (CRITICAL)
+       try:
+               timeout_returncode = config.getint('nagixsc','plugin_timeout_returncode')
+       except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+               timeout_returncode = 2
+
+       # Read "add_pnp4nagios_template_hint" from "[nagixsc]", default "False"
+       try:
+               add_pnp4nagios_template_hint = config.getboolean('nagixsc','add_pnp4nagios_template_hint')
+       except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+               add_pnp4nagios_template_hint = False
+
+       # Read "command_prefix" from "[nagixsc]", default "" (empty string)
+       try:
+               cmdprefix_conffile = config.get('nagixsc','command_prefix')
+       except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+               cmdprefix_conffile = ''
+
        # Sections are Hosts (not 'nagixsc'), options in sections are Services
        hosts = config.sections()
        if 'nagixsc' in hosts:
@@ -110,10 +187,18 @@ def conf2dict(config, opt_host=None, opt_service=None):
 
 
                services = config.options(host)
+               # Look for host/section specific "command_prefix"
+               if '_command_prefix' in services:
+                       cmdprefix = config.get(host, '_command_prefix')
+               else:
+                       cmdprefix = cmdprefix_conffile
+
                # Look for host check
                if '_host_check' in services and not opt_service:
                        cmdline = config.get(host, '_host_check')
-                       check = exec_check(host_name, None, cmdline)
+                       check = exec_check(host_name, None, cmdline, cmdprefix, timeout, timeout_returncode)
+                       if add_pnp4nagios_template_hint and '|' in check['output']:
+                               check['output'] += ' [%s]' % check['command']
                        checks.append(check)
 
 
@@ -129,7 +214,9 @@ def conf2dict(config, opt_host=None, opt_service=None):
                        if service[0] != '_':
                                cmdline = config.get(host, service)
 
-                               check = exec_check(host_name, service, cmdline)
+                               check = exec_check(host_name, service, cmdline, cmdprefix, timeout, timeout_returncode)
+                               if add_pnp4nagios_template_hint and '|' in check['output']:
+                                       check['output'] += ' [%s]' % check['command']
                                checks.append(check)
 
        return checks
@@ -141,7 +228,7 @@ def dict2out_passive(checks, xmltimestamp, opt_pipe, opt_verb=0):
        FORMAT_HOST = '[%s] PROCESS_HOST_CHECK_RESULT;%s;%s;%s'
        FORMAT_SERVICE = '[%s] PROCESS_SERVICE_CHECK_RESULT;%s;%s;%s;%s'
        count_services = 0
-       now = datetime.datetime.now().strftime('%s')
+       now = str(long(time.time()))
 
        # Prepare
        if opt_verb <= 2:
@@ -156,14 +243,13 @@ def dict2out_passive(checks, xmltimestamp, opt_pipe, opt_verb=0):
                        timestamp = check['timestamp']
                else:
                        timestamp = xmltimestamp
-               count_services += 1
 
                if check['service_description'] == None or check['service_description'] == '':
                        # Host check
-                       line = FORMAT_HOST % (now, check['host_name'], check['returncode'], check['output'].replace('\n', '\\n'))
+                       line = FORMAT_HOST % (timestamp, check['host_name'], check['returncode'], check['output'].replace('\n', '\\n'))
                else:
                        # Service check
-                       line =  FORMAT_SERVICE % (now, check['host_name'], check['service_description'], check['returncode'], check['output'].replace('\n', '\\n'))
+                       line =  FORMAT_SERVICE % (timestamp, check['host_name'], check['service_description'], check['returncode'], check['output'].replace('\n', '\\n'))
 
                if pipe:
                        pipe.write(line + '\n')
@@ -178,12 +264,13 @@ def dict2out_passive(checks, xmltimestamp, opt_pipe, opt_verb=0):
        return count_services
 
 
-def dict2out_checkresult(checks, xmltimestamp, opt_checkresultdir, opt_verb):
+def dict2out_checkresult(checks, xmltimestamp, opt_checkresultdir, opt_verb=0):
        count_services = 0
        count_failed = 0
        list_failed = []
        chars = string.letters + string.digits
-       ctimestamp = datetime.datetime.now().ctime()
+       ctimestamp = time.ctime()
+       random.seed()
 
        for check in checks:
                count_services += 1
@@ -216,7 +303,6 @@ def dict2out_checkresult(checks, xmltimestamp, opt_checkresultdir, opt_verb):
 
 def read_xml(options):
        if options.url != None:
-               import urllib2
 
                if options.httpuser and options.httppasswd:
                        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
@@ -247,6 +333,34 @@ def read_xml_from_string(content):
        return libxml2.parseDoc(content)
 
 
+def write_xml(xmldoc, outfile, httpuser=None, httppasswd=None):
+       if outfile.startswith('http'):
+               (headers, body) = encode_multipart(xmldoc, httpuser, httppasswd)
+               response = urllib2.urlopen(urllib2.Request(outfile, body, headers)).read()
+               return response
+
+       elif outfile == '-':
+               xmldoc.saveFormatFile('-', format=1)
+               return None
+
+       else:
+               xmldoc.saveFile(outfile)
+               return None
+
+
+def write_xml_or_die(xmldoc, outfile, httpuser=None, httppasswd=None):
+       try:
+               response = write_xml(xmldoc, outfile, httpuser, httppasswd)
+       except urllib2.HTTPError, error:
+               print error
+               sys.exit(11)
+       except urllib2.URLError, error:
+               print error.reason[1]
+               sys.exit(12)
+
+       return response
+
+
 ##############################################################################
 
 def xml_check_version(xmldoc):
@@ -276,7 +390,7 @@ def xml_get_timestamp(xmldoc):
 
 def xml_to_dict(xmldoc, verb=0, hostfilter=None, servicefilter=None):
        checks = []
-       now = int(datetime.datetime.now().strftime('%s'))
+       now = long(time.time())
        filetimestamp = reset_future_timestamp(xml_get_timestamp(xmldoc), now)
 
        if hostfilter:
@@ -306,7 +420,8 @@ def xml_to_dict(xmldoc, verb=0, hostfilter=None, servicefilter=None):
                else:
                        timestamp = filetimestamp
 
-               if retcode and output:
+               # Append only if no service filter
+               if not servicefilter and retcode and output:
                        checks.append({'host_name':hostname, 'service_description':None, 'returncode':retcode, 'output':output, 'timestamp':timestamp})
 
 
@@ -351,7 +466,7 @@ def xml_from_dict(checks, encoding='base64'):
        xmldoc = libxml2.newDoc('1.0')
        xmlroot = xmldoc.newChild(None, 'nagixsc', None)
        xmlroot.setProp('version', '1.0')
-       xmltimestamp = xmlroot.newChild(None, 'timestamp', datetime.datetime.now().strftime('%s'))
+       xmltimestamp = xmlroot.newChild(None, 'timestamp', str(long(time.time())))
 
        for entry in db:
                check = entry[1]
@@ -382,6 +497,14 @@ def xml_from_dict(checks, encoding='base64'):
        return xmldoc
 
 
+def xml_merge(xmldocs):
+       checks = []
+       for xmldoc in xmldocs:
+               checks.extend(xml_to_dict(xmldoc))
+       newxmldoc = xml_from_dict(checks)
+       return newxmldoc
+
+
 def check_mark_outdated(check, now, maxtimediff, markold):
        timedelta = now - check['timestamp']
        if timedelta > maxtimediff:
@@ -399,7 +522,7 @@ def reset_future_timestamp(timestamp, now):
 
 ##############################################################################
 
-def encode_multipart(xmldoc, httpuser, httppasswd):
+def encode_multipart(xmldoc, httpuser=None, httppasswd=None):
        BOUNDARY = mimetools.choose_boundary()
        CRLF = '\r\n'
        L = []
@@ -428,7 +551,7 @@ def daemonize(pidfile=None, stdin='/dev/null', stdout='/dev/null', stderr='/dev/
                if pid > 0:
                        sys.exit(0)
        except OSError, e:
-               sys.stderr.write("1st fork failed: (%d) %sn" % (e.errno, e.strerror))
+               sys.stderr.write("1st fork failed: (%d) %s\n" % (e.errno, e.strerror))
                sys.exit(1)
        # Prepare 2nd fork
        os.chdir("/")
@@ -440,8 +563,18 @@ def daemonize(pidfile=None, stdin='/dev/null', stdout='/dev/null', stderr='/dev/
                if pid > 0:
                        sys.exit(0)
        except OSError, e:
-               sys.stderr.write("2nd fork failed: (%d) %sn" % (e.errno, e.strerror))
+               sys.stderr.write("2nd fork failed: (%d) %s\n" % (e.errno, e.strerror))
                sys.exit(1)
+
+       # Try to write PID file
+       if pidfile:
+               pid = str(os.getpid())
+               try:
+                       file(pidfile, 'w+').write('%s\n' % pid)
+               except IOError:
+                       sys.stderr.write("Could not write PID file, exiting...\n")
+                       sys.exit(1)
+
        # Redirect stdin, stdout, stderr
        sys.stdout.flush()
        sys.stderr.flush()
@@ -452,31 +585,38 @@ def daemonize(pidfile=None, stdin='/dev/null', stdout='/dev/null', stderr='/dev/
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())
 
-       if pidfile:
-               pid = str(os.getpid())
-               file(pidfile, 'w+').write('%s\n' % pid)
-
        return
 
 ##############################################################################
 
-class MyHTTPServer(BaseHTTPServer.HTTPServer):
+if 'ForkingMixIn' in SocketServer.__dict__:
+       MixInClass = SocketServer.ForkingMixIn
+else:
+       MixInClass = SocketServer.ThreadingMixIn
+
+class MyHTTPServer(MixInClass, BaseHTTPServer.HTTPServer):
        def __init__(self, server_address, HandlerClass, ssl=False, sslpemfile=None):
+               SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
+
                if ssl:
-                       # FIXME: SSL is in Py2.6
                        try:
-                               from OpenSSL import SSL
+                               import ssl
+                               self.socket = ssl.wrap_socket(socket.socket(self.address_family, self.socket_type), keyfile=sslpemfile, certfile=sslpemfile)
+
                        except:
-                               print 'No Python OpenSSL wrapper/bindings found!'
-                               sys.exit(127)
-
-                       SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
-                       context = SSL.Context(SSL.SSLv23_METHOD)
-                       context.use_privatekey_file (sslpemfile)
-                       context.use_certificate_file(sslpemfile)
-                       self.socket = SSL.Connection(context, socket.socket(self.address_family, self.socket_type))
+
+                               try:
+                                       from OpenSSL import SSL
+                               except:
+                                       print 'No Python SSL or OpenSSL wrapper/bindings found!'
+                                       sys.exit(127)
+
+                               context = SSL.Context(SSL.SSLv23_METHOD)
+                               context.use_privatekey_file (sslpemfile)
+                               context.use_certificate_file(sslpemfile)
+                               self.socket = SSL.Connection(context, socket.socket(self.address_family, self.socket_type))
+
                else:
-                       SocketServer.BaseServer.__init__(self, server_address, HandlerClass)
                        self.socket = socket.socket(self.address_family, self.socket_type)
 
                self.server_bind()
@@ -491,3 +631,86 @@ class MyHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
 
 ##############################################################################
 
+def prepare_socket(socket_path):
+       try:
+               if socket_path.startswith('/'):
+                       s_family=socket.AF_UNIX
+                       s_sockaddr = socket_path
+               elif socket_path.startswith('unix:'):
+                       s_family=socket.AF_UNIX
+                       s_sockaddr = socket_path[5:]
+               elif socket_path.find(':') >= 0:
+                       s_port = socket_path.split(':')[-1]
+                       s_host = ':'.join(socket_path.split(':')[:-1])
+                       if s_host.startswith('[') and s_host.endswith(']'):
+                               s_host = s_host[1:-1]
+                       (s_family, s_socktype, s_proto, s_canonname, s_sockaddr) = socket.getaddrinfo(s_host, s_port, 0, socket.SOCK_STREAM)[0]
+               else:
+                       return None
+       except:
+               return None
+
+       return (s_family, s_sockaddr)
+
+
+def read_socket(s_opts, commands):
+       # print '%20s => %s %s' % (sock, s_family, s_sockaddr)
+
+       s = socket.socket(s_opts[0], socket.SOCK_STREAM)
+       s.connect(s_opts[1])
+       for line in commands:
+               if not line.endswith('\n'):
+                       line += '\n'
+               s.send(line)
+       s.shutdown(socket.SHUT_WR)
+
+       answer = ''
+       try:
+               while True:
+                       s.settimeout(10)
+                       data = s.recv(32768)
+                       if data:
+                               answer += data
+                       else:
+                               break
+       except socket.timeout:
+               return ''
+
+       return answer
+
+
+def livestatus2dict(s_opts, host=None, service=None):
+       checks = []
+
+       # Get host information only if NO service specified
+       if not service:
+               commands = []
+               commands.append('GET hosts\n')
+               commands.append('Columns: name state plugin_output long_plugin_output last_check\n')
+               if host:
+                       commands.append('Filter: name = %s' % host)
+               answer = read_socket(s_opts, commands)
+
+               for line in answer.split('\n')[:-1]:
+                       line = line.split(';')
+                       checks.append({'host_name':line[0], 'service_description':None, 'returncode':line[1], 'output':'\n'.join([line[2], line[3]]).rstrip(), 'timestamp':str(line[4])})
+
+       # Get service information(s)
+       commands = []
+       commands.append('GET services\n')
+       commands.append('Columns: host_name description state plugin_output long_plugin_output last_check\n')
+       if host:
+               commands.append('Filter: host_name = %s' % host)
+       if service:
+               commands.append('Filter: description = %s' % service)
+
+       answer = read_socket(s_opts, commands)
+
+       for line in answer.split('\n')[:-1]:
+               line = line.split(';')
+               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])})
+                               
+
+       return checks
+##############################################################################
+