diff --git a/checklib.py b/checklib.py index dc806fa..9a7eec7 100644 --- a/checklib.py +++ b/checklib.py @@ -1,7 +1,10 @@ import sys import traceback import argparse -from enums import Enum +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 @@ -10,18 +13,24 @@ 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. + 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 -class Check: + 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. @@ -38,7 +47,7 @@ class Check: 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_message and so on. + 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, @@ -55,13 +64,101 @@ class Check: 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): + 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() + ... diff --git a/example.py b/example.py old mode 100644 new mode 100755 index a792f02..ee3285d --- a/example.py +++ b/example.py @@ -1,9 +1,11 @@ +#!/usr/bin/env python3 + from checklib import Check, Result class MyCheck(Check): def check(self): - self.result.state = Result.UNKNOWN - self.result.short_message = 'I don\'t know :-)' + self.result = Result.UNKNOWN + self.short_status = 'I don\'t know :-)' def add_arguments(self): pass @@ -12,3 +14,7 @@ class MyCheck(Check): # If defining __init__, call super().__init__ first -- it populates # self.result, self.argumentparser and so on. + + +if __name__ == '__main__': + MyCheck.run()