You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

168 lines
5.2 KiB
Python

# SPDX-FileCopyrightText: 2022 LEdoian <checklib@pokemon.ledoian.cz>
# 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()
...