2026-09-14 11:45:34 +02:00
#!/usr/bin/env python3
""" CUST-WP-0071-T01: join live cluster allocation with owner/service/tenant. """
from __future__ import annotations
import argparse
import importlib . util
import json
import sys
from datetime import UTC , datetime
from pathlib import Path
from typing import Any
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
CLUSTER_OBS = Path . home ( ) / " railiance-cluster " / " tools " / " observe_cluster_resources.py "
HEADROOM = Path . home ( ) / " state-hub " / " scripts " / " release_headroom_preflight.py "
OWNERS = ROOT / " docs " / " evidence " / " namespace-ownership.yaml "
def load_module ( path : Path , name : str ) :
spec = importlib . util . spec_from_file_location ( name , path )
if spec is None or spec . loader is None :
raise RuntimeError ( f " cannot load { path } " )
module = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( module )
return module
def join ( observation : dict [ str , Any ] , owners : dict [ str , Any ] ) - > dict [ str , Any ] :
mapping = owners . get ( " namespaces " ) or { }
rows = [ ]
unknown = [ ]
zero_request = [ ]
for workload in observation . get ( " utilization " , { } ) . get ( " workloads " ) or [ ] :
ns = workload [ " namespace " ]
meta = mapping . get ( ns , { " owner " : " unknown " , " service " : " unknown " , " tenant " : " unknown " } )
row = {
" namespace " : ns ,
" workload " : workload [ " workload " ] ,
" pods " : workload [ " pods " ] ,
" cpu_request_m " : workload [ " requests " ] [ " cpu_millicores " ] ,
" memory_request_bytes " : workload [ " requests " ] [ " memory_bytes " ] ,
" owner " : meta . get ( " owner " , " unknown " ) ,
" service " : meta . get ( " service " , " unknown " ) ,
" tenant " : meta . get ( " tenant " , " unknown " ) ,
}
rows . append ( row )
if ns not in mapping :
unknown . append ( ns )
if workload [ " requests " ] [ " cpu_millicores " ] == 0 :
zero_request . append ( f " { ns } / { workload [ ' workload ' ] } " )
pending = observation . get ( " allocation " , { } ) . get ( " pending_unscheduled " ) or { }
alloc = observation . get ( " allocation " ) or { }
capacity = observation . get ( " capacity " , { } ) . get ( " totals " ) or { }
residual = ( alloc . get ( " residual_capacity " ) or { } ) . get ( " cpu_millicores " , 0 )
requested = ( alloc . get ( " denominators " ) or { } ) . get ( " cpu_requested_millicores " , 0 )
return {
" schema " : " custodian.allocation-reconcile.v1 " ,
" workplan_id " : " CUST-WP-0071-T01 " ,
" captured_at " : observation . get ( " captured_at " ) ,
" cluster_observation_schema " : observation . get ( " schema_version " ) ,
" count_host_and_cluster_cpus_once " : True ,
" host " : owners . get ( " host " ) ,
" capacity_cpu_m " : capacity . get ( " cpu_millicores " ) ,
" scheduled_request_cpu_m " : requested ,
" pending_unscheduled_cpu_m " : pending . get ( " cpu_millicores " , 0 ) ,
" residual_cpu_m " : residual ,
" not_a_scheduling_guarantee " : True ,
" workloads " : sorted ( rows , key = lambda r : ( - r [ " cpu_request_m " ] , r [ " namespace " ] , r [ " workload " ] ) ) ,
" unknown_namespaces " : sorted ( set ( unknown ) ) ,
" zero_request_workloads " : sorted ( zero_request ) ,
" pending_unscheduled " : pending . get ( " pods " ) or [ ] ,
" measurement_gaps " : observation . get ( " measurement_gaps " ) or [ ] ,
" state_hub_preflight_observation " : observation . get ( " state_hub_preflight_observation " ) ,
" signal_notes " : [
" Host resource:hosteurope:railiance01 and cluster resource:railiance:reef-railiance:k3s are the same 4 vCPU; do not add them. " ,
" Prometheus namespace_cpu:kube_pod_container_resource_requests:sum omitted Forgejo 100m on 2026-09-11; Kubernetes API is the scheduler-effective source. " ,
" node-exporter was disabled; host CPU usage is not a reservation and is not treated as spare capacity. " ,
" Zero-request pods are demand, not zero. Missing metrics are listed as gaps, not zeros. " ,
" STATE-WP-0091 preflight consumes state_hub_preflight_observation; residual millicores are not admission. " ,
] ,
}
def render_markdown ( report : dict [ str , Any ] , preflight : dict [ str , Any ] | None ) - > str :
lines = [
f " # Railiance allocation reconcile — { report [ ' captured_at ' ] } " ,
" " ,
" CUST-WP-0071-T01. Scheduler-effective requests from the Kubernetes API " ,
" via `railiance-cluster` observe (RCLUSTER-WP-0014 / RAIL-BS-WP-0014). " ,
" Host and cluster are the same 4 vCPU and are counted once. " ,
" " ,
f " - Capacity: { report [ ' capacity_cpu_m ' ] } m " ,
f " - Scheduled requests: { report [ ' scheduled_request_cpu_m ' ] } m " ,
f " - Pending unscheduled: { report [ ' pending_unscheduled_cpu_m ' ] } m " ,
f " - Residual (not a guarantee): { report [ ' residual_cpu_m ' ] } m " ,
f " - Unknown namespaces: { ' , ' . join ( report [ ' unknown_namespaces ' ] ) or ' none ' } " ,
f " - Zero-request workloads: { len ( report [ ' zero_request_workloads ' ] ) } " ,
" " ,
" | Namespace | Workload | Owner | Tenant | CPU request (m) | Pods | " ,
" |---|---|---|---|---:|---:| " ,
]
for row in report [ " workloads " ] :
lines . append (
f " | { row [ ' namespace ' ] } | { row [ ' workload ' ] } | { row [ ' owner ' ] } | { row [ ' tenant ' ] } | { row [ ' cpu_request_m ' ] } | { row [ ' pods ' ] } | "
)
lines + = [ " " , " ## STATE-WP-0091 preflight " , " " ]
if preflight :
lines . append ( f " - ok: ` { preflight . get ( ' ok ' ) } ` " )
lines . append ( f " - remaining_cpu_m: { preflight . get ( ' remaining_cpu_m ' ) } " )
for reason in preflight . get ( " reasons " ) or [ ] :
lines . append ( f " - refuse: { reason } " )
for note in preflight . get ( " notes " ) or [ ] :
lines . append ( f " - note: { note } " )
lines + = [ " " , " ## Signal notes " , " " ]
for note in report [ " signal_notes " ] :
lines . append ( f " - { note } " )
lines . append ( " " )
return " \n " . join ( lines )
def main ( ) - > int :
parser = argparse . ArgumentParser ( description = __doc__ )
parser . add_argument ( " --observation " , type = Path , help = " Existing cluster observation JSON " )
parser . add_argument ( " --kubectl " , default = " kubectl " )
parser . add_argument ( " --output-dir " , type = Path , default = ROOT / " docs " / " evidence " )
args = parser . parse_args ( )
cluster = load_module ( CLUSTER_OBS , " cluster_resources " )
if args . observation :
observation = json . loads ( args . observation . read_text ( ) )
else :
observation = cluster . collect ( args . kubectl )
2026-10-21 08:13:07 +02:00
import yaml # main only: join() stays stdlib-only for the in-cluster runner
2026-09-14 11:45:34 +02:00
owners = yaml . safe_load ( OWNERS . read_text ( ) )
report = join ( observation , owners )
preflight = None
if observation . get ( " state_hub_preflight_observation " ) :
headroom = load_module ( HEADROOM , " release_headroom " )
preflight = headroom . evaluate ( observation [ " state_hub_preflight_observation " ] )
report [ " state_hub_preflight " ] = preflight
stamp = datetime . now ( UTC ) . strftime ( " % Y- % m- %d " )
out_json = args . output_dir / f " { stamp } -allocation-reconcile.json "
out_md = args . output_dir / f " { stamp } -allocation-reconcile.md "
args . output_dir . mkdir ( parents = True , exist_ok = True )
out_json . write_text ( json . dumps ( report , indent = 2 ) + " \n " , encoding = " utf-8 " )
out_md . write_text ( render_markdown ( report , preflight ) , encoding = " utf-8 " )
print ( out_json )
print ( out_md )
return 0
if __name__ == " __main__ " :
raise SystemExit ( main ( ) )