source: trunk/noc/nagios/scripts-plugins/check_svn @ 1304

Last change on this file since 1304 was 1304, checked in by quentin, 13 years ago
Use Python 2.5-compatible kill
  • Property svn:executable set to *
File size: 15.2 KB
Line 
1#!/usr/bin/env python
2#
3#   Copyright Hari Sekhon 2008
4#
5#   This program is free software; you can redistribute it and/or modify
6#   it under the terms of the GNU General Public License as published by
7#   the Free Software Foundation; either version 2 of the License, or
8#   (at your option) any later version.
9#
10#   This program is distributed in the hope that it will be useful,
11#   but WITHOUT ANY WARRANTY; without even the implied warranty of
12#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#   GNU General Public License for more details.
14#
15#   You should have received a copy of the GNU General Public License
16#   along with this program; if not, write to the Free Software
17#   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18#
19
20"""Nagios plugin to test the status of a Subversion (SVN) server. Requires
21   the subversion client "svn" to be installed somewhere in the path"""
22
23# Standard Nagios return codes
24OK       = 0
25WARNING  = 1
26CRITICAL = 2
27UNKNOWN  = 3
28
29import os
30import re
31import sys
32import signal
33import time
34try:
35    from subprocess import Popen, PIPE, STDOUT
36except ImportError:
37    print "UNKNOWN: Failed to import python subprocess module.",
38    print "Perhaps you are using a version of python older than 2.4?"
39    sys.exit(CRITICAL)
40from optparse import OptionParser
41
42__author__      = "Hari Sekhon"
43__title__       = "Nagios Plugin for Subversion"
44__version__     = 0.4
45
46DEFAULT_TIMEOUT = 10
47
48processes = []
49
50def end(status, message):
51    """Prints a message and exits. First arg is the status code
52    Second Arg is the string message"""
53
54    for process in processes:
55        try:
56                os.kill(process.pid, signal.SIGKILL)
57        except:
58                pass
59   
60    check_name = "SVN "
61    if status == OK:
62        print "%sOK: %s" % (check_name, message)
63        sys.exit(OK)
64    elif status == WARNING:
65        print "%sWARNING: %s" % (check_name, message)
66        sys.exit(WARNING)
67    elif status == CRITICAL:
68        print "%sCRITICAL: %s" % (check_name, message)
69        sys.exit(CRITICAL)
70    else:
71        # This one is intentionally different
72        print "UNKNOWN: %s" % message
73        sys.exit(UNKNOWN)
74
75
76# Pythonic version of "which", inspired by my beloved *nix core utils
77# although I've decided it makes more sense to fetch a non-executable
78# program and alert on it rather than say it wasn't found in the path
79# at all from a user perspective.
80def which(executable):
81    """Takes an executable name as a string and tests if it is in the path.
82    Returns the full path of the executable if it exists in path, or None if it
83    does not"""
84
85    for basepath in os.environ['PATH'].split(os.pathsep):
86        path = os.path.join(basepath, executable)
87        if os.path.isfile(path):
88            if os.access(path, os.X_OK):
89                return path
90            else:
91                #print >> sys.stderr, "Warning: '%s' in path is not executable"
92                end(UNKNOWN, "svn utility '%s' is not executable" % path)
93
94    return None
95
96
97BIN = which("svn")
98if not BIN:
99    end(UNKNOWN, "'svn' cannot be found in path. Please install the " \
100               + "subversion client or fix your PATH environment variable")
101
102
103class SvnTester:
104    """Holds state for the svn test"""
105
106    def __init__(self):
107        """Initializes all variables to their default states"""
108
109        self.directory  = ""
110        self.http       = False
111        self.https      = False
112        self.password   = ""
113        self.port       = ""
114        self.protocol   = "svn"
115        self.server     = ""
116        self.timeout    = DEFAULT_TIMEOUT
117        self.username   = ""
118        self.verbosity  = 0
119
120
121    def validate_variables(self):
122        """Runs through the validation of all test variables
123        Should be called before the main test to perform a sanity check
124        on the environment and settings"""
125
126        self.validate_host()
127        self.validate_protocol()
128        self.validate_port()
129        self.validate_timeout()
130
131
132    def validate_host(self):
133        """Exits with an error if the hostname
134        does not conform to expected format"""
135
136        # Input Validation - Rock my regex ;-)
137        re_hostname = re.compile("^[a-zA-Z0-9]+[a-zA-Z0-9-]*((([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6})?$")
138        re_ipaddr   = re.compile("^((25[0-5]|2[0-4]\d|[01]\d\d|\d?\d)\.){3}(25[0-5]|2[0-4]\d|[01]\d\d|\d?\d)$")
139
140        if self.server == None:
141            end(UNKNOWN, "You must supply a server hostname or ip address. " \
142                       + "See --help for details")
143
144        if not re_hostname.match(self.server) and \
145           not re_ipaddr.match(self.server):
146            end(UNKNOWN, "Server given does not appear to be a valid " \
147                       + "hostname or ip address")
148   
149
150    def validate_protocol(self):
151        """Determines the protocol to use and sets it in the object"""
152
153        if self.http and self.https:
154            end(UNKNOWN, "cannot choose both http and https, they are " \
155                       + "mutually exclusive")
156        elif self.http:   
157            self.protocol = "http"
158        elif self.https:
159            self.protocol = "https"
160        else:
161            self.protocol = "svn"
162
163
164    def validate_port(self):
165        """Exits with an error if the port is not valid"""
166
167        if self.port == None:
168            self.port = ""
169        else:
170            try:
171                self.port = int(self.port)
172                if not 1 <= self.port <= 65535:
173                    raise ValueError
174            except ValueError:
175                end(UNKNOWN, "port number must be a whole number between " \
176                           + "1 and 65535")
177
178
179    def validate_timeout(self):
180        """Exits with an error if the timeout is not valid"""
181
182        if self.timeout == None:
183            self.timeout = DEFAULT_TIMEOUT
184        try:
185            self.timeout = int(self.timeout)
186            if not 1 <= self.timeout <= 65535:
187                end(UNKNOWN, "timeout must be between 1 and 3600 seconds")
188        except ValueError:
189            end(UNKNOWN, "timeout number must be a whole number between " \
190                       + "1 and 3600 seconds")
191
192        if self.verbosity == None:
193            self.verbosity = 0
194
195
196    def run(self, cmd):
197        """runs a system command and returns a tuple containing
198        the return code and the output as a single text block"""
199
200        if cmd == "" or cmd == None:
201            end(UNKNOWN, "Internal python error - " \
202                       + "no cmd supplied for run function")
203       
204        self.vprint(3, "running command: %s" % cmd)
205
206        try:
207            process = Popen( cmd.split(), 
208                             shell=False, 
209                             stdin=PIPE, 
210                             stdout=PIPE, 
211                             stderr=STDOUT )
212            processes.append(process)
213        except OSError, error:
214            error = str(error)
215            if error == "No such file or directory":
216                end(UNKNOWN, "Cannot find utility '%s'" % cmd.split()[0])
217            else:
218                end(UNKNOWN, "Error trying to run utility '%s' - %s" \
219                                                      % (cmd.split()[0], error))
220
221        stdout, stderr = process.communicate()
222
223        if stderr == None:
224            pass
225
226        if stdout == None or stdout == "":
227            end(UNKNOWN, "No output from utility '%s'" % cmd.split()[0])
228       
229        returncode = process.returncode
230
231        self.vprint(3, "Returncode: '%s'\nOutput: '%s'" % (returncode, stdout))
232        return (returncode, str(stdout))
233
234
235    def set_timeout(self):
236        """Sets an alarm to time out the test"""
237
238        if self.timeout == 1:
239            self.vprint(2, "setting plugin timeout to 1 second")
240        else:
241            self.vprint(2, "setting plugin timeout to %s seconds"\
242                                                                % self.timeout)
243
244        signal.signal(signal.SIGALRM, self.sighandler)
245        signal.alarm(self.timeout)
246
247
248    def sighandler(self, discarded, discarded2):
249        """Function to be called by signal.alarm to kill the plugin"""
250
251        # Nop for these variables
252        discarded = discarded2
253        discarded2 = discarded
254
255        if self.timeout == 1:
256            timeout = "(1 second)"
257        else:
258            timeout = "(%s seconds)" % self.timeout
259
260        end(CRITICAL, "svn plugin has self terminated after exceeding " \
261                    + "the timeout %s" % timeout)
262
263
264    def generate_uri(self):
265        """Creates the uri and returns it as a string"""
266
267        if self.port == "" or self.port == None:
268            port = ""
269        else:
270            port = ":" + str(self.port)
271
272        if self.directory == None:
273            directory = ""
274        else:
275            directory = "/" + str(self.directory).lstrip("/")
276
277        uri = self.protocol + "://"  \
278              + str(self.server)     \
279              + str(port)            \
280              + str(directory)
281
282        return str(uri)
283
284
285    def test_svn(self):
286        """Performs the test of the subversion server"""
287
288        self.validate_variables()
289        self.set_timeout()
290
291        self.vprint(2, "now running subversion test")
292
293        uri = self.generate_uri()
294
295        self.vprint(3, "subversion server address is '%s'" % uri)
296
297        cmd = BIN + " ls " + uri + " --no-auth-cache --non-interactive"
298        if self.username:
299            cmd += " --username=%s" % self.username
300        if self.password:
301            cmd += " --password=%s" % self.password
302
303        result, output = self.run(cmd)
304       
305        if result == 0:
306            if len(output) == 0:
307                return (WARNING, "Test passed but no output was received " \
308                               + "from svn program, abnormal condition, "  \
309                               + "please check.")
310            else:
311                if self.verbosity >= 1:
312                    return(OK, "svn repository online - directory listing: %s" \
313                                        % output.replace("\n", " ").rstrip(" "))
314                else:
315                    return (OK, "svn repository online - " \
316                              + "directory listing successful")
317        else:
318            if len(output) == 0:
319                return (CRITICAL, "Connection failed. " \
320                                + "There was no output from svn")
321            else:
322                if output == "svn: Can't get password\n":
323                    output = "password required to access this repository but" \
324                           + " none was given or cached"
325                output = output.lstrip("svn: ")
326                return (CRITICAL, "Error connecting to svn server - %s " \
327                                        % output.replace("\n", " ").rstrip(" "))
328 
329
330    def vprint(self, threshold, message):
331        """Prints a message if the first arg is numerically greater than the
332        verbosity level"""
333
334        if self.verbosity >= threshold:
335            print "%s" % message
336
337
338def main():
339    """Parses args and calls func to test svn server"""
340
341    tester = SvnTester()
342    parser = OptionParser()
343    parser.add_option( "-H",
344                       "-S",
345                       "--host",
346                       "--server",
347                       dest="server",
348                       help="The Hostname or IP Address of the subversion "    \
349                          + "server")
350
351    parser.add_option( "-p",
352                       "--port",
353                       dest="port",
354                       help="The port on the server to test if not using the " \
355                          + "default port which is 3690 for svn://, 80 for "   \
356                          + "http:// or 443 for https://.")
357
358    parser.add_option( "--http",
359                       action="store_true",
360                       dest="http",
361                       help="Connect to the server using the http:// " \
362                          + "protocol (Default is svn://)")
363
364    parser.add_option( "--https",
365                       action="store_true",
366                       dest="https",
367                       help="Connect to the server using the https:// " \
368                          + "protocol (Default is svn://)")
369
370    parser.add_option( "--dir",
371                       "--directory",
372                       dest="directory",
373                       help="The directory on the host. Optional but usually " \
374                          + "necessary if using http/https, eg if using an "   \
375                          + "http WebDAV repository "                          \
376                          + "http://somehost.domain.com/repos/svn so this "    \
377                          + "would be --dir /repos/svn. Not usually needed "   \
378                          + "for the default svn:// unless you want to test "  \
379                          + "a specific directory in the repository")
380
381    parser.add_option( "-U",
382                       "--username",
383                       dest="username",
384                       help="The username to use to connect to the subversion" \
385                          + " server.")
386
387    parser.add_option( "-P",
388                       "--password",
389                       dest="password",
390                       help="The password to use to connect to the subversion" \
391                          + " server.")
392
393    parser.add_option( "-t",
394                       "--timeout",
395                       dest="timeout",
396                       help="Sets a timeout after which the the plugin will"   \
397                          + " self terminate. Defaults to %s seconds." \
398                                                              % DEFAULT_TIMEOUT)
399
400    parser.add_option( "-T",
401                       "--timing",
402                       action="store_true",
403                       dest="timing",
404                       help="Enable timer output")
405
406    parser.add_option(  "-v",
407                        "--verbose",
408                        action="count",
409                        dest="verbosity",
410                        help="Verbose mode. Good for testing plugin. By "     \
411                           + "default only one result line is printed as per" \
412                           + " Nagios standards")
413
414    parser.add_option( "-V",
415                        "--version",
416                        action = "store_true",
417                        dest = "version",
418                        help = "Print version number and exit" )
419
420    (options, args) = parser.parse_args()
421
422    if args:
423        parser.print_help()
424        sys.exit(UNKNOWN)
425
426    if options.version:
427        print "%s %s" % (__title__, __version__)
428        sys.exit(UNKNOWN)
429
430    tester.directory  = options.directory
431    tester.http       = options.http
432    tester.https      = options.https
433    tester.password   = options.password
434    tester.port       = options.port
435    tester.server     = options.server
436    tester.timeout    = options.timeout
437    tester.username   = options.username
438    tester.verbosity  = options.verbosity
439
440    if options.timing:
441        start_time = time.time()
442
443    returncode, output = tester.test_svn()
444
445    if options.timing:
446        finish_time = time.time()
447        total_time = finish_time - start_time
448       
449        output += ". Test completed in %.3f seconds" % total_time
450
451    end(returncode, output)
452    sys.exit(UNKNOWN)
453
454
455if __name__ == "__main__":
456    try:
457        main()
458    except KeyboardInterrupt:
459        print "Caught Control-C..."
460        sys.exit(CRITICAL)
Note: See TracBrowser for help on using the repository browser.