51 lines
3.9 KiB
Python
51 lines
3.9 KiB
Python
"""Offline parse public resource streams; never concatenate JSON as one object."""
|
|
import json,re
|
|
from decimal import Decimal
|
|
|
|
def objects(value):
|
|
if value is None:return []
|
|
if type(value) is dict:return [value]
|
|
if type(value) is not str:raise ValueError('resource stream type')
|
|
out=[];decoder=json.JSONDecoder();offset=0
|
|
while offset<len(value):
|
|
while offset<len(value) and value[offset].isspace():offset+=1
|
|
if offset==len(value):break
|
|
item,length=decoder.raw_decode(value,offset)
|
|
if type(item) is not dict:raise ValueError('resource object required')
|
|
out.append(item);offset=length
|
|
return out
|
|
|
|
def quantity(value,kind):
|
|
if value is None:return Decimal(0)
|
|
match=re.fullmatch(r'([0-9]+(?:\.[0-9]+)?)([A-Za-z]*)',str(value))
|
|
if not match:raise ValueError('unsupported quantity')
|
|
number,suffix=match.groups();number=Decimal(number)
|
|
factors={'':1,'n':Decimal('0.000000001'),'u':Decimal('0.000001'),'m':Decimal('0.001'),'k':1000,'M':1000000,'G':1000000000,'Ki':1024,'Mi':1024**2,'Gi':1024**3,'Ti':1024**4}
|
|
if suffix not in factors:raise ValueError('unsupported quantity suffix')
|
|
return number*factors[suffix]*(1000 if kind=='cpu' else 1)
|
|
|
|
def analyze(receipt,supplement=None):
|
|
rows=(supplement['pods']['rows'] if supplement else receipt['observations']['resource_requests']['rows'])
|
|
nodes=(supplement['nodes']['rows'] if supplement else receipt['observations']['nodes']['rows'])
|
|
node=next(n for n in nodes if any(a.get('address')=='92.205.62.239' for a in n['status.addresses']))
|
|
totals={r:Decimal(0) for r in ['cpu','memory']};count=0;multi=0;init_count=0;pod_level_count=0;sidecars=0
|
|
for row in rows:
|
|
if row['status.phase'] in ['Succeeded','Failed'] or row['spec.nodeName']!=node['metadata.name']:continue
|
|
count+=1;containers=objects(row['spec.containers[*].resources']);init=objects(row['spec.initContainers[*].resources']);multi+=len(containers)>1;init_count+=bool(init)
|
|
if row.get('spec.resources'):pod_level_count+=1
|
|
restartable=bool(row.get('spec.initContainers[*].restartPolicy'));sidecars+=restartable
|
|
for resource in totals:
|
|
app=sum((quantity(x.get('requests',{}).get(resource),resource) for x in containers),Decimal(0))
|
|
init_requests=[quantity(x.get('requests',{}).get(resource),resource) for x in init]
|
|
# Sum of app+all init is a conservative bound for restartable init.
|
|
effective=app+sum(init_requests,Decimal(0)) if restartable else max([app]+init_requests)
|
|
if row.get('spec.resources'):effective=max(effective,quantity(row['spec.resources'].get('requests',{}).get(resource),resource))
|
|
effective+=quantity((row.get('spec.overhead') or {}).get(resource),resource)
|
|
totals[resource]+=effective
|
|
capacity={r:quantity(node['status.allocatable'][r],r) for r in totals}
|
|
return {'node':node['metadata.name'],'nonterminal_bound_pods':count,'multicontainer_pods':multi,'pods_with_init_resources':init_count,'pods_with_pod_level_resources':pod_level_count,'pods_with_restartable_init_fields':sidecars,'supplement_used':bool(supplement),'requested_cpu_m':str(totals['cpu']),'allocatable_cpu_m':str(capacity['cpu']),'unrequested_cpu_m':str(capacity['cpu']-totals['cpu']),'requested_memory_bytes':str(totals['memory']),'allocatable_memory_bytes':str(capacity['memory']),'unrequested_memory_bytes':str(capacity['memory']-totals['memory']),'assumptions':[] if supplement else ['Init restartPolicy and pod-level resources not collected; scheduler totals provisional.'],'method':'exclude terminal/unbound/other-node Pods; sum regular requests; per-resource maximum init request; overhead; conservative sum if restartable init present; max with pod-level requests'}
|
|
if __name__=='__main__':
|
|
import sys
|
|
from pathlib import Path
|
|
p=Path(sys.argv[1]);s=Path(sys.argv[2]) if len(sys.argv)>2 else None
|
|
print(json.dumps(analyze(json.loads(p.read_text()),json.loads(s.read_text()) if s else None),indent=2))
|