diff --git a/Makefile b/Makefile index c39045bc..5f9fd854 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PLATFORM:=$(shell $(PYTHON) -c "from distutils.util import get_platform; print g FILESCHECK_URL:=http://localhost/~calvin/ PYTHONSRC:=${HOME}/src/cpython-hg/Lib #PYTHONSRC:=/usr/lib/$(PYTHON) -PY_FILES_DIRS:=linkcheck tests *.py linkchecker linkchecker-gui cgi-bin config doc +PY_FILES_DIRS:=linkcheck tests *.py linkchecker linkchecker-nagios linkchecker-gui cgi-bin config doc TESTS ?= tests/ # set test options, eg. to "--nologcapture" TESTOPTS= @@ -186,6 +186,7 @@ doccheck: cgi-bin/lc.wsgi \ linkchecker \ linkchecker-gui \ + linkchecker-nagios \ *.py filescheck: localbuild @@ -252,7 +253,7 @@ gui: .PHONY: count count: - @sloccount linkchecker linkchecker-gui linkcheck | grep "Total Physical Source Lines of Code" + @sloccount linkchecker linkchecker-gui linkchecker-nagios linkcheck | grep "Total Physical Source Lines of Code" # run eclipse ide .PHONY: ide diff --git a/linkcheck/cmdline.py b/linkcheck/cmdline.py new file mode 100644 index 00000000..aac80a6f --- /dev/null +++ b/linkcheck/cmdline.py @@ -0,0 +1,132 @@ +# -*- coding: iso-8859-1 -*- +# Copyright (C) 2000-2012 Bastian Kleineidam +# +# 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; either version 2 of the License, or +# (at your option) any later version. +# +# 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Utility functions suitable for command line clients. +""" +import sys +import optparse +from . import fileutil, ansicolor, strformat, add_intern_pattern, checker +from .director import console +from .decorators import notimplemented + +def print_version(exit_code=0): + """Print the program version and exit.""" + console.print_version() + sys.exit(exit_code) + + +def print_usage (msg, exit_code=2): + """Print a program msg text to stderr and exit.""" + program = sys.argv[0] + print >> console.stderr, _("Error: %(msg)s") % {"msg": msg} + print >> console.stderr, _("Execute '%(program)s -h' for help") % {"program": program} + sys.exit(exit_code) + + +class LCHelpFormatter (optparse.IndentedHelpFormatter, object): + """Help formatter indenting paragraph-wise.""" + + def __init__ (self): + """Set current console width for this formatter.""" + width = ansicolor.get_columns(sys.stdout) + super(LCHelpFormatter, self).__init__(width=width) + + def format_option (self, option): + """Customized help display with indentation.""" + # The help for each option consists of two parts: + # * the opt strings and metavars + # eg. ("-x", or "-fFILENAME, --file=FILENAME") + # * the user-supplied help string + # eg. ("turn on expert mode", "read data from FILENAME") + + # If possible, we write both of these on the same line: + # -x turn on expert mode + + # But if the opt string list is too long, we put the help + # string on a second line, indented to the same column it would + # start in if it fit on the first line. + # -fFILENAME, --file=FILENAME + # read data from FILENAME + result = [] + opts = self.option_strings[option] + opt_width = self.help_position - self.current_indent - 2 + if len(opts) > opt_width: + opts = "%*s%s\n" % (self.current_indent, "", opts) + indent_first = self.help_position + else: # start help on same line as opts + opts = "%*s%-*s " % (self.current_indent, "", opt_width, opts) + indent_first = 0 + result.append(opts) + if option.help: + text = strformat.wrap(option.help, self.help_width) + help_lines = text.splitlines() + result.append("%*s%s\n" % (indent_first, "", help_lines[0])) + result.extend(["%*s%s\n" % (self.help_position, "", line) + for line in help_lines[1:]]) + elif opts[-1] != "\n": + result.append("\n") + return "".join(result) + + +class LCOptionParser (optparse.OptionParser, object): + """Option parser with custom help text layout.""" + + def __init__ (self, err_exit_code=2): + """Initializing using our own help formatter class.""" + super(LCOptionParser, self).__init__(formatter=LCHelpFormatter()) + self.err_exit_code = err_exit_code + + def error (self, msg): + """Print usage info and given message.""" + print_usage(msg, exit_code=self.err_exit_code) + + @notimplemented + def get_usage (self): + pass + + def print_help_msg (self, s, out): + """Print a help message to stdout.""" + s = console.encode(s) + if fileutil.is_tty(out): + strformat.paginate(s) + else: + print >>out, s + sys.exit(0) + + @notimplemented + def print_help (self, file=None): + pass + + +def aggregate_url (aggregate, config, url, err_exit_code=2): + """Append given commandline URL to input queue.""" + get_url_from = checker.get_url_from + if url.lower().startswith("www."): + # syntactic sugar + url = "http://%s" % url + elif url.lower().startswith("ftp."): + # syntactic sugar + url = "ftp://%s" % url + url_data = get_url_from(url, 0, aggregate) + try: + add_intern_pattern(url_data, config) + except UnicodeError: + log.error(LOG_CMDLINE, + _("URL has unparsable domain name: %(domain)s") % + {"domain": sys.exc_info()[1]}) + sys.exit(err_exit_code) + aggregate.urlqueue.put(url_data) diff --git a/linkchecker b/linkchecker index e1ff1d3d..25ff4a77 100755 --- a/linkchecker +++ b/linkchecker @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python -u # -*- coding: iso-8859-1 -*- # Copyright (C) 2000-2012 Bastian Kleineidam # @@ -33,6 +33,8 @@ import linkcheck # override optparse gettext method with the one from linkcheck.init_i18n() optparse._ = _ # now import the rest of the linkchecker gang +from linkcheck.cmdline import print_version, print_usage, LCOptionParser, \ + aggregate_url from linkcheck import log, LOG_CMDLINE, i18n, strformat import linkcheck.checker import linkcheck.configuration @@ -195,19 +197,6 @@ file entry: for tag, desc in sorted(linkcheck.checker.const.Warnings.items())]) -def print_version (): - """Print the program version and exit.""" - console.print_version() - sys.exit(0) - - -def print_usage (msg): - """Print a program msg text to stderr and exit.""" - print >> console.stderr, _("Error: %(msg)s") % {"msg": msg} - print >> console.stderr, _("Execute 'linkchecker -h' for help") - sys.exit(1) - - def viewprof (): """Print profiling data and exit.""" if not has_pstats: @@ -246,62 +235,8 @@ def has_encoding (encoding): except LookupError: return False - -class LCHelpFormatter (optparse.IndentedHelpFormatter, object): - """Help formatter indenting paragraph-wise.""" - - def __init__ (self): - """Set current console width for this formatter.""" - width = linkcheck.ansicolor.get_columns(sys.stdout) - super(LCHelpFormatter, self).__init__(width=width) - - def format_option (self, option): - """Customized help display with indentation.""" - # The help for each option consists of two parts: - # * the opt strings and metavars - # eg. ("-x", or "-fFILENAME, --file=FILENAME") - # * the user-supplied help string - # eg. ("turn on expert mode", "read data from FILENAME") - - # If possible, we write both of these on the same line: - # -x turn on expert mode - - # But if the opt string list is too long, we put the help - # string on a second line, indented to the same column it would - # start in if it fit on the first line. - # -fFILENAME, --file=FILENAME - # read data from FILENAME - result = [] - opts = self.option_strings[option] - opt_width = self.help_position - self.current_indent - 2 - if len(opts) > opt_width: - opts = "%*s%s\n" % (self.current_indent, "", opts) - indent_first = self.help_position - else: # start help on same line as opts - opts = "%*s%-*s " % (self.current_indent, "", opt_width, opts) - indent_first = 0 - result.append(opts) - if option.help: - text = strformat.wrap(option.help, self.help_width) - help_lines = text.splitlines() - result.append("%*s%s\n" % (indent_first, "", help_lines[0])) - result.extend(["%*s%s\n" % (self.help_position, "", line) - for line in help_lines[1:]]) - elif opts[-1] != "\n": - result.append("\n") - return "".join(result) - - -class LCOptionParser (optparse.OptionParser, object): - """Option parser with custom help text layout.""" - - def __init__ (self): - """Initializing using our own help formatter class.""" - super(LCOptionParser, self).__init__(formatter=LCHelpFormatter()) - - def error (self, msg): - """Print usage info and given message.""" - print_usage(msg) +# instantiate option parser and configure options +class MyOptionParser (LCOptionParser): def get_usage (self): """Return translated usage text.""" @@ -312,15 +247,10 @@ class LCOptionParser (optparse.OptionParser, object): s = u"%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s" % (self.format_help(), Examples, LoggerTypes, RegularExpressions, CookieFormat, ProxySupport, Notes, Retval, Warnings) - s = console.encode(s) - if linkcheck.fileutil.is_tty(sys.stdout): - strformat.paginate(s) - else: - print s - sys.exit(0) + self.print_help_msg(s) # instantiate option parser and configure options -optparser = LCOptionParser() +optparser = MyOptionParser() # build a config object for this check session config = linkcheck.configuration.Configuration() @@ -524,26 +454,6 @@ if has_optcomplete: optcomplete.autocomplete(optparser, arg_completer=FileCompleter) -def aggregate_url (aggregate, config, url): - """Append given commandline URL to input queue.""" - get_url_from = linkcheck.checker.get_url_from - if url.lower().startswith("www."): - # syntactic sugar - url = "http://%s" % url - elif url.lower().startswith("ftp."): - # syntactic sugar - url = "ftp://%s" % url - url_data = get_url_from(url, 0, aggregate) - try: - linkcheck.add_intern_pattern(url_data, config) - except UnicodeError: - log.error(LOG_CMDLINE, - _("URL has unparsable domain name: %(domain)s") % - {"domain": sys.exc_info()[1]}) - sys.exit(1) - aggregate.urlqueue.put(url_data) - - def read_stdin_urls (): """Read list of URLs, separated by white-space, from stdin.""" urls = [] diff --git a/linkchecker-nagios b/linkchecker-nagios new file mode 100755 index 00000000..b49a9582 --- /dev/null +++ b/linkchecker-nagios @@ -0,0 +1,183 @@ +#!/usr/bin/python -u +# -*- coding: iso-8859-1 -*- +# Copyright (C) 2012 Bastian Kleineidam +# +# 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; either version 2 of the License, or +# (at your option) any later version. +# +# 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Check HTML pages for broken links. This is the commandline nagios plugin. +Run this file without options to see how it's done. +""" + +import sys +import os +import socket +import optparse +import pprint +# installs _() and _n() gettext functions into global namespace +import linkcheck +# override optparse gettext method with the one from linkcheck.init_i18n() +optparse._ = _ +# now import the rest of the linkchecker gang +from linkcheck.cmdline import print_version, print_usage, LCOptionParser, \ + aggregate_url +from linkcheck import log, LOG_CMDLINE, strformat +import linkcheck.checker +import linkcheck.configuration +import linkcheck.fileutil +import linkcheck.logger +import linkcheck.ansicolor +from linkcheck.director import console, check_urls, get_aggregate + +# usage texts +Usage = _("""USAGE\tlinkchecker-nagios [options] [file-or-url]...""") + +Retval = _(r"""RETURN VALUE +0 - everything checked ok without errors or warnings +1 - URL check warnings were found +2 - URL check errors were found +3 - internal or config error +""") + +Examples = _(r"""EXAMPLES + linkchecker-nagios -v www.example.org +""") + + +class MyOptionParser (LCOptionParser): + """Option parser for LinkChecker nagios plugin.""" + + def get_usage (self): + """Return translated usage text.""" + return Usage + + def print_help (self, file=None): + """Print translated help text.""" + s = u"%s\n%s\n%s" % (self.format_help(), Examples, Retval) + if file is None: + file = sys.stdout + self.print_help_msg(s, file) + +# instantiate option parser and configure options +optparser = MyOptionParser(err_exit_code=3) + +# build a config object for this check session +config = linkcheck.configuration.Configuration() +# default recursion level is 1 +config['recursionlevel'] = 1 + +# Standard options +group = optparse.OptionGroup(optparser, _("Standard nagios options")) +group.add_option("-v", "--verbose", action="count", default=0, dest="verbose", + help=_("""Increase verbosity. This option can be given multiple times.""")) +group.add_option("-V", "--version", action="store_true", dest="version", + help=_("""Print version and exit.""")) +group.add_option("-t", "--timeout", type="int", dest="timeout", + metavar="NUMBER", + help=_( +"""Set the timeout for connection attempts in seconds. The default +timeout is %d seconds.""") % config["timeout"]) + +# Checking options +group = optparse.OptionGroup(optparser, _("Checking options")) +group.add_option("-f", "--config", type="string", dest="configfile", + metavar="FILENAME", + help=_( +"""Use FILENAME as configuration file. Per default LinkChecker uses +~/.linkchecker/linkcheckerrc (under Windows +%HOMEPATH%\\.linkchecker\\linkcheckerrc).""")) + + +# read and parse command line options and arguments +(options, args) = optparser.parse_args() + +if options.version is not None: + print_version() +if options.timeout is not None: + if options.timeout > 0: + config["timeout"] = options.timeout + else: + print_usage(_("Illegal argument %(arg)r for option %(option)s") % \ + {"arg": options.timeout, "option": "'--timeout'"}, + exit_code=3) +socket.setdefaulttimeout(config["timeout"]) +if options.verbose >= 3: + debug = ['all'] +else: + debug = None +# initialize logging +config.init_logging(console.StatusLogger(), debug=debug) +log.debug(LOG_CMDLINE, _("Python %(version)s on %(platform)s") % \ + {"version": sys.version, "platform": sys.platform}) +# read configuration files +try: + files = [] + if options.configfile: + path = linkcheck.configuration.normpath(options.configfile) + if os.path.isfile(path): + files.append(path) + else: + log.warn(LOG_CMDLINE, + _("Unreadable config file: %r"), options.configfile) + sys.exit(3) + config.read(files=files) +except linkcheck.LinkCheckerError, msg: + # config error + print_usage(str(msg), exit_code=3) +linkcheck.drop_privileges() + + +socket.setdefaulttimeout(config["timeout"]) +if options.verbose < 0: + config['logger'] = config.logger_new('none') +else: + config["verbose"] = True + config["warnings"] = True + if options.verbose >= 2: + config["complete"] = True + +# check missing passwords +for entry in config["authentication"]: + if entry["password"] is None: + pattern = entry["pattern"].pattern + log.warn(LOG_CMDLINE, _("missing password for pattern %(pattern)s") % dict(pattern=pattern)) + sys.exit(3) +# sanitize the configuration +config.sanitize() + +log.debug(LOG_CMDLINE, "configuration: %s", + pprint.pformat(sorted(config.items()))) +# prepare checking queue +aggregate = get_aggregate(config) +# add urls to queue +if args: + for url in args: + aggregate_url(aggregate, config, strformat.stripurl(url), err_exit_code=3) +else: + log.warn(LOG_CMDLINE, _("no files or URLs given")) + sys.exit(3) + +# finally, start checking +check_urls(aggregate) + +stats = config['logger'].stats +# on internal errors, exit with status 3 +if stats.internal_errors: + sys.exit(3) +# on errors, exit with status 2 +if stats.errors: + sys.exit(2) +# on warnings, exit with status 1 +if stats.warnings_printed and config['warnings']: + sys.exit(1) diff --git a/setup.py b/setup.py index 2764f7ae..7921fdd1 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,9 @@ It includes the following features: - automatic MANIFEST.in check - automatic generation of .mo locale files - automatic permission setting on POSIX systems for installed files + +Because of all the features, this script is nasty and big. +Change it very careful. """ import sys @@ -581,7 +584,7 @@ library_dirs = [] # libraries libraries = [] # scripts -scripts = ['linkchecker', 'linkchecker-gui'] +scripts = ['linkchecker', 'linkchecker-gui', 'linkchecker-nagios'] if os.name == 'nt': # windows does not have unistd.h