"""
import importlib
+from importlib import util
from os.path import dirname
from pkgutil import iter_modules
import inspect
from pathlib import Path
+import subprocess
+from subprocess import Popen, PIPE
from typing import get_type_hints
import logging
"""
raise NotImplementedError("Failed to find an entry point for parser")
+ def check_dependencies(self) -> tuple:
+ """
+ Parsers should check their own requirements here and return a boolean
+ value reflecting whether the parser can run successfully. Typically
+ this method should check for the program whose logs are being parsed,
+ as well as any external dependencies like libsystemd. This method
+ should return a tuple containing a boolean representing whether or not
+ the dependencies are satisfied and list containing the names of any
+ dependencies that are unsatisfied.
+ """
+ return (True, None)
+
+ def _check_dependency_command(self, cmdline) -> tuple:
+ """
+ Runs a shell command (typically something --version) and returns the
+ output and return code as a tuple. The command to run is passed as a
+ string, optionally including arguments, in the `cmdline` argument.
+ """
+ logger.debug("Checking output of command " + cmdline)
+ cmd = subprocess.getstatusoutput(cmdline)
+ if cmd[0] != 0:
+ logger.warning("{0} is not available on this system (`{1}` "
+ "returned code {2}: \"{3}\")".format(
+ cmdline.split()[0], cmdline, *cmd))
+ return cmd
+ else:
+ logger.debug("Command {0} succeeded".format(cmdline))
+ return cmd
+
class ParserLoader:
self.pkg = pkg
self.path = path
self.parsers = []
+ self.has_systemd = False
def search(self, pattern):
"""
- Basic wrapper for the two search functions below.
+ Find a parser and determine its journald attribute. When a user
+ requests a parser of the form .*_journald, this function will use
+ that parser if it exists, but if not it will revert to using the
+ base parser (without the _journald) if it has a journald attribute.
+ If it does not have this error (and the parser as requested does not
+ exist), then no parser is loaded..
"""
+ # Separate into underscore words
+ split_name = pattern.split("_")
+
+ # Check if parser exists with exact name requested by user
+ result = self._search_both(pattern)
+
+ if result == None and split_name[-1] == "journald":
+ # No match for exact name but .*_journald was requested...
+ if self.has_systemd:
+ # Look for base parser with journald attribute
+ result = self._search_both("".join(split_name[:-1]))
+ if result == None:
+ logger.error("Couldn't find a matching parser module "
+ "for {0}".format(pattern))
+ if not hasattr(result, "journald"):
+ logger.error("Found parser {} but it does not support "
+ "journald".format("".join(split_name[:-1])))
+ result = None
+ else:
+ result.journald = True
+ else:
+ logger.error("A parser that requires systemd was requested "
+ "but the dependencies are not installed.")
+ return None
+ if not result.deps_ok:
+ return None
+
+ if result == None:
+ # Still can't find a matching parser
+ logger.error("Couldn't find a matching parser module "
+ "for {0}".format(pattern))
+ else:
+ self.parsers.append(result)
+
+ return result
+
+ def _search_both(self, pattern):
+ """
+ Basic wrapper for the two search functions below.
+ """
default_parser = self._search_default(pattern)
if default_parser != None:
- self.parsers.append(default_parser)
return default_parser
else:
user_parser = self._search_user(pattern)
if user_parser != None:
- self.parsers.append(user_parser)
return user_parser
else:
- logger.warning("Couldn't find a matching parser module "
- "for search term {0}".format(pattern))
return None
def _search_user(self, pattern):
4. Must provide the parse_log() method
5. Must not return None
6. Must not match an already-loaded class
+ 7. Dependencies must exist
"""
logger.debug("Checking validity of module {0} at {1}".format(
parser_module.__name__, parser_module.__file__))
available_parsers = []
+ missing_dependencies = []
clsmembers = inspect.getmembers(parser_module, inspect.isclass)
# Check individual classes
if not (issubclass(c, Parser) & (c is not Parser)):
continue
if c in self.parsers:
- logger.warning("Parser class {0} has already been loaded "
+ logger.error("Parser class {0} has already been loaded "
"from another source, ignoring it".format(
c.__class__.__name__, c.__file__))
if not inspect.isroutine(c.parse_log):
- logger.warning("Parser class {0} in {1} does not contain a "
+ logger.error("Parser class {0} in {1} does not contain a "
"parse_log() method".format(
c.__class__.__name__, c.__file__))
continue
if None in get_type_hints(c):
- logger.warning("Parser class {0} in {1} contains a "
+ logger.error("Parser class {0} in {1} contains a "
"null-returning parse_log() method".format(
c.__class__.__name__, c.__file__))
continue
logger.warning("Parser {0} is deprecated - "
"use {1} instead".format(
parser_obj.name, parser_obj.successor))
+ # Check dependencies
+ deps = parser_obj.check_dependencies()
+ if deps[0]:
+ parser_obj.deps_ok = True
+ else:
+ logger.error("The following dependencies are missing for "
+ "parser {0}: {1}".format(parser_obj.name,
+ ", ".join(deps[1])))
+ missing_dependencies.append(parser_obj)
+ parser_obj.deps_ok = False
+
logger.debug("Found parser {0}.{1}".format(
c.__module__, c.__class__.__name__))
available_parsers.append(parser_obj)
# Check module structure
+ if len(available_parsers) > 1:
+ logger.error("Found multiple valid parser classes in {0} at {1} "
+ "- ignoring this module"
+ .format(parser_module.__name__, parser_module.__file__))
+ return None
+ elif len(available_parsers) == 0:
+ if len(missing_dependencies) > 0:
+ return None
+ logger.error("No valid classes in {0} at {1}".
+ format(parser_module.__name__, parser_module.__file__))
+ return None
if len(available_parsers) == 1:
logger.debug("Parser module {0} at {1} passed validity checks"
.format(parser_module.__name__, parser_module.__file__))
return available_parsers[0]
- elif len(available_parsers) == 0:
- logger.warning("No valid classes in {0} at {1}".
- format(parser_module.__name__, parser_module.__file__))
- return None
- elif len(available_parsers) > 1:
- logger.warning("Found multiple valid parser classes in {0} at {1} "
- "- ignoring this module"
- .format(parser_module.__name__, parser_module.__file__))
- return None
+
+ def check_systemd(self):
+ """
+ Check if the appropriate dependencies are installed for parsing
+ systemd logs.
+
+ Output codes:
+ 0. systemd + libsystemd + systemd-python are installed
+ 1. systemd + libsystemd are installed
+ 2. systemd is installed
+ 3. systemd is not installed, no support required
+ """
+ # Test if systemctl works
+ systemctl_cmd = Popen(["systemctl", "--version"], stdout=PIPE)
+ systemctl_cmd.communicate()
+
+ if systemctl_cmd.returncode == 0:
+ logger.debug("Passed systemctl test")
+
+ # Test if libsystemd exists
+ libsystemd_cmd = Popen(["locate", "libsystemd.so.0"], stdout=PIPE)
+ libsystemd_cmd.communicate()
+
+ if libsystemd_cmd.returncode == 0:
+ logger.debug("Passed libsystemd test")
+
+ # Test if systemd-python exists
+ if util.find_spec("systemd") is not None:
+ logger.debug("Passed systemd-python test")
+ self.has_systemd = True
+ logger.debug("Passed all systemd dependency checks")
+ return 0
+ else:
+ logger.warning("Systemd is running on this system but the "
+ "package systemd-python is not installed. Parsers "
+ "that use journald will not work. For more "
+ "features, install systemd-python from "
+ "<https://pypi.org/project/systemd-python/> or "
+ "`pip install systemd-python`.")
+ return 1
+ else:
+ logger.warning("Systemd is running on this system but "
+ "libsystemd headers are missing. This package is "
+ "required to make use of the journald parsers. "
+ "Libsystemd should be available with your package "
+ "manager of choice.")
+ return 2
+ else:
+ logger.debug("Systemd not installed.. parsers that use journald "
+ "will not work.")
+ return 3
+
def load_pkg(self):
"""