net-kingdom/tools/bootstrap-recovery/inventory-scaleway.py
tegwick c262470089
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Inventory off-host recovery assets through existing bounded backup routes
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
2026-10-15 23:58:41 +02:00

104 lines
5 KiB
Python

#!/usr/bin/env python3
"""Read-only bounded S3 backup metadata through existing scoped runtime delivery.
No bootstrap API key, new grant, object content read, age key, provider write or
retention action. The caller must already have admitted cluster read authority.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
BUCKET='railiance-platform-pg-backup'
ENDPOINT='https://s3.nl-ams.scw.cloud'
PREFIX='platform-pg/'
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
class Refused(Exception):pass
def kube(args):
p=subprocess.run(['kubectl',*args,'-o','json'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=30)
if p.returncode:raise Refused()
return json.loads(p.stdout)
def list_prefix(client,prefix):
if not prefix.startswith(PREFIX) or not re.fullmatch(r'[A-Za-z0-9._/-]+',prefix) or '..' in prefix.split('/'):raise Refused()
directories=[];files=[];continuation=None
for _ in range(20):
args={'Bucket':BUCKET,'Prefix':prefix,'Delimiter':'/','MaxKeys':1000}
if continuation:args['ContinuationToken']=continuation
page=client.list_objects_v2(**args)
for row in page.get('CommonPrefixes',[]):
value=row['Prefix']
if not value.startswith(prefix) or not value.endswith('/') or not re.fullmatch(r'[A-Za-z0-9._/-]+',value) or '..' in value.split('/'):raise Refused()
directories.append(value)
for row in page.get('Contents',[]):
name=row['Key']
if not name.startswith(prefix) or not re.fullmatch(r'[A-Za-z0-9._/-]+',name) or '..' in name.split('/'):raise Refused()
if name.endswith(('.age','.json','.yaml')):
size=row['Size']
if type(size) is not int or size<0:raise Refused()
files.append({'key':name,'bytes':size})
if not page.get('IsTruncated'):break
continuation=page.get('NextContinuationToken')
if not continuation:raise Refused()
else:raise Refused()
return {'directories':sorted(set(directories)),'files':sorted(files,key=lambda x:x['key'])}
def inventory(client):
pending=[(PREFIX,0)];result={}
while pending:
prefix,depth=pending.pop(0)
if prefix in result:raise Refused()
if len(result)>=64:raise Refused()
row=list_prefix(client,prefix);result[prefix]=row
if depth<3:
for child in row['directories']:
# Native database WAL/base trees are not application/key-file
# archives. Record their presence without enumerating every WAL.
if any(x in child.split('/') for x in ['wals','base']):continue
pending.append((child,depth+1))
return result
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.scaleway-backup-inventory.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'read_only':True,
'provider':'Scaleway','bucket':BUCKET,'prefix':PREFIX,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'bootstrap_api_key_used':False,'age_keys_read':False,'object_contents_downloaded':False,
'secret_values_exported':False,'scope':'Bounded prefix metadata, at most three child levels; WAL/base trees not enumerated; other buckets/prefixes unassessed'}
try:
if kube(['get','namespace','kube-system'])['metadata']['uid']!=CLUSTER:raise Refused()
delivered=kube(['-n','databases','get','secret','platform-pg-backup-s3'])
if delivered['metadata']['namespace']!='databases' or delivered['metadata']['name']!='platform-pg-backup-s3':raise Refused()
values=delivered['data']
# Same governed runtime delivery used by Platform's archive verifier;
# explicit credentials avoid SDK ambient/profile/metadata discovery.
import boto3
from botocore.config import Config
client=boto3.client('s3',endpoint_url=ENDPOINT,region_name='nl-ams',
aws_access_key_id=base64.b64decode(values['ACCESS_KEY_ID'],validate=True).decode(),
aws_secret_access_key=base64.b64decode(values['ACCESS_SECRET_KEY'],validate=True).decode(),
config=Config(signature_version='s3v4',proxies={},connect_timeout=15,read_timeout=30,
retries={'max_attempts':1},s3={'addressing_style':'path'}))
r['prefixes']=inventory(client);r['status']='metadata_inventory_complete'
r['runtime_delivery_uid']=delivered['metadata']['uid']
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
return int(r['status']=='failed')
if __name__=='__main__':raise SystemExit(main())