mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
Four probes returned the number of failures as the process exit code. An exit status is 8 bits wide, so exactly 256 or 512 failures arrive as 0 and the probe reports success. sys-service installs these as Type=oneshot units (roles/sys-service/vars/main.yml:28) and sys-service-terminator starts them with until: result is not failed, so that exit code is the whole health verdict. The count is not lost: each of the four prints it to stdout a line earlier. Only the exit status stops carrying it, because it cannot. This makes no CI job green and is not meant to. For any count other than 256 or 512 nothing changes; at those two it turns a false success into an honest failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
102 lines
3.1 KiB
Python
Executable File
102 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Ultra-thin checker: consume a JSON mapping of {domain: [expected_status_codes]}
|
|
and verify HTTP HEAD responses. All mapping logic is done in the filter
|
|
`web_health_expectations`.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
|
|
def parse_args(argv=None):
|
|
p = argparse.ArgumentParser(
|
|
description="Web health checker (expects precomputed domain→codes mapping)."
|
|
)
|
|
p.add_argument(
|
|
"--web-protocol",
|
|
default="https",
|
|
choices=["http", "https"],
|
|
help="Protocol to use",
|
|
)
|
|
p.add_argument(
|
|
"--expectations", required=True, help='JSON STRING: {"domain": [codes], ...}'
|
|
)
|
|
return p.parse_args(argv)
|
|
|
|
|
|
def _protocol_for(domain: str, default_protocol: str) -> str:
|
|
"""Onion services are plaintext-only (no TLS): probe them over http
|
|
regardless of the clearnet default. Everything else uses the default."""
|
|
return "http" if domain.endswith(".onion") else default_protocol
|
|
|
|
|
|
def _parse_json_mapping(name: str, value: str) -> dict[str, list[int]]:
|
|
try:
|
|
obj = json.loads(value)
|
|
except json.JSONDecodeError as e:
|
|
raise SystemExit(f"--{name} must be a valid JSON string: {e}") from e
|
|
if not isinstance(obj, dict):
|
|
raise SystemExit(f"--{name} must be a JSON object (mapping)")
|
|
clean = {}
|
|
for k, v in obj.items():
|
|
if isinstance(v, list):
|
|
try:
|
|
clean[k] = [int(x) for x in v]
|
|
except Exception:
|
|
clean[k] = []
|
|
else:
|
|
clean[k] = []
|
|
return clean
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
args = parse_args(argv)
|
|
expectations = _parse_json_mapping("expectations", args.expectations)
|
|
verify = True
|
|
ca_trust_cert_host = os.environ.get("CA_TRUST_CERT_HOST", "").strip()
|
|
if ca_trust_cert_host:
|
|
if not Path(ca_trust_cert_host).is_file():
|
|
print(
|
|
f"CA_TRUST_CERT_HOST points to a missing certificate: {ca_trust_cert_host}"
|
|
)
|
|
return 1
|
|
verify = ca_trust_cert_host
|
|
|
|
errors = 0
|
|
for domain in sorted(expectations.keys()):
|
|
expected = expectations[domain] or []
|
|
is_onion = domain.endswith(".onion")
|
|
url = f"{_protocol_for(domain, args.web_protocol)}://{domain}"
|
|
timeout = 30 if is_onion else 10
|
|
try:
|
|
r = requests.head(
|
|
url, allow_redirects=False, timeout=timeout, verify=verify
|
|
)
|
|
if expected and r.status_code in expected:
|
|
print(f"{domain}: OK")
|
|
elif not expected:
|
|
print(
|
|
f"{domain}: ERROR: No expectations provided. Got {r.status_code}."
|
|
)
|
|
errors += 1
|
|
else:
|
|
print(f"{domain}: ERROR: Expected {expected}. Got {r.status_code}.")
|
|
errors += 1
|
|
except requests.RequestException as e:
|
|
print(f"{domain}: error due to {e}")
|
|
errors += 1
|
|
|
|
if errors:
|
|
print(f"Warning: {errors} domains responded with an unexpected status code.")
|
|
return 1 if errors else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|