logparse / interface.pyon commit add parser-specific docs & rewrite sudo parser for journald (aa62c56)
   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3
   4"""
   5This module is the entrypoint of the `logparse` shell command and also contains
   6single-use functions which don't fit elsewhere. All user interaction with
   7logparse should be through this module.
   8
   9This module provides the following methods:
  10    - `main()`:         Set up arguments, config, logging, and execute parsers
  11    - `rotate()`:       Rotate logs using systemd logrotate
  12    - `rotate_sim()`:   Simulate log rotation
  13"""
  14
  15import argparse
  16from copy import copy
  17import logging
  18import logging.handlers
  19import os
  20from sys import stdin, version
  21from subprocess import check_output
  22from datetime import datetime
  23
  24import logparse
  25from logparse import formatting, mail, config, load_parsers, util
  26
  27
  28def main():
  29    """
  30    Initialisation and general management of logparse functionaliy.
  31    """
  32
  33    # Get arguments
  34
  35    global argparser
  36    argparser = get_argparser()
  37
  38    # Load config
  39
  40    config.prefs = config.loadconf(argparser.parse_args().config)
  41    if argparser.parse_args().time_period:
  42        config.prefs.set("logparse", "period",
  43                argparser.parse_args().time_period)
  44    
  45    # Set up logging
  46    logger = logging.getLogger(__name__)
  47    if (argparser.parse_args().quiet
  48            or config.prefs.getboolean("logparse", "quiet")):
  49        logparse.logger.setLevel(logging.CRITICAL)
  50    elif (argparser.parse_args().verbose 
  51            or config.prefs.getboolean("logparse", "verbose")):
  52        logparse.logger.setLevel(logging.DEBUG)
  53        logger.debug("Verbose mode turned on")
  54    else:
  55        logparse.logger.setLevel(logging.INFO)
  56
  57
  58
  59    # Time analysis
  60
  61    global start
  62    start = datetime.now()
  63    logger.info("Beginning log analysis at {0} {1}".format(
  64        start.strftime(formatting.DATEFMT), start.strftime(formatting.TIMEFMT)))
  65    logger.debug("This is {0} version {1}, running on Python {2}".format(
  66        logparse.__name__, logparse.__version__, version.replace('\n', '')))
  67     
  68    # Write header
  69
  70    formatting.init_var()
  71
  72    if argparser.parse_args().plain:
  73        output = formatting.PlaintextOutput(
  74                linewidth=config.prefs.getint("plain", "linewidth"))
  75        output.append_header()
  76    else:
  77        output = formatting.HtmlOutput()
  78        output.append_header(config.prefs.get("html", "header"))
  79
  80    # Find parsers
  81    
  82    parser_names = []
  83    ignore_logs = []
  84    if argparser.parse_args().logs:
  85        parser_names = set(argparser.parse_args().logs.split())
  86    elif config.prefs.get("logparse", "parsers"):
  87        parser_names = set(config.prefs.get("logparse", "parsers").split())
  88
  89    if argparser.parse_args().ignore_logs:
  90        ignore_logs = argparser.parse_args().ignore_logs.split()
  91    elif config.prefs.get("logparse", "ignore-parsers"):
  92        ignore_logs = config.prefs.get("logparse", "ignore-parsers").split()
  93
  94    # Set up parsers
  95
  96    loader = load_parsers.ParserLoader()
  97    if parser_names:
  98        for parser_name in parser_names:
  99            if parser_name not in ignore_logs:
 100                loader.search(parser_name)
 101    else:
 102        loader.load_pkg()
 103        if ignore_logs:
 104            loader.ignore(ignore_logs)
 105
 106    # Execute parsers
 107
 108    for parser in loader.parsers:
 109        output.append_section(parser.parse_log())
 110
 111    # Write footer
 112    output.append_footer()
 113
 114    # Write output
 115    if ((argparser.parse_args().destination
 116        or config.prefs.get("logparse", "output"))
 117        and not argparser.parse_args().no_write):
 118
 119        # Determine destination path
 120        if argparser.parse_args().destination:
 121            dest_path = argparser.parse_args().destination
 122        else:
 123            dest_path = config.prefs.get("logparse", "output")
 124
 125        logger.debug("Outputting to {0}".format(dest_path))
 126
 127        # Determine whether to clobber old file
 128        if (not os.path.isfile(dest_path)) \
 129        and not (argparser.parse_args().overwrite
 130                or config.prefs.getboolean("logparse", "overwrite")):
 131
 132            if (argparser.parse_args().embed_styles
 133                    or config.prefs.getboolean("html", "embed-styles")) \
 134                and not (argparser.parse_args().plain
 135                        or config.prefs.getboolean("plain", "plain")):
 136                # Embed CSS stylesheet
 137                output.embed_css(config.prefs.get("html", "css"))
 138                output.write_embedded(dest_path)
 139            else:
 140                output.write(dest_path)
 141
 142        elif logging.root.level == logging.CRITICAL:
 143
 144            # Don't write output if running in quiet mode (only stdout)
 145            pass
 146
 147        else:
 148
 149            logger.warning("Destination file already exists")
 150            if input("Would you like to overwrite {0}? (y/n) [n] "
 151                    .format(dest_path)) == 'y':
 152                if (argparser.parse_args().embed_styles
 153                        or config.prefs.getboolean("html", "embed-styles")) \
 154                    and not (argparser.parse_args().plain
 155                        or config.prefs.getboolean("plain", "plain")):
 156
 157                    output.embed_css(config.prefs.get("html", "css"))
 158                    output.write_embedded(dest_path)
 159
 160                else:
 161                    output.write(dest_path)
 162            else:
 163                logger.warning("No output written")
 164
 165    # Send email if requested
 166
 167    if (str(argparser.parse_args().to) or str(config.prefs.get("mail", "to"))) \
 168            and not argparser.parse_args().no_mail:
 169
 170        if str(argparser.parse_args().to):
 171            to = argparser.parse_args().to
 172        else:
 173            to = config.prefs.get("mail", "to")
 174
 175        mail.sendmail(
 176            mailbin=config.prefs.get("mail", "mailbin"),
 177            body=(output.embed_css(config.prefs.get("html", "css"))
 178                if isinstance(output, formatting.HtmlOutput) else output.content),
 179            recipient=to,
 180            subject=formatting.fsubject(config.prefs.get("mail", "subject")),
 181            html=isinstance(output, formatting.HtmlOutput),
 182            sender=config.prefs.get("mail", "from"))
 183
 184    # Rotate logs if requested
 185
 186    if not argparser.parse_args().no_rotate:
 187        if (argparser.parse_args().simulate
 188                or config.prefs.getboolean("logparse", "rotate")):
 189            rotate_sim()
 190        elif (config.prefs.getboolean("logparse", "rotate")
 191                or argparser.parse_args().rotate):
 192            rotate()
 193        else:
 194            logger.debug("User doesn't want to rotate logs")
 195    else:
 196        logger.debug("User doesn't want to rotate logs")
 197
 198    # Finish up
 199
 200    finish = datetime.now()
 201    logger.info("Finished parsing logs at {0} {1} (total time: {2})".format(
 202        finish.strftime(formatting.DATEFMT),
 203        finish.strftime(formatting.TIMEFMT),
 204        finish - start))
 205
 206    if argparser.parse_args().printout:
 207        if isinstance(output, formatting.HtmlOutput) \
 208                and argparser.parse_args().embed_styles \
 209                or config.prefs.getboolean("html", "embed-styles"):
 210            output.print_stdout_embedded()
 211        else:
 212            output.print_stdout()
 213
 214    return
 215
 216def get_argparser():
 217    """
 218    Initialise arguments (in a separate function for documentation purposes)
 219    """
 220
 221    argparser = argparse.ArgumentParser(description=
 222            'Grab logs of some common services and send them by email')
 223    argparser.add_argument('-t','--to', required=False,
 224            help='mail recipient (\"to\" address)')
 225    argparser.add_argument('-c', '--config', required=False,
 226            default="/etc/logparse/logparse.conf",
 227            help='path to config file')
 228    argparser.add_argument('-p', '--print', required=False, dest='printout',
 229            action='store_true', default=False,
 230            help='print HTML to stdout')
 231    argparser.add_argument('-d', '--destination', required=False, 
 232            help='file to output HTML')
 233    argparser.add_argument('-f', '--overwrite', required=False,
 234            action='store_true', default=False, 
 235            help='force overwrite an existing output file')
 236    argparser.add_argument('-v', '--verbose', required=False, default=False,
 237            action='store_true',
 238            help='verbose console/syslog output (for debugging)')
 239    argparser.add_argument('-r', '--rotate', required=False, default=False,
 240            action='store_true',
 241            help='force rotate log files using systemd logrotate (overrides \
 242            --rotate and "rotate" in logparse.conf)')
 243    argparser.add_argument('-nr', '--no-rotate', required=False, default=False,
 244            action='store_true', 
 245            help='do not rotate log files (overrides config)')
 246    argparser.add_argument('-s', '--simulate', required=False, default=False,
 247            action="store_true",
 248            help="test run logrotate (do not actually change files)")
 249    argparser.add_argument('-l', '--logs', required=False, 
 250            help='services to analyse')
 251    argparser.add_argument('-nl', '--ignore-logs', required=False,
 252            help='skip these services (takes precedence over -l)')
 253    argparser.add_argument('-es', '--embed-styles', required=False,
 254            default=False, action='store_true',
 255            help='make CSS rules inline rather than linking the file')
 256    argparser.add_argument('-nh', '--plain', required=False, default=False,
 257            action='store_true', help='write/send plain text rather than HTML')
 258    argparser.add_argument('-q', '--quiet', required=False, default=False,
 259            action='store_true', help='no output to stdout')
 260    argparser.add_argument('-nm', '--no-mail', required=False, default=False,
 261            action="store_true",
 262            help="do not send email (overrides config file)")
 263    argparser.add_argument('-nw', '--no-write', required=False, default=False,
 264            action="store_true",
 265            help="do not write output file (overrides config file)")
 266    argparser.add_argument('-tp', '--time-period', required=False,
 267            help="time period to analyse logs for (applies to all parsers)")
 268
 269    return argparser
 270
 271
 272
 273
 274def rotate():
 275    """
 276    Rotate logs using systemd logrotate. This requires root privileges, and a
 277    basic check for this is attempted below. Root password will be prompted
 278    for if permissions are not automatically granted.
 279    """
 280
 281    logger = logging.getLogger(__name__)
 282    try:
 283        if not os.geteuid() == 0:
 284            if stdin.isatty():
 285                logger.warning("Not running as root, using sudo \
 286                        (may require password to be entered)")
 287                rotate_shell = check_output(
 288                        "sudo logrotate /etc/logrotate.conf", shell=True)
 289            else:
 290                raise PermissionError("Root priviliges are required to run \
 291                        logrotate but were not provided")
 292        else:
 293            rotate_shell = check_output(
 294                    "/usr/sbin/logrotate /etc/logrotate.conf", shell=True)
 295        logger.info("Rotated logfiles")
 296        logger.debug("logrotate output: " + rotate_shell)
 297    except Exception as e:
 298        logger.warning("Failed to rotate log files: " + str(e))
 299
 300
 301def rotate_sim():   # Simulate log rotation
 302    """
 303    Simulate log rotation using logrotate's -d flag. This does not require root
 304    privileges, but permission errors will be shown in the output without it.
 305    """
 306
 307    logger = logging.getLogger(__name__)
 308    try:
 309        if not os.geteuid() == 0:
 310            logger.warning("Cannot run logrotate as root - \
 311                    you will see permission errors in the output below")
 312        sim_cmd = "logrotate -d /etc/logrotate.conf"
 313        logger.debug("Here is the output of `{0}` (simulated):".format(sim_cmd))
 314        sim = check_output(sim_cmd, shell=True)
 315        logger.debug(sim)
 316    except Exception as e:
 317        logger.warning("Failed to get logrotate simulation: " + str(e))