# SPDX-FileCopyrightText: 2022 LEdoian # SPDX-License-Identifier: WTFPL import sys import traceback import argparse from enum import Enum from typing import NoReturn from functools import total_ordering import abc # Temporary: the only abstract method is Check.check(), until it can fall back to using Check.measure() """ CheckLib: a simple wrapper around Icinga checks dealing with common code like correct error codes and output formatting The main class is `Check`, other classes are just helpers. """ @total_ordering class Result(Enum): """ Enumeration of possible check results. The values are related return codes. Results can be compared, "larger" means worse status. WARNING < UNKNOWN < CRITICAL """ OK = 0 WARNING = 1 CRITICAL = 2 UNKNOWN = 3 def __lt__(self, other): ordering = (Result.OK, Result.WARNING, Result.UNKNOWN, Result.CRITICAL) return ordering.index(self) < ordering.index(other) class Check(abc.ABC): """ The class representing the check. Expected usage is by invoking the `run` classmethod, which overtakes the control, performs the check and returns the correct return value. The sequence is following: 1. run: Runs the check: 1. __init__: initialization - Creation of default values in the instance - Preparation of argument parser (TODO: Do we want this here?) 1. prepare_argparse: Optionally tweaks argparse's ArgumentParser, e.g. adding more parameters 1. self.args = argparse.parse_args(sys.argv) 1. check(): the function to check state and populate self.result, self.short_status and so on. 1. sys.exit with the relevant exit code according to self.result - If anything fails, e.g. by raising an exception, the run method returns the UNKNOWN result and uses the traceback as message. Instead of using self.check, the default implementation invokes measure method and compares its value to thresholds given either by semi-standard arguments, or defaults defined in the class. (If neither method is defined, the check fails. In other words, you need to either implement measure, or override check.) FIXME: It is currently not documented, which attributes of the instance are used. """ # mypy wants __init__ defined before run, because it is otherwise confused # by using self.result before assignment. I do not like tools dictating me # how to code, but this seems to be a change small enough to be granted an # exception. (But I had to at least write this here…) def __init__(self): """ Initialises the check. This method may be overridden. If so, super().__init__() should be called early in order for our attributes to be available. (It is not recommended to not call this initialization.) """ # We do not know what the result is unless somebody fills it in. self.result = Result.UNKNOWN self.short_status = None self.long_status = None # TODO: Initialize argparser @staticmethod def _bail_out() -> NoReturn: """ A last-resort fail function. Does not make any assumptions, just prints traceback (if any) and exits with UNKNOWN status. Not meant to be called by subclasses. """ short_status = 'Error, checklib bailing out!' # FIXME: do not hardcode name. long_status = 'No details known :-(' e_type, e_value, e_tb = sys.exc_info() if e_type is not None: short_status = f'EXCEPTION: {repr(e_value)}' long_status = traceback.format_exc() # Let's just hope here that the lines do not contain '|' (which would # mark performance data following)… print(short_status) if long_status: print(long_status) sys.exit(Result.UNKNOWN.value) @classmethod def run(cls, *args, **kwargs) -> NoReturn: """ This method implements the checking start-to-end. The arguments, if any, are passed to the __init__ of the class. Please run this method on the class, do not create instances. This allows to construct the instance in a `try` block that handles possible exceptions in compliance with the Plugin API. However, unless we do a dark Python magic with descriptors and stuff, it is not possible for us to know how you call it. So we cannot even warn you if you do the wrong thing. """ try: inst = cls(*args, **kwargs) # TODO: Add argparse arguments # TODO: Parse arguments into inst.args # TODO: Prime the timer inst.check() # No code from subclass should be run from this moment, so we allow # ourself to modify the instance. (This is potentially controversial.) if inst.short_status is None: inst.short_status = 'UNKNOWN: No status supplied!' inst.result = max(inst.result, Result.UNKNOWN) print(inst.short_status) if inst.long_status is not None: print(inst.long_status) return_value = inst.result.value assert isinstance(return_value, int) except BaseException: # We catch everything here, even exits. The idea is that the # subclass should not exit on its own. This however means that it # is harder to fail fast from methods other than check() – FIXME. # TODO: Special handling for timeouts Check._bail_out() # The only happy-path exit. sys.exit(return_value) @abc.abstractmethod def check(self): """ This function does the checking. Subclasses should """ # TODO: Fallback for self.measure() ...