#!/usr/bin/python3 """Actual normal HTTP/PTY, renewal and D09 owner acceptance in the private realm. No replacement service, fake APP_READY, surrogate terminal, payment or private owner wire injection. Lifecycle requests originate through the native FD10 RPC. """ import base64 import copy import hashlib import http.cookies import json import os from pathlib import Path import re import runpy import secrets import shutil import socket import struct import subprocess import time import urllib.request import urllib.error PACKAGE=Path('/opt/c01-fixture');EVIDENCE=Path('/var/lib/c01-normal') scope=runpy.run_path(str(PACKAGE/'tests/regression/c01-compose.py')) provision=scope['d09_module']();owner=provision.owner import d09_store as store import d09_wire as wire generation,policy=scope['generation'],scope['policy'] sha=scope['sha'];dump=scope['dump'];run=scope['run'] def ledger():return json.loads((owner.ROOT/'ledger.json').read_text()) def binding():return ledger().get('active_binding') or json.loads(owner.BINDING.read_text()) def wait(body,label,seconds=180): end=time.monotonic()+seconds while time.monotonic() /home/archipelago/Work/'+nonce+'; ' 'printf ok > /home/archipelago/.local/state/archipelago/sessions/'+nonce+'; ' 'if printf bad > /opt/archipelago/c01-writable-fixture; then printf BAD_WRITE; ' 'else printf DENIED_WRITE; fi; printf "\\n'+marker+'\\n"') if os.environ.get('C01_HISTORY_MIGRATION')=='1': command=(command.rsplit('printf "\\n'+marker+'\\n"',1)[0]+ '/usr/bin/podman inspect --format \'{"id":"{{.Id}}","state":{{json .State}}}\' fixture-stopped ' '> /home/archipelago/Work/'+nonce+'-container; ' '/usr/bin/podman container exists fixture-removed; ' 'printf "%s\\n" "$?" > /home/archipelago/Work/'+nonce+'-absent; ' 'printf "\\n'+marker+'\\n"') yaya_declarations=EVIDENCE/'yaya-container-declarations.json' if yaya_declarations.exists(): declarations=json.loads(yaya_declarations.read_text())['records'] assert len(declarations)==38 and all(re.fullmatch(r'yaya-declaration-[0-9]+',row['name']) for row in declarations) command=(command.rsplit('printf "\\n'+marker+'\\n"',1)[0]+ '/usr/bin/podman inspect --format \'{"id":"{{.Id}}","state":{{json .State}}}\' '+ ' '.join(row['name'] for row in declarations)+' > /home/archipelago/Work/'+nonce+'-yaya; '+ 'printf "\\n'+marker+'\\n"') try: send({'type':'input','data':command});send({'type':'input','data':'\r'}) def take(count): nonlocal buffer while len(buffer)=2 and all(epoch in successful_epochs for _,epoch in normal_renewals[-2:]): assert all(pid==app_pid for pid,_ in renewals) and len({e for _,e in renewals})>=2 break prior_renewals=renewals time.sleep(2.3) else: dump(EVIDENCE/'normal-renewals-refusal.json',{'successful_native_reads':successes, 'refusals':refusals,'renewals':renewals,'successful_read_epochs':sorted(successful_epochs)}) raise AssertionError('two post-readiness clock-only renewals with successful reads not observed') dump(EVIDENCE/'normal-renewals.json',{'application_pid':app_pid,'manager_invocation':initial['InvocationID'], 'renewals':normal_renewals,'baseline_renewals':baseline_renewals, 'successful_read_epochs':sorted(successful_epochs), 'successful_native_reads':successes,'transient_read_refusals':refusals}) def phase(name): receipt=ledger().get('receipt');return receipt if receipt and receipt['phase']==name else None def exchange(request): with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as channel: channel.settimeout(5);channel.connect(str(owner.SOCKET));channel.sendall(wire.canonical(request));channel.shutdown(socket.SHUT_WR) raw=b'' while True: part=channel.recv(8192) if not part:break raw+=part;assert len(raw)<=wire.MAX return wire.parse(raw) def publish(value): raw=wire.canonical(value);digest=wire.digest(raw) store.atomic(owner.ROOT/'observations'/(digest+'.json'),raw);return digest def observe(): receipt=wait(lambda: phase('AWAITING_OBSERVATION'),'native generation ready',240) assert owner.service()['ActiveState']=='active' and owner.service()['InvocationID']==receipt['observed_invocation_id'] http.login();proof=terminal(http) after={k:signature(p) for k,p in retained.items()};assert after==before base={'transaction_id':receipt['transaction_id'],'observed_invocation_id':receipt['observed_invocation_id']} if os.environ.get('C01_HISTORY_MIGRATION')=='1': extended=dict(base,preserved={k:{'before':before[k],'after':after[k]} for k in before}) proof['migration_preservation_digest']=publish(extended) dump(EVIDENCE/('history-preservation-'+receipt['observed_invocation_id']+'.json'),extended) namespace=publish(dict(base,qualification='actual native HTTP default terminal and WS PTY commands', default_terminal_created=True,default_terminal_workspace=proof['workspace'], namespace_writable=['/home/archipelago/Work','/home/archipelago/.local/state/archipelago/sessions'],native_proof=proof)) preservation=publish(dict(base,preserved={k:{'before':wire.digest(wire.canonical(before[k])), 'after':wire.digest(wire.canonical(after[k]))} for k in ('work','history','custom_fragments')})) observation={k:receipt[k] for k in ('transaction_id','target_binding_digest','composition_digest', 'observed_invocation_id','effective_config_observation_digest','config_current_digest')} observation.update(namespace_observation_digest=namespace,preservation_observation_digest=preservation) value=binding() request=dict(schema_version=1,operation='ACK_OBSERVATION',route='OBSERVER',reason='OBSERVATION_READY', transaction_id=value['transaction_id'],request_id=secrets.token_hex(16),target_binding_digest=value['target_binding_digest'], composition_digest=value['composition_digest'],grant_digest=ledger().get('notice_grant') or value['grants']['OBSERVER'],expected_sequence=ledger()['sequence'], requester_invocation_id=None,observer_receipt_digest=publish(observation),selected_release_digest=None,staging_generation=None) assert exchange(request)['status']=='ACCEPTED' return wait(lambda: phase('COMPLETE'),'independent native observation accepted') def action(method): try:http.rpc(method) except (OSError,TimeoutError):pass # The actual owner may retire this HTTP invocation before reply. unchanged=(owner.ROOT/'ledger.json').read_bytes() try:http.rpc('update.activate-admitted',{'path':'/unappointed'}) except RuntimeError:pass else:raise AssertionError('caller-selected activation argument accepted') assert (owner.ROOT/'ledger.json').read_bytes()==unchanged first_binding=copy.deepcopy(binding()) action('update.activate-admitted');b_receipt=observe() assert sha(owner.DESTINATIONS['backend'])==first_binding['artifacts']['backend'] wait(lambda: run('systemctl','show',ledger()['execution']['unit'],'-p','ActiveState','--value').stdout.strip()==b'inactive','B worker drain') # Retry through the new native B process, not a root-injected request. Its # retained request must resolve by lookup without another dispatch/grant. b_invocation=owner.service()['InvocationID'];b_history=(owner.ROOT/'ledger.json').read_bytes() http.rpc('update.activate-admitted') assert owner.service()['InvocationID']==b_invocation and (owner.ROOT/'ledger.json').read_bytes()==b_history # Root coordinator supplies a new, history-bound appointment. It cannot # reset the old ledger or manufacture an application/observer result. incoming=owner.ROOT/'incoming';archive=PACKAGE/'retained-incoming-B';archive.mkdir(mode=0o700) for path in incoming.iterdir():path.rename(archive/path.name) old_launch=json.loads(policy.LAUNCH.read_text()) for label in ('new','restore'): for name,path in owner.destinations(first_binding).items(): target=incoming/(label+'-'+name) if path.is_dir():shutil.copytree(path,target) elif path.exists():shutil.copy2(path,target) with (incoming/'new-backend').open('ab') as stream:stream.write(b'\nC01-disposable-generation-C\n') new_launch=dict(old_launch,native_sha256=sha(incoming/'new-backend')) dump(incoming/'new-launch',new_launch) dump(incoming/'exclusion.json',json.loads((policy.STATE/'journal.json').read_text())) effective=wire.digest(owner.effective_configuration().encode()) successor=copy.deepcopy(first_binding) successor.pop('requester_uid');successor.pop('requester_gid') successor.update(transaction_id=secrets.token_hex(16),admission_epoch_id=secrets.token_hex(16),composition_digest=secrets.token_hex(32), grants={k:secrets.token_hex(32) for k in first_binding['grants']}, artifacts={'backend':sha(incoming/'new-backend'),'frontend':owner.object_digest(incoming/'new-frontend')}, restore_artifacts=first_binding['artifacts'],effective_config={'APPLY':effective,'RESTORE':effective}, exclusion_evidence_digest=sha(incoming/'exclusion.json'), prerequisite={'grant_digest':secrets.token_hex(32),'backend_sha256':sha(owner.DESTINATIONS['backend']),'effective_config_digest':effective}) successor['release_identity']=['fixture-C',[['archipelago','fixture-C',successor['artifacts']['backend'],(incoming/'new-backend').stat().st_size]]] successor['objects']={name:{'old':owner.object_digest(path),'new':owner.object_digest(incoming/('new-'+name)), 'restore':owner.object_digest(incoming/('restore-'+name))} for name,path in owner.destinations(successor).items()} successor['c01']['generations']={name:{'native_sha256':launch['native_sha256'],'launch_sha256':wire.digest(generation.canonical(launch))} for name,launch in [('old',old_launch),('new',new_launch),('restore',old_launch)]} for operation,a,b in [('APPLY',old_launch,new_launch),('RESTORE',new_launch,old_launch)]: record={'version':1,'policy_sha256':sha(policy.CONFIG),'runtime_sha256':sha(policy.RUNTIME), 'transaction_id':successor['transaction_id'],'action':operation,'grant_digest':secrets.token_hex(32), 'from_launch':a,'to_launch':b,'from_launch_sha256':wire.digest(generation.canonical(a)), 'to_launch_sha256':wire.digest(generation.canonical(b)), 'from_workspace_digest':successor['objects']['config']['old' if operation=='APPLY' else 'new'], 'to_workspace_digest':successor['objects']['config']['new' if operation=='APPLY' else 'restore']} raw=generation.canonical(record);record['external_history_digest']=wire.digest(raw) store.atomic(incoming/('c01-history-'+wire.digest(raw)+'.json'),raw) raw=generation.canonical(record);digest=wire.digest(raw) store.atomic(incoming/('c01-generation-'+digest+'.json'),raw);successor['c01']['transitions'][operation]=digest dump(incoming/'admission.json',successor) protected=dict(successor,requester_uid=first_binding['requester_uid'],requester_gid=first_binding['requester_gid']) fields=dict(history_fields(),successor_binding_digest=wire.digest(wire.canonical(protected)),terminal_disposition='APPLY') record,history=external_record(fields) original_journal=list(ledger()['journal']);provision.successor(incoming/'admission.json',record,history) assert ledger()['journal']==original_journal unchanged=(owner.ROOT/'ledger.json').read_bytes();provision.successor(incoming/'admission.json',record,history) assert unchanged==(owner.ROOT/'ledger.json').read_bytes() action('update.activate-admitted');c_receipt=observe() assert sha(owner.DESTINATIONS['backend'])==successor['artifacts']['backend'] wait(lambda: run('systemctl','show',ledger()['execution']['unit'],'-p','ActiveState','--value').stdout.strip()==b'inactive','C worker drain') action('update.recover-admitted');restored=observe() assert restored['config_action']=='RESTORE' and sha(owner.DESTINATIONS['backend'])==first_binding['artifacts']['backend'] wait(lambda: run('systemctl','show',ledger()['execution']['unit'],'-p','ActiveState','--value').stdout.strip()==b'inactive','restore worker drain') previous_invocation=owner.service()['InvocationID'];saved=(owner.ROOT/'ledger.json').read_bytes() run('systemctl','restart','archipelago-d09-lifecycle.service') wait(lambda: owner.service()['MainPID']=='0' and owner.service()['ActiveState'] in ('inactive','failed'),'owner-loss retirement') assert (owner.ROOT/'ledger.json').read_bytes()==saved,'owner restart changed retained history' probe=owner.Owner.__new__(owner.Owner) drain_waits=[] def drained(): try:return probe.drained_disposition(ledger()) except ValueError as error: reason=str(error) if reason not in ('management process/job not drained','prerequisite process still exists', 'worker descendants not retired','worker process/job not drained','worker process still exists'): raise if reason not in drain_waits:drain_waits.append(reason) return None disposition=wait(drained,'owned worker drain proof') fields=dict(history_fields(),allowed_action='REACTIVATE_RETAINED',new_epoch_id=secrets.token_hex(16), new_grant_digest=secrets.token_hex(32),execution_grant_digest=secrets.token_hex(32),disposition_digest=disposition) record,history=external_record(fields);provision.readmit(record,history,activation=True) recovered=observe();fresh_invocation=owner.service()['InvocationID'] assert fresh_invocation!=previous_invocation provision.readmit(record,history,activation=True) assert owner.service()['InvocationID']==fresh_invocation,'readmission replay dispatched twice' dump(EVIDENCE/'normal-generations.json',{'generations':['A','B','C','B'],'B':b_receipt,'C':c_receipt,'restored':restored, 'owner_recovery':recovered,'recovery_invocation':fresh_invocation,'drain_waits':drain_waits, 'preservation':before,'journal_entries':len(ledger()['journal']),'consumed_grants':len(ledger()['grants'])}) if os.environ.get('C01_RUNNING_SERVICES')=='1': import c01_admission launch=json.loads(policy.LAUNCH.read_text()) wait(lambda: run('systemctl','show',ledger()['execution']['unit'],'-p','ActiveState','--value').stdout.strip()==b'inactive', 'final recovery worker drain') observations=[] # Actual producer loss must retire the application, not retarget a # restarted daemon behind its already-issued capability. run('systemctl','stop','archipelago-fips.service') wait(lambda: owner.service()['MainPID']=='0' and owner.service()['ActiveState'] in ('failed','inactive'), 'required FIPS producer-loss manager retirement') for name,unit in c01_admission.UNITS.items(): if name!='fips':run('systemctl','stop',unit) try: contract=c01_admission.Contract(launch) except (ValueError,OSError) as error: observations.append({'service':name,'unit':unit,'admission_refused':True,'reason':str(error)[:512]}) else: contract.close();raise AssertionError('running service profile accepted stopped '+name) run('systemctl','start',unit) assert owner.service()['MainPID']=='0','service restoration silently reactivated manager' assert {k:signature(p) for k,p in retained.items()}==before dump(EVIDENCE/'running-service-loss.json',{'producer_loss_retired_manager':True, 'automatic_manager_reactivation':False,'refusals':observations,'preservation_unchanged':True}) return 0 if __name__=='__main__':raise SystemExit(main())