|
|
# 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()
|
|
|
...
|