fleet-sensor-baseline 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fleet_sensor_baseline/__init__.py +20 -0
- fleet_sensor_baseline/baseline.py +234 -0
- fleet_sensor_baseline/cli.py +559 -0
- fleet_sensor_baseline/collect/__init__.py +22 -0
- fleet_sensor_baseline/collect/backends/__init__.py +19 -0
- fleet_sensor_baseline/collect/backends/mock.py +93 -0
- fleet_sensor_baseline/collect/backends/subprocess_backend.py +94 -0
- fleet_sensor_baseline/collect/collector.py +231 -0
- fleet_sensor_baseline/drift.py +102 -0
- fleet_sensor_baseline/exits.py +57 -0
- fleet_sensor_baseline/formats.py +302 -0
- fleet_sensor_baseline/outliers.py +70 -0
- fleet_sensor_baseline/report.py +121 -0
- fleet_sensor_baseline/store.py +209 -0
- fleet_sensor_baseline/verdict.py +123 -0
- fleet_sensor_baseline/walk.py +155 -0
- fleet_sensor_baseline-0.1.0.dist-info/METADATA +225 -0
- fleet_sensor_baseline-0.1.0.dist-info/RECORD +22 -0
- fleet_sensor_baseline-0.1.0.dist-info/WHEEL +4 -0
- fleet_sensor_baseline-0.1.0.dist-info/entry_points.txt +2 -0
- fleet_sensor_baseline-0.1.0.dist-info/licenses/LICENSE +202 -0
- fleet_sensor_baseline-0.1.0.dist-info/licenses/NOTICE +38 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Fleet-wide sensor presence and configuration drift, across machines and time.
|
|
2
|
+
|
|
3
|
+
`bmc-sensor-audit` judges one machine against one declaration. This layer holds
|
|
4
|
+
the list of machines and the history of captures, and answers the two questions
|
|
5
|
+
the referee cannot: *which units differ from their cohort*, and *what changed on
|
|
6
|
+
this unit across time and firmware*.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
# One home for the version, read by `pyproject.toml` through hatchling. Two
|
|
12
|
+
# literals that happen to agree is the arrangement that drifts.
|
|
13
|
+
#
|
|
14
|
+
# **0.0.0 means unreleased, and it is load-bearing.** The community-file checks
|
|
15
|
+
# read this to decide whether the README may name a tag at all: an unreleased
|
|
16
|
+
# tree that announces `v0.1.0` hands a reader a tag to check out that does not
|
|
17
|
+
# exist. Bump this at step 1 of a release, not at step 9.
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Deriving a `fleet-baseline/1` -- an additional, labeled, downgraded declaration.
|
|
2
|
+
|
|
3
|
+
**Wherever a manufacturer declaration exists, it wins.** A fleet-derived
|
|
4
|
+
baseline is blind to an absence the whole cohort shares, which is the founding
|
|
5
|
+
problem of this family at fleet scale: two thousand trays that all lost the same
|
|
6
|
+
sensor in the same firmware agree with each other perfectly. Consensus reports
|
|
7
|
+
them clean. Only a declaration written by somebody who knew what the board has
|
|
8
|
+
can see it, and `bmc-sensor-audit coverage` is where that comparison belongs.
|
|
9
|
+
|
|
10
|
+
So this module derives, labels what it derived, and refuses to derive from a
|
|
11
|
+
cohort too small to mean anything.
|
|
12
|
+
|
|
13
|
+
**The scope is matched on DECLARED fields and never inferred.** `model` is the
|
|
14
|
+
operator's own name for a class of machine, compared as an opaque string;
|
|
15
|
+
`firmware.release` is a dotted version the collector declares. A range is never
|
|
16
|
+
matched by pattern-guessing at a vendor string like `GB200-fw-1.4.2`, because
|
|
17
|
+
the guess would be a hardcoded assumption about one vendor's formatting living
|
|
18
|
+
inside a tool that claims not to have any. A record with no `firmware.release`
|
|
19
|
+
is EXCLUDED FROM THE DENOMINATOR AND NAMED when a range is in use -- never
|
|
20
|
+
counted as absent, and never silently dropped.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from typing import Iterable, Sequence
|
|
27
|
+
|
|
28
|
+
from .formats import BASELINE_FORMAT, DOWNGRADE_NOTICE, PROVENANCE_DERIVED
|
|
29
|
+
from .store import surface_of
|
|
30
|
+
|
|
31
|
+
#: A cohort smaller than this is an anecdote wearing a format key. Lowering it
|
|
32
|
+
#: takes an explicit flag, so a nine-unit baseline is always somebody's decision.
|
|
33
|
+
DEFAULT_FLOOR = 20
|
|
34
|
+
|
|
35
|
+
DEFAULT_THRESHOLD = 0.99
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaselineError(Exception):
|
|
39
|
+
"""A derivation this module refuses, with the reason in the message."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class Selection:
|
|
44
|
+
"""What the scope selected, and what it could not judge."""
|
|
45
|
+
records: list[dict] = field(default_factory=list)
|
|
46
|
+
#: `(unit_key, reason)` for records a range could not be applied to. Kept
|
|
47
|
+
#: because a unit excluded for being unjudgeable is not a unit that failed.
|
|
48
|
+
excluded: list[tuple[str, str]] = field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_range(text: str) -> list[tuple[str, tuple[int, ...]]]:
|
|
52
|
+
"""`>=1.4,<1.5` into comparable clauses.
|
|
53
|
+
|
|
54
|
+
Deliberately small: `>=`, `>`, `<=`, `<`, `==`, over dotted integers.
|
|
55
|
+
Categorical presence needs explainable, auditable arithmetic, and a
|
|
56
|
+
comparator nobody can read by eye has no place deciding which machines are
|
|
57
|
+
in a denominator.
|
|
58
|
+
"""
|
|
59
|
+
clauses: list[tuple[str, tuple[int, ...]]] = []
|
|
60
|
+
for raw in text.split(","):
|
|
61
|
+
piece = raw.strip()
|
|
62
|
+
if not piece:
|
|
63
|
+
continue
|
|
64
|
+
for operator in (">=", "<=", "==", ">", "<"):
|
|
65
|
+
if piece.startswith(operator):
|
|
66
|
+
value = piece[len(operator):].strip()
|
|
67
|
+
clauses.append((operator, _version(value, piece)))
|
|
68
|
+
break
|
|
69
|
+
else:
|
|
70
|
+
raise BaselineError(
|
|
71
|
+
f"{piece!r} does not start with one of >=, <=, ==, >, <; a "
|
|
72
|
+
f"firmware range is a list of comparisons")
|
|
73
|
+
if not clauses:
|
|
74
|
+
raise BaselineError(f"{text!r} contains no comparisons")
|
|
75
|
+
return clauses
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _version(value: str, where: str) -> tuple[int, ...]:
|
|
79
|
+
parts = value.split(".")
|
|
80
|
+
if not value or not all(part.isdigit() for part in parts):
|
|
81
|
+
raise BaselineError(
|
|
82
|
+
f"{where!r} compares against {value!r}, which is not a dotted "
|
|
83
|
+
f"numeric version")
|
|
84
|
+
return tuple(int(part) for part in parts)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _pad(left: tuple[int, ...], right: tuple[int, ...]) -> tuple[tuple, tuple]:
|
|
88
|
+
"""`1.4` and `1.4.2` compare as `1.4.0` and `1.4.2`.
|
|
89
|
+
|
|
90
|
+
Without this, `>=1.4` refuses `1.4.2` on a tuple comparison of unequal
|
|
91
|
+
length in some orderings and accepts it in others -- a range whose answer
|
|
92
|
+
depends on how many components somebody typed.
|
|
93
|
+
"""
|
|
94
|
+
width = max(len(left), len(right))
|
|
95
|
+
return (left + (0,) * (width - len(left)), right + (0,) * (width - len(right)))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def in_range(release: str, clauses: Sequence[tuple[str, tuple[int, ...]]]) -> bool:
|
|
99
|
+
version = _version(release, release)
|
|
100
|
+
for operator, bound in clauses:
|
|
101
|
+
left, right = _pad(version, bound)
|
|
102
|
+
if operator == ">=" and not left >= right:
|
|
103
|
+
return False
|
|
104
|
+
if operator == ">" and not left > right:
|
|
105
|
+
return False
|
|
106
|
+
if operator == "<=" and not left <= right:
|
|
107
|
+
return False
|
|
108
|
+
if operator == "<" and not left < right:
|
|
109
|
+
return False
|
|
110
|
+
if operator == "==" and left != right:
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def select(records: Iterable[dict], *, model: str | None = None,
|
|
116
|
+
firmware_range: str | None = None,
|
|
117
|
+
firmware: str | None = None) -> Selection:
|
|
118
|
+
"""Records within a scope, plus the ones a range could not judge."""
|
|
119
|
+
clauses = parse_range(firmware_range) if firmware_range else None
|
|
120
|
+
out = Selection()
|
|
121
|
+
for record in records:
|
|
122
|
+
if model is not None and record.get("model") != model:
|
|
123
|
+
continue
|
|
124
|
+
info = record.get("firmware") or {}
|
|
125
|
+
if firmware is not None and info.get("version") != firmware:
|
|
126
|
+
continue
|
|
127
|
+
if clauses is not None:
|
|
128
|
+
release = info.get("release")
|
|
129
|
+
if not isinstance(release, str) or not release:
|
|
130
|
+
out.excluded.append((
|
|
131
|
+
record["unit_key"],
|
|
132
|
+
"no firmware.release to compare against the range; a "
|
|
133
|
+
"version string is not a version"))
|
|
134
|
+
continue
|
|
135
|
+
try:
|
|
136
|
+
if not in_range(release, clauses):
|
|
137
|
+
continue
|
|
138
|
+
except BaselineError as exc:
|
|
139
|
+
out.excluded.append((record["unit_key"], str(exc)))
|
|
140
|
+
continue
|
|
141
|
+
out.records.append(record)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def latest_per_unit(records: Iterable[dict]) -> dict[str, list[dict]]:
|
|
146
|
+
"""The newest capture per surface, grouped by unit.
|
|
147
|
+
|
|
148
|
+
**A unit contributes once to the denominator, however often it was
|
|
149
|
+
captured.** Otherwise a rack that reports hourly outvotes a rack that
|
|
150
|
+
reports weekly, and the baseline describes the collection schedule rather
|
|
151
|
+
than the fleet.
|
|
152
|
+
"""
|
|
153
|
+
newest: dict[tuple, dict] = {}
|
|
154
|
+
for record in records:
|
|
155
|
+
key = surface_of(record)
|
|
156
|
+
current = newest.get(key)
|
|
157
|
+
if current is None or record.get("captured_at", "") >= current.get(
|
|
158
|
+
"captured_at", ""):
|
|
159
|
+
newest[key] = record
|
|
160
|
+
out: dict[str, list[dict]] = {}
|
|
161
|
+
for record in newest.values():
|
|
162
|
+
out.setdefault(record["unit_key"], []).append(record)
|
|
163
|
+
return out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def derive(present_by_unit: dict[str, set[str]],
|
|
167
|
+
paths_by_unit: dict[str, dict[str, str]],
|
|
168
|
+
*, scope: dict, threshold: float = DEFAULT_THRESHOLD,
|
|
169
|
+
floor: int = DEFAULT_FLOOR,
|
|
170
|
+
window: tuple[str, str] | None = None) -> dict:
|
|
171
|
+
"""Build the `fleet-baseline/1`. Raises `BaselineError` below the floor."""
|
|
172
|
+
total = len(present_by_unit)
|
|
173
|
+
if total == 0:
|
|
174
|
+
raise BaselineError(
|
|
175
|
+
"the scope selected no units at all. A baseline over an empty "
|
|
176
|
+
"cohort is not a weak baseline, it is no measurement")
|
|
177
|
+
if total < floor:
|
|
178
|
+
raise BaselineError(
|
|
179
|
+
f"the scope selected {total} unit(s) and the floor is {floor}. A "
|
|
180
|
+
f"baseline of {total} is an anecdote wearing a format key: at that "
|
|
181
|
+
f"size one unlucky machine moves every ratio past the threshold. "
|
|
182
|
+
f"Raise the cohort, or lower the floor explicitly with --floor")
|
|
183
|
+
if not 0.0 < threshold <= 1.0:
|
|
184
|
+
raise BaselineError(
|
|
185
|
+
f"--present-threshold is {threshold}, outside (0, 1]")
|
|
186
|
+
|
|
187
|
+
counts: dict[str, int] = {}
|
|
188
|
+
for names in present_by_unit.values():
|
|
189
|
+
for name in names:
|
|
190
|
+
counts[name] = counts.get(name, 0) + 1
|
|
191
|
+
|
|
192
|
+
sensors = []
|
|
193
|
+
for name in sorted(counts):
|
|
194
|
+
ratio = counts[name] / total
|
|
195
|
+
if ratio < threshold:
|
|
196
|
+
continue
|
|
197
|
+
entry: dict = {"name": name, "present_ratio": round(ratio, 6)}
|
|
198
|
+
# **Only when the contributing units agree.** A baseline that asserts
|
|
199
|
+
# one URI while the cohort reports several is asserting something no
|
|
200
|
+
# unit said. Matching is by name regardless; this key is advisory, and
|
|
201
|
+
# advisory metadata that is wrong is worse than absent.
|
|
202
|
+
seen = {paths[name] for paths in paths_by_unit.values() if name in paths}
|
|
203
|
+
if len(seen) == 1:
|
|
204
|
+
entry["uri_suffix"] = seen.pop()
|
|
205
|
+
sensors.append(entry)
|
|
206
|
+
|
|
207
|
+
derived: dict = {"units": total, "present_threshold": threshold}
|
|
208
|
+
if window is not None:
|
|
209
|
+
derived["captured_between"] = list(window)
|
|
210
|
+
return {
|
|
211
|
+
"format": BASELINE_FORMAT,
|
|
212
|
+
"scope": scope,
|
|
213
|
+
"derived": derived,
|
|
214
|
+
"sensors": sensors,
|
|
215
|
+
"provenance": PROVENANCE_DERIVED,
|
|
216
|
+
"notice": DOWNGRADE_NOTICE,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def expected_names(baseline: dict) -> set[str]:
|
|
221
|
+
return {sensor["name"] for sensor in baseline.get("sensors", [])}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def derivation_line(baseline: dict) -> str:
|
|
225
|
+
"""The one line every consumer prints above a judgment made with this.
|
|
226
|
+
|
|
227
|
+
Denominator, window and threshold, because a ratio without its predicate is
|
|
228
|
+
a number somebody will quote.
|
|
229
|
+
"""
|
|
230
|
+
derived = baseline.get("derived", {})
|
|
231
|
+
window = derived.get("captured_between")
|
|
232
|
+
when = f", captured between {window[0]} and {window[1]}" if window else ""
|
|
233
|
+
return (f"derived from {derived.get('units')} unit(s) at a presence "
|
|
234
|
+
f"threshold of {derived.get('present_threshold')}{when}")
|