railiance-infra/scripts/observe-host-capacity.py
codex 0a97ce12be
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled
Publish railiance01 resource evidence and close RAIL-HO-WP-0008
Non-secret identity, a reproducible capacity observation, and host-ops
labor/exit inputs for resource:hosteurope:railiance01. Booked price and
contract dates stay unknown for their owners.
2026-08-15 19:04:40 +02:00

216 lines
7.6 KiB
Python
Executable file

#!/usr/bin/env python3
"""Collect a non-secret host capacity observation over SSH.
Does not dump cloud-init metadata wholesale (that blob can contain
admin_pass). Only named keys are read.
Usage:
scripts/observe-host-capacity.py Railiance01
scripts/observe-host-capacity.py Railiance01 > docs/evidence/resource-hosteurope-railiance01/observations/....json
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
REMOTE = r"""
set -euo pipefail
python3 - <<'PY'
import json, os, subprocess, shutil
def sh(cmd):
return subprocess.check_output(cmd, shell=True, text=True).strip()
def cloud(key):
if not shutil.which("cloud-init"):
return None
try:
out = subprocess.check_output(
["cloud-init", "query", key], text=True, stderr=subprocess.DEVNULL
).strip()
except subprocess.CalledProcessError:
return None
if not out or out.lower() in {"none", "null"}:
return None
return out
mem = open("/proc/meminfo").read().split()
kv = {}
for i, tok in enumerate(mem):
if tok.endswith(":") and i + 1 < len(mem):
try:
kv[tok[:-1]] = int(mem[i + 1])
except ValueError:
pass
st = os.statvfs("/")
k3s = {}
if shutil.which("k3s"):
raw = subprocess.check_output(
["sudo", "k3s", "kubectl", "get", "node", "-o", "json"], text=True
)
node = json.loads(raw)["items"][0]
cap = node["status"]["capacity"]
alloc = node["status"]["allocatable"]
k3s = {
"node_name": node["metadata"]["name"],
"capacity": {
"cpu": cap.get("cpu"),
"memory": cap.get("memory"),
"ephemeral_storage": cap.get("ephemeral-storage"),
"pods": cap.get("pods"),
},
"allocatable": {
"cpu": alloc.get("cpu"),
"memory": alloc.get("memory"),
"ephemeral_storage": alloc.get("ephemeral-storage"),
"pods": alloc.get("pods"),
},
}
try:
top = subprocess.check_output(
["sudo", "k3s", "kubectl", "top", "node", "--no-headers"],
text=True,
stderr=subprocess.DEVNULL,
).split()
# NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
if len(top) >= 5:
k3s["observed"] = {
"cpu": top[1],
"cpu_pct": top[2],
"memory": top[3],
"memory_pct": top[4],
}
except subprocess.CalledProcessError:
k3s["observed"] = None
print(json.dumps({
"hostname": sh("hostname -f"),
"os_pretty": sh("grep ^PRETTY_NAME= /etc/os-release").split("=", 1)[1].strip('"'),
"nproc": int(sh("nproc")),
"cpu_model": sh("lscpu | awk -F: '/Model name/ {print $2; exit}'").strip(),
"hypervisor": sh("lscpu | awk -F: '/Hypervisor vendor/ {print $2; exit}'").strip() or None,
"mem_total_kib": kv.get("MemTotal"),
"mem_available_kib": kv.get("MemAvailable"),
"swap_total_kib": kv.get("SwapTotal"),
"root_total_bytes": st.f_frsize * st.f_blocks,
"root_used_bytes": st.f_frsize * (st.f_blocks - st.f_bfree),
"root_avail_bytes": st.f_frsize * st.f_bavail,
"block_devices": sh("lsblk -dn -o NAME,SIZE,TYPE,MODEL"),
"loadavg": open("/proc/loadavg").read().split()[:3],
"uptime_seconds": float(open("/proc/uptime").read().split()[0]),
"instance_type": cloud("ds.ec2_metadata.instance-type"),
"instance_id_ec2": cloud("ds.ec2_metadata.instance-id"),
"instance_uuid": cloud("ds.meta_data.uuid"),
"instance_name": cloud("ds.meta_data.name"),
"product_family": cloud("ds.meta_data.meta.app"),
"product_role": cloud("ds.meta_data.meta.role"),
"local_hostname": cloud("ds.ec2_metadata.local-hostname"),
"project_id": cloud("ds.meta_data.project_id"),
"k3s": k3s or None,
}))
PY
"""
def inventory_ssh(name: str) -> tuple[str, str]:
import yaml
path = os.path.join(os.path.dirname(__file__), "..", "inventory", "servers.yaml")
with open(path) as f:
servers = yaml.safe_load(f).get("servers") or []
for s in servers:
if s.get("name") == name:
return s["ip"], s.get("ssh_user", "tegwick")
raise SystemExit(f"unknown inventory host: {name}")
def main() -> int:
if len(sys.argv) != 2:
print("Usage: observe-host-capacity.py <InventoryName>", file=sys.stderr)
return 2
name = sys.argv[1]
ip, user = inventory_ssh(name)
observed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
raw = subprocess.check_output(
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", f"{user}@{ip}", REMOTE],
text=True,
)
host = json.loads(raw)
rec = {
"schema_version": "1.0",
"record_type": "host_capacity_observation",
"resource_id": "resource:hosteurope:railiance01" if name == "Railiance01" else f"resource:host:{name.lower()}",
"inventory_name": name,
"observed_at": observed_at,
"observer": "railiance-infra/scripts/observe-host-capacity.py",
"source_commands": [
"nproc / lscpu /proc/meminfo /proc/loadavg statvfs(/)",
"cloud-init query <named keys only>",
"sudo k3s kubectl get node -o json",
"sudo k3s kubectl top node",
],
"layers": {
"provider_declared": {
"instance_type": host.get("instance_type"),
"instance_uuid": host.get("instance_uuid"),
"instance_id_ec2": host.get("instance_id_ec2"),
"instance_name": host.get("instance_name"),
"product_family": host.get("product_family"),
"product_role": host.get("product_role"),
"local_hostname": host.get("local_hostname"),
"project_id": host.get("project_id"),
"note": "Declared by the provider metadata service. Not a booked SKU confirmation.",
},
"operating_system": {
"hostname": host["hostname"],
"os": host["os_pretty"],
"cpu": {
"value": host["nproc"],
"unit": "vCPU",
"kind": "usable",
"model": host.get("cpu_model"),
"hypervisor": host.get("hypervisor"),
},
"memory": {
"total_kib": host["mem_total_kib"],
"available_kib": host["mem_available_kib"],
"unit": "KiB",
"kind": "usable",
},
"swap": {
"total_kib": host["swap_total_kib"],
"unit": "KiB",
"kind": "observed",
"note": "OS swapfile, not a provider disk grant",
},
"root_filesystem": {
"total_bytes": host["root_total_bytes"],
"used_bytes": host["root_used_bytes"],
"avail_bytes": host["root_avail_bytes"],
"kind": "provisioned",
},
"block_devices": host["block_devices"],
"loadavg": host["loadavg"],
"uptime_seconds": host["uptime_seconds"],
},
"kubernetes": host.get("k3s"),
},
"unknown": [
{
"field": "traffic_allowance",
"classification": "unknown",
"owner": "operator",
"source": "Host Europe control panel or contract",
}
],
}
json.dump(rec, sys.stdout, indent=2)
sys.stdout.write("\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())