logparse / util.pyon commit rename parsers, better journald integration (e1f7605)
   1"""
   2Commonly used general functions.
   3
   4This module provides the following methods:
   5    - `hostname`:       get the current machine's hostname
   6    - `getlocaldomain`: get the current machine's domain name
   7    - `resolve`:        attempt to convert a local/public IP to hostname
   8    - `readlog`:        read contents of a log file from disk
   9And the following classes:
  10    - `LogPeriod`:      period to search logs (wrapper for datetime.timedelata)
  11"""
  12
  13from datetime import datetime, timedelta
  14import copy
  15from configparser import NoSectionError
  16import ipaddress
  17import logging
  18import os
  19from pkg_resources import Requirement, resource_filename
  20import re
  21import socket
  22from sys import exit
  23
  24from logparse import config, formatting
  25from logparse.timeparse import timeparse
  26
  27
  28logger = logging.getLogger(__name__)
  29
  30
  31def hostname(path): # get the hostname of current server
  32    """
  33    Get the hostname of the current machine using the file supplied in the
  34    `hostname-path` config option.
  35    """
  36
  37    hnfile = open(path, 'r')
  38    hn = re.search('^(\w*)\n*', hnfile.read()).group(1)
  39    return hn
  40
  41
  42def getlocaldomain(): # get the parent fqdn of current server
  43    """
  44    Get parent domain name (possibly FQDN) of the current machine. Note: if
  45    `socket.fetfqdn()` returns localhost, make sure the first entry in the
  46    hostname file includes the FQDN.
  47    """
  48
  49    domain = socket.getfqdn().split('.', 1)
  50    if len(domain) != 2:
  51        logger.warning("Could not get domain of this server, only hostname. "
  52            "Please consider updating the hostname file at {0}".format(
  53                config.prefs.get("logparse", "hostname-path")))
  54        return 'localdomain'
  55    else:
  56        return domain[-1]
  57
  58
  59def resolve(ip, fqdn=None):        # try to resolve an ip to hostname
  60    """
  61    Attempt to resolve an IP into a hostname or FQDN.
  62    Possible values for fqdn:
  63        - fqdn            show full hostname and domain
  64        - fqdn-implicit   show hostname and domain unless local
  65        - host-only       only show hostname
  66        - ip              never resolve anything
  67    Note resolve-domains settings defined in individual sections of the config
  68    take priority over the global config (this is enforced in parser modules)
  69    """
  70    
  71    if not fqdn:
  72        fqdn = config.prefs.get("logparse", "resolve-domains")
  73
  74    if fqdn == 'ip':
  75        return(ip)
  76
  77    try:
  78        ip_obj = ipaddress.ip_address(ip)
  79    except ValueError as err:
  80        logger.debug("Invalid format: " + str(err))
  81        return ip
  82
  83    try:
  84        hn = socket.gethostbyaddr(ip)[0] # resolve ip to hostname
  85    except socket.herror:
  86        # cannot resolve ip
  87        # LOGGING DISABLED TO AVOID SPAM
  88#        logger.debug(ip + " cannot be found, might not exist anymore")
  89        return(ip)
  90    except Exception as err:
  91        logger.warning("Failed to resolve hostname for " + ip + ": " + str(err))
  92        return(ip)  # return ip if no hostname exists
  93
  94    if (fqdn == "host-only") or (fqdn == "fqdn-implicit" and ip_obj.is_private):
  95        return hn.split('.')[0]
  96    if fqdn == 'fqdn' or fqdn == 'fqdn-implicit':
  97        return hn
  98    return hn
  99
 100
 101
 102def readlog(path = None, mode = 'r'):
 103    """
 104    Read a logfile from disk and return string
 105    """
 106    
 107    if (path == None):
 108        logger.error("No path provided")
 109        return 1
 110    else:
 111        if (os.path.isfile(path) is False):
 112            logger.error("Log at {0} was requested but does not exist"
 113                    .format(path))
 114            return ''
 115        else:
 116            try:
 117                return open(path, mode).read()
 118            except IOError or OSError as e:
 119                logger.error("Error reading log at {0}: {1}"
 120                        .format(path, e.strerror))
 121                return 1
 122
 123class LogPeriod:
 124    """
 125    Represents a time period for which logs should be parsed (this is given to
 126    journald.seek_realtime). Uses timeparse.py by Will Roberts.
 127    """
 128
 129    def __init__(self, section):
 130        """
 131        If no period is defined for the section config, it is inherited from
 132        the global config. Failing that, the program will die.
 133        """
 134        try:
 135            self.startdate = datetime.now() \
 136                - timeparse(config.prefs.get(section.split("_")[0], "period"))
 137            logger.debug("Parsing logs for {0} since {1}".format(
 138                section, self.startdate.strftime(formatting.DATEFMT
 139                    + " " + formatting.TIMEFMT)))
 140            self.unique = True
 141        except (NoSectionError, TypeError):
 142            logger.debug("No period defined in section {0} - inheriting "
 143                    "global period".format(section))
 144            self.startdate = datetime.now() \
 145                - timeparse(config.prefs.get("logparse", "period"))
 146            self.unique = False
 147        except Exception as e:
 148            logger.error("Could not find valid time period for parser {0}"
 149                    " {1}".format(section, e))
 150            return None
 151
 152    def compare(self, d, ephemeral=False) -> bool:
 153        """
 154        Compare the datetime `d` of a log record with the starting datetime of 
 155        this LogPeriod and return a boolean result representing whether the 
 156        log record is after the starting datetime. If either `self.startdate` 
 157        or `d` are UTC-naive, an appropriate correction is attempted. The 
 158        `ephemeral` boolean argument, if set to true, prevents permanent 
 159        modification of `self.startdate` to match the UTC-awareness of the 
 160        section's log records, in the case that `d` may represent an anomaly.
 161        """
 162        try:
 163            # First attempt a direct comparison
 164            return True if d > self.startdate else False
 165
 166        except TypeError as e:
 167            # If the direct comparison fails, make sure that both objects have 
 168            # a UTC offset
 169
 170            if d.tzinfo is None \
 171                    or (d.tzinfo is not None
 172                    and not d.tzinfo.utcoffset(d)):
 173                # d has no timezone info,
 174                # OR d has timezone info but no offset
 175
 176                if self.startdate.tzinfo is not None \
 177                        and self.startdate.tzinfo.utcoffset(self.startdate):
 178                    # If d is naive and self.startdate is aware, assume that
 179                    # the timezone of d is the same as self.startdate by
 180                    # making self.startdate naive
 181
 182                    logger.warning("{0} checking date {1}: {2}. Time "
 183                            "of log record is naive. Inheriting UTC "
 184                            "offset for logs from system date".format(
 185                        e.__class__.__name__, d, e))
 186                    if ephemeral:
 187                        # Compare with UTC-naive version of self.startdate
 188                        return True if d > self.startdate.replace(tzinfo=None) \
 189                                else False
 190                    else:
 191                        # Remove UTC awareness for self.startdate
 192                        self.startdate = self.startdate.replace(tzinfo=None)
 193                        return True if d > self.startdate else False
 194
 195            elif self.startdate.tzinfo is None \
 196                    or (self.startdate.tzinfo is not None
 197                    and not self.startdate.tzinfo.utcoffset(self.startdate)):
 198                # d has timezoneinfo and offset, but self.startdate has either 
 199                # no timezone info, or timezone  info and no offset. In this 
 200                # case, self.startdate inherits the offset of d.
 201
 202                logger.warning("{0} checking date {1}: {2}. Time of "
 203                        "start date is naive. Inheriting UTC offset "
 204                        "for date comparison from logs".format(
 205                    e.__class__.__name__, d, e))
 206
 207                if ephemeral:
 208                    # Compare with UTC-aware version of self.startdate
 209                    return True if d > self.startdate.astimezone(d.tzinfo) \
 210                            else False
 211                else:
 212                    # Add UTC awareness for self.startdate
 213                    self.startdate = self.startdate.astimezone(d.tzinfo)
 214                    return True if d > self.startdate else False
 215                        
 216            else:
 217                # Other errors return false (effectively ignore this record)
 218                logger.error("{0} comparing date {1}: {2}".format(
 219                    e.__class__.__name__, d, e))
 220                return False