kiwi-simple-metrics/kiwi_simple_metrics/metrics/external.py

128 lines
4 KiB
Python
Raw Normal View History

2023-09-02 00:46:14 +00:00
import os
import subprocess
from typing import Iterator
from ..settings import SETTINGS
from ._report import Report, ReportData
def _hwdata() -> Iterator[ReportData]:
2023-09-02 00:46:14 +00:00
def parse_output(exe: os.PathLike) -> ReportData:
try:
# check exe is executable
# => AssertionError
2023-09-02 00:46:14 +00:00
assert os.access(exe, os.X_OK)
try:
# run exe
# => TimeoutExpired: execution took too long
execution = subprocess.run(
args=[exe],
stdout=subprocess.PIPE,
timeout=SETTINGS.external.timeout,
)
stdout = execution.stdout
returncode = execution.returncode
except subprocess.TimeoutExpired as e:
# output might still be valid
assert (stdout := e.stdout) is not None
returncode = 0
2023-09-02 00:46:14 +00:00
# look at the first four output lines
# => UnicodeDecodeError: output is not decodable
output = stdout.decode().split("\n")[:4]
2023-09-02 00:46:14 +00:00
# extract and check name (fail if empty)
# => IndexError, AssertionError
2023-12-30 17:23:55 +00:00
assert (
name := "".join(char for char in output[0] if char.isprintable())[:100]
) != ""
2023-09-02 00:46:14 +00:00
# check exit status
# => AssertionError
assert returncode == 0
except (AssertionError, UnicodeDecodeError, IndexError):
2023-09-02 00:46:14 +00:00
return ReportData.from_settings(
name=os.path.basename(exe)[:100],
value=100,
settings=SETTINGS.external,
)
try:
# check output length
# => AssertionError
2023-09-02 00:46:14 +00:00
assert len(output) == 4
# extract threshold and value
# => ValueError
2023-09-02 00:46:14 +00:00
threshold = float(output[1])
value = float(output[3])
# extract and check inversion
# => AssertionError
2023-09-02 00:46:14 +00:00
assert (inverted := output[2].strip().lower()) in (
2023-12-30 17:23:55 +00:00
"normal",
"inverted",
2023-09-02 00:46:14 +00:00
)
except (AssertionError, ValueError):
2023-09-02 00:46:14 +00:00
return ReportData.from_settings(
name=name,
value=100,
settings=SETTINGS.external,
)
# success
return ReportData(
name=name,
value=value,
threshold=threshold,
inverted=inverted == "inverted",
format=SETTINGS.external.report,
)
2023-12-30 17:23:55 +00:00
yield from (parse_output(exe) for exe in SETTINGS.external.executables)
def external() -> Report | None:
2023-09-02 00:46:14 +00:00
"""
External Metric
=====
This metric's values are defined external executables (e.g. shell scripts).
Any executable with suitable output can be used as a value for this metric.
To comply, the executable's output must be UTF-8 decodable and start with
four consecutive lines holding the following information:
1. value name
2. percent threshold
3. the string "normal" or "inverted", without quotes
4. percent current value
The executable may produce additional output, which will be ignored.
Percentages may be floating point numbers and must use a decimal point "."
as a separator in that case.
The output is evaluated once execution finishes with exit status 0.
A report is generated for each executable. Its value name is stripped of
non-printable characters and limited to a length of 100.
Non-compliance will be reported as failed values, i.e. normal values with a
threshold of 0% and a value of 100%, in these cases:
- non-executable files and executables outputting non-UTF8:
reported as the files' basename
- executables with generally noncompliant outputs:
reported as the first line of output
- failure to parse any of the threshold, inversion or current value
- otherwise compliant executables with non-zero exit status
"""
return Report.aggregate(
settings=SETTINGS.external,
get_data=_hwdata,
)