350 lines
20 KiB
Python
350 lines
20 KiB
Python
#!/usr/bin/python3
|
|
"""QEMU-only bootstrap -> real D09 owner/worker -> actual OS transaction.
|
|
|
|
Manager ELF is a synthetic Python requester, NOT accepted deployed Archipelago.
|
|
OS metadata/package effects are real; release verifier is the VM-only exact-pin
|
|
adapter from the installed qualification fixture, never production signing.
|
|
"""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import pwd
|
|
import secrets
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
assert 'QEMU' in Path('/sys/class/dmi/id/sys_vendor').read_text()
|
|
assert subprocess.check_output(['findmnt', '-n', '-o', 'SOURCE', '/'], text=True).strip() == '/dev/vda'
|
|
PACKAGE = Path('/opt/ota-source')
|
|
NATIVE = (PACKAGE/'accepted-old-archipelago').exists()
|
|
sys.path.insert(0, str(PACKAGE / 'scripts'))
|
|
import os_updater_bootstrap as bootstrap
|
|
import os_updater_activate as activation
|
|
import os_update_executor as executor
|
|
import d09_wire as wire
|
|
|
|
|
|
def run(*args, check=True):
|
|
r = subprocess.run(args, capture_output=True, text=True, timeout=180)
|
|
print('COMMAND', args, r.returncode, r.stdout, r.stderr, flush=True)
|
|
if check:
|
|
r.check_returncode()
|
|
return r
|
|
|
|
|
|
def write(path, content, mode=0o600):
|
|
path = Path(path)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(content.encode() if isinstance(content, str) else content)
|
|
path.chmod(mode)
|
|
|
|
|
|
def sha(path):
|
|
return bootstrap.digest(Path(path).read_bytes())
|
|
|
|
|
|
def wait(predicate, label, timeout=900):
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
value = predicate()
|
|
if value:
|
|
return value
|
|
time.sleep(.2)
|
|
raise TimeoutError(label)
|
|
|
|
|
|
def native_health():
|
|
state = owner.service()
|
|
if state['MainPID']=='0':
|
|
return None
|
|
r = subprocess.run(['nsenter','--target',state['MainPID'],'--net','--',
|
|
'curl','--max-time','2','--fail','--silent','http://127.0.0.1:19432/health'],capture_output=True,text=True)
|
|
return r.stdout if r.returncode==0 else None
|
|
|
|
|
|
# Reuse the installed fixture's actual Debian planner, NOT its direct stage
|
|
# shortcut. Keep generated inputs outside protected plan storage until bootstrap.
|
|
planner = (PACKAGE / 'tests/regression/os-ota-installed-stage.py').read_text()
|
|
marker = " status = executor.stage(binding, 'APPLY', 'prepare')"
|
|
assert planner.count(marker) == 1
|
|
prefix = planner.split(marker)[0]
|
|
old = "source = executor.ROOT/'plans'/plan_sha"
|
|
assert prefix.count(old) == 1
|
|
prefix = prefix.replace(old, "source = Path('/root/bootstrap-generated-plan')")
|
|
context = {'__name__': 'bootstrap_planner', '__file__': str(PACKAGE / 'tests/regression/os-ota-installed-stage.py')}
|
|
exec(compile(prefix + '\nexcept BaseException:\n raise\n', context['__file__'], 'exec'), context)
|
|
print('BOOTSTRAP_PLANNER_COMPLETE', flush=True)
|
|
spec = importlib.util.spec_from_file_location('provision', PACKAGE / 'scripts/provision-d09-lifecycle.py')
|
|
provision = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(provision)
|
|
owner = provision.owner
|
|
activation.untouched_authority()
|
|
run('systemctl', 'stop', 'archipelago.service', check=False)
|
|
try:
|
|
account = pwd.getpwnam('archipelago')
|
|
except KeyError:
|
|
# Minimal Debian rootfs has no Archipelago principal. Establish this fixture
|
|
# installation before taking preservation observations or invoking bootstrap.
|
|
run('useradd','--uid','1000','--user-group','--create-home','--shell','/bin/bash','archipelago')
|
|
account = pwd.getpwnam('archipelago')
|
|
# A separate pre-existing fixture admin represents the independently verified
|
|
# access channel. Creation is fixture setup, never done by production bootstrap.
|
|
run('useradd', '--no-create-home', '--shell', '/bin/bash', 'ota-fixture-admin')
|
|
admin = pwd.getpwnam('ota-fixture-admin')
|
|
if executor.ROOT == Path('/var/lib/archipelago-os-update'):
|
|
# Match the recorded deployed data-root ownership without weakening custody.
|
|
os.chown('/var/lib/archipelago',account.pw_uid,account.pw_gid)
|
|
control = Path('/run/d09-fixture-client')
|
|
control.mkdir(mode=0o700)
|
|
os.chown(control, account.pw_uid, account.pw_gid)
|
|
cache = Path('/var/lib/archipelago/d09-requests')
|
|
cache.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
os.chown(cache, account.pw_uid, account.pw_gid)
|
|
requester = '''import json,pathlib,subprocess,time
|
|
p=pathlib.Path('/run/d09-fixture-client')
|
|
while True:
|
|
for request in p.glob('request-*.json'):
|
|
output=request.with_name(request.name.replace('request-','result-'))
|
|
if output.exists():continue
|
|
r=subprocess.run(['/usr/libexec/archipelago-d09ctl',*json.loads(request.read_text())],capture_output=True,text=True,timeout=60)
|
|
output.write_text(json.dumps({'exit':r.returncode,'stdout':r.stdout,'stderr':r.stderr}))
|
|
time.sleep(.1)
|
|
'''
|
|
write('/root/fixture-access.json', json.dumps({'fixture': True, 'independent_uid': admin.pw_uid,
|
|
'machine_id_sha256': sha('/etc/machine-id'), 'existing_access_checked': True}))
|
|
write(PACKAGE / 'requester.py', requester, 0o644)
|
|
python = Path(sys.executable).resolve().read_bytes()
|
|
old_backend = (PACKAGE/'accepted-old-archipelago').read_bytes() if NATIVE else python
|
|
new_backend = Path('/opt/os-ota-native').read_bytes() if NATIVE else python+b'\nfixture-v2\n'
|
|
if NATIVE:
|
|
assert wire.digest(old_backend)=='2cf004fcc7f207c65b3bbd1eac248f8c9556ec1cd0ec139011fb4b1e1ef96494'
|
|
proof = json.loads(Path('/opt/os-ota-native-proof.json').read_text())
|
|
assert wire.digest(new_backend)==proof['packaged_sha256']
|
|
write(owner.DESTINATIONS['backend'], old_backend, 0o755)
|
|
web = owner.DESTINATIONS['frontend']
|
|
write(web / 'index.html', 'old frontend\n', 0o644)
|
|
template = Path('archipelago-runtime/image-recipe/configs/nginx-archipelago.conf')
|
|
write(web / template, (PACKAGE / 'image-recipe/configs/nginx-archipelago.conf').read_bytes(), 0o644)
|
|
unit = '''[Unit]
|
|
Description=Disposable old/new manager requester
|
|
[Service]
|
|
Type=exec
|
|
User=archipelago
|
|
ExecStart=/usr/local/bin/archipelago /opt/ota-source/requester.py
|
|
NoNewPrivileges=yes
|
|
ProtectSystem=strict
|
|
ProtectHome=read-only
|
|
ReadWritePaths=/run/d09-fixture-client /var/lib/archipelago/d09-requests
|
|
Restart=no
|
|
'''
|
|
if NATIVE:
|
|
manager_data = Path('/var/lib/os-bootstrap-native-manager')
|
|
manager_data.mkdir(mode=0o700)
|
|
os.chown(manager_data,account.pw_uid,account.pw_gid)
|
|
write(PACKAGE/'requester-launch.py', '''import subprocess
|
|
subprocess.Popen(['/usr/bin/python3','-B','/opt/ota-source/requester.py'],stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True)
|
|
''', 0o644)
|
|
unit = unit.replace('ExecStart=/usr/local/bin/archipelago /opt/ota-source/requester.py',
|
|
'ExecStart=/usr/local/bin/archipelago\nExecStartPost=/usr/bin/python3 -B /opt/ota-source/requester-launch.py\n'
|
|
'PrivateNetwork=yes\nEnvironment=ARCHIPELAGO_DATA_DIR=/var/lib/os-bootstrap-native-manager\n'
|
|
'Environment=ARCHIPELAGO_RUNTIME_DATA_DIR=/var/lib/os-bootstrap-native-manager\n'
|
|
'Environment=ARCHIPELAGO_BIND=127.0.0.1:19432\n'
|
|
'Environment=ARCHIPELAGO_NOSTR_DISCOVERY_ENABLED=false\nEnvironment=ARCHIPELAGO_NOSTR_RELAYS=\n'
|
|
'Environment=ARCHIPELAGO_SWARM_ENABLED=false\nReadWritePaths=/var/lib/os-bootstrap-native-manager')
|
|
write('/etc/systemd/system/archipelago.service', unit, 0o644)
|
|
native_identity = {}
|
|
if NATIVE:
|
|
# Run the real accepted installation before any bootstrap/D09 owner exists.
|
|
# Drain it externally only after health and nonempty identity are observed.
|
|
run('systemctl','daemon-reload')
|
|
run('systemctl','start','archipelago.service')
|
|
health = wait(native_health,'pre-bootstrap accepted native health',timeout=90)
|
|
state = owner.service()
|
|
assert sha('/proc/'+state['MainPID']+'/exe')==wire.digest(old_backend)
|
|
native_identity = {str(p):sha(p) for p in manager_data.joinpath('identity').iterdir() if p.is_file()}
|
|
assert native_identity
|
|
write('/root/accepted-old-running.json',wire.canonical(dict(backend_sha256=wire.digest(old_backend),
|
|
health=health,identity=native_identity,invocation_id=state['InvocationID'])))
|
|
print('PRE_BOOTSTRAP_ACCEPTED_NATIVE_RUNNING',health,flush=True)
|
|
run('systemctl','stop','archipelago.service')
|
|
write('/etc/systemd/system/archipelago.service.d/50-d09-lifecycle.conf',
|
|
(PACKAGE / 'image-recipe/configs/50-d09-lifecycle.conf').read_bytes(), 0o644)
|
|
run('systemctl', 'daemon-reload')
|
|
incoming = owner.ROOT / 'incoming'
|
|
incoming.mkdir(parents=True, mode=0o700)
|
|
exclusion = wire.canonical({'fixture': True, 'independent_root': True})
|
|
write(incoming / 'exclusion.json', exclusion)
|
|
for generation in ('new', 'restore'):
|
|
shutil.copytree(web, incoming / (generation + '-frontend'))
|
|
write(incoming / (generation + '-backend'), new_backend if generation=='new' else old_backend, 0o755)
|
|
new_config = (PACKAGE / 'image-recipe/configs/60-terminal-workspace.conf').read_bytes()
|
|
write(incoming / 'new-config', new_config)
|
|
restore_digest = wire.digest(owner.effective_configuration().encode())
|
|
write(owner.DESTINATIONS['config'], new_config, 0o644)
|
|
run('systemctl', 'daemon-reload')
|
|
apply_digest = wire.digest(owner.effective_configuration().encode())
|
|
owner.DESTINATIONS['config'].unlink()
|
|
run('systemctl', 'daemon-reload')
|
|
binding = dict(schema_version=1, target_binding_digest=secrets.token_hex(32), composition_digest=secrets.token_hex(32),
|
|
admission_epoch_id=secrets.token_hex(16), transaction_id=secrets.token_hex(16),
|
|
grants={r: secrets.token_hex(32) for r in ('UPDATER','AUTOMATIC','MANUAL','GUARD','OBSERVER')},
|
|
objects={name: dict(old=owner.object_digest(dest), new=owner.object_digest(incoming/('new-'+name)),
|
|
restore=owner.object_digest(incoming/('restore-'+name))) for name,dest in owner.DESTINATIONS.items()},
|
|
artifacts={'backend':sha(incoming/'new-backend'),'frontend':owner.object_digest(incoming/'new-frontend')},
|
|
restore_artifacts={'backend':sha(owner.DESTINATIONS['backend']),'frontend':owner.object_digest(web)},
|
|
effective_config={'APPLY':apply_digest,'RESTORE':restore_digest}, exclusion_evidence_digest=wire.digest(exclusion),
|
|
helper_digest=sha(PACKAGE/'image-recipe/configs/prepare-terminal-workspace.sh'),
|
|
prerequisite={'grant_digest':secrets.token_hex(32),'backend_sha256':sha(owner.DESTINATIONS['backend']),
|
|
'effective_config_digest':restore_digest}, os_update=context['binding']['os_update'])
|
|
binding['release_identity'] = ['2.0.1-alpha', [['archipelago','2.0.1-alpha',binding['artifacts']['backend'],(incoming/'new-backend').stat().st_size],
|
|
['os-update-plan.json','2.0.1-alpha',binding['os_update']['plan_sha256'],(context['source']/'plan.json').stat().st_size]]]
|
|
manifest = dict(version='2.0.1-alpha', components=[dict(zip(('name','new_version','sha256','size_bytes'), row))
|
|
for row in binding['release_identity'][1]],
|
|
os_update=dict(version=1,plan_sha256=binding['os_update']['plan_sha256']),
|
|
qualification='ROOT-APPOINTED-EXACT-FIXTURE-NOT-PRODUCTION-SIGNATURE')
|
|
write(context['source']/'manifest.json', wire.canonical(manifest))
|
|
binding['os_update']['manifest_sha256'] = sha(context['source']/'manifest.json')
|
|
# A tiny native exact-pin adapter in the disposable guest, with QEMU refusal.
|
|
# No signing key, production verifier substitution, or production pass is implied.
|
|
native_verifier = r'''#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
int main(int argc,char **argv) {
|
|
char vendor[256]={0}, hash[256]={0};
|
|
FILE *f=fopen("/sys/class/dmi/id/sys_vendor","r");
|
|
if(!f || !fgets(vendor,sizeof vendor,f) || !strstr(vendor,"QEMU")) return 71;
|
|
fclose(f);
|
|
if(argc!=4 || strcmp(argv[1],"ceremony") || strcmp(argv[2],"verify")) return 72;
|
|
int p[2], status; if(pipe(p)) return 73;
|
|
pid_t pid=fork(); if(pid<0) return 74;
|
|
if(!pid) { close(p[0]);dup2(p[1],1);close(p[1]);
|
|
execl("/usr/bin/sha256sum","sha256sum","--",argv[3],(char*)0);_exit(75); }
|
|
close(p[1]); f=fdopen(p[0],"r"); if(!fgets(hash,sizeof hash,f)) return 76;
|
|
fclose(f); if(waitpid(pid,&status,0)!=pid || !WIFEXITED(status) || WEXITSTATUS(status)) return 77;
|
|
return strncmp(hash,"EXPECTED_SHA256",64) ? 78 : 0;
|
|
}
|
|
'''.replace('EXPECTED_SHA256', binding['os_update']['manifest_sha256'])
|
|
write('/root/fixture-native-verifier.c', native_verifier)
|
|
run('gcc','-O2','-o',str(executor.VERIFIER),'/root/fixture-native-verifier.c')
|
|
executor.VERIFIER.chmod(0o755)
|
|
binding['os_update']['verifier_sha256'] = sha(executor.VERIFIER)
|
|
# Explicit disposable authentication adapter appointment. Production initial
|
|
# custody must use the already appointed native release-root verifier; we cannot
|
|
# produce a production-signed candidate here. No production code is patched.
|
|
# Model independently established verifier custody and record the substitution.
|
|
custody = dict(version=1,verifier_sha256=sha(executor.VERIFIER),
|
|
prerequisite_backend_sha256=sha(executor.VERIFIER))
|
|
assert not (executor.ROOT/'verifier-custody.json').exists()
|
|
write(executor.ROOT/'verifier-custody.json', wire.canonical(custody))
|
|
write('/root/fixture-verifier-appointment.json', wire.canonical(dict(
|
|
fixture_only=True,production_signature_approval=False,custody=custody)))
|
|
write(incoming / 'admission.json', wire.canonical(binding))
|
|
write(context['EVIDENCE']/'binding.json', wire.canonical(binding))
|
|
files = {name: sha(context['source']/name) for name in ('plan.json','manifest.json','preservation.json')}
|
|
plan = dict(schema=1, machine_id_sha256=sha('/etc/machine-id'),
|
|
current={'version':'2.0.0-alpha+uat','source_sha256':sha(PACKAGE/'scripts/d09_worker.py'), 'backend_sha256':sha(owner.DESTINATIONS['backend'])},
|
|
candidate={'version':'2.0.1-alpha','source_sha256':wire.digest(wire.canonical(files)), 'backend_sha256':binding['artifacts']['backend']},
|
|
admin={'uid':admin.pw_uid,'service_uid':account.pw_uid,'access_receipt_sha256':sha('/root/fixture-access.json')},
|
|
observations={'root/fixture-access.json':sha('/root/fixture-access.json'),
|
|
'root/fixture-verifier-appointment.json':sha('/root/fixture-verifier-appointment.json')}, files=files)
|
|
if NATIVE:
|
|
plan['current']['source_sha256']=sha(PACKAGE/'accepted-source-receipt.json')
|
|
plan['observations'].update({'opt/ota-source/accepted-source-receipt.json':sha(PACKAGE/'accepted-source-receipt.json'),
|
|
'opt/os-ota-native-proof.json':sha('/opt/os-ota-native-proof.json'),
|
|
'root/accepted-old-running.json':sha('/root/accepted-old-running.json')})
|
|
write('/root/bootstrap-preparation.json', wire.canonical(plan))
|
|
bootstrap.prepare(Path('/'), Path('/root/bootstrap-preparation.json'), sha('/root/bootstrap-preparation.json'), context['source'])
|
|
approval = dict(version=1, prepared_sha256=sha('/root/bootstrap-preparation.json'), package_root=str(PACKAGE),
|
|
package_files=activation.package_inventory(PACKAGE), admission_sha256=sha(incoming/'admission.json'),
|
|
os_inputs=files, helper_sha256=sha(PACKAGE/'scripts/os_updater_activate.py'),
|
|
owner_dropin_sha256=bootstrap.digest(activation.owner_policy(executor.ROOT)))
|
|
write('/root/bootstrap-activation.json', wire.canonical(approval))
|
|
preserved = {str(p): sha(p) for p in [Path('/etc/passwd'),Path('/etc/group'),Path('/etc/shadow'),
|
|
*Path('/var/lib/archipelago/ota-synthetic-canaries').iterdir()]}
|
|
try:
|
|
result = activation.activate(Path('/root/bootstrap-activation.json'), sha('/root/bootstrap-activation.json'))
|
|
except BaseException as exc:
|
|
print('ACTIVATION_FAILURE', repr(exc), getattr(exc,'stdout',None), getattr(exc,'stderr',None), flush=True)
|
|
run('journalctl','-u','archipelago-d09-lifecycle.service','-u','archipelago.service','--no-pager',check=False)
|
|
raise
|
|
print('BOOTSTRAP_ACTIVATED', json.dumps(result), flush=True)
|
|
assert result['state'] == 'D09_PREREQUISITE_ACTIVATED'
|
|
if NATIVE:
|
|
print('ACCEPTED_OLD_NATIVE_HEALTH',wait(native_health,'accepted old native health',timeout=90),flush=True)
|
|
assert native_identity == {path:sha(path) for path in native_identity}
|
|
try:
|
|
activation.activate(Path('/root/bootstrap-activation.json'), sha('/root/bootstrap-activation.json'))
|
|
raise AssertionError('activation replay accepted')
|
|
except ValueError as exc:
|
|
assert 'existing activation intent' in str(exc)
|
|
staging_generation = secrets.token_hex(16)
|
|
write(control/'request-apply.json', json.dumps(['prepare', '--selected-release',
|
|
json.dumps(binding['release_identity']), '--staging-generation', staging_generation]), 0o644)
|
|
reply = wait(lambda: (control/'result-apply.json').read_text() if (control/'result-apply.json').exists() else None, 'client apply')
|
|
print('D09_APPLY_REPLY', reply, flush=True)
|
|
lost_reply = json.loads(reply)['exit'] != 0
|
|
if lost_reply:
|
|
assert 'TimeoutError: timed out' in json.loads(reply)['stderr'], reply
|
|
# A lost reply is not permission to replay an OS operation. Observe the
|
|
# independently protected exact reservation; never send another request.
|
|
def accepted_after_lost_reply():
|
|
value = json.loads((owner.ROOT/'ledger.json').read_text())
|
|
return any(item['request'].get('staging_generation')==staging_generation
|
|
for item in value['requests'].values())
|
|
wait(accepted_after_lost_reply,'protected reservation after lost reply',timeout=30)
|
|
print('D09_REPLY_LOST_RESERVATION_OBSERVED_NO_RETRY',flush=True)
|
|
status_path = executor.ROOT/'transactions'/binding['os_update']['authority_digest']/'status.json'
|
|
def completed():
|
|
if status_path.exists():
|
|
status = json.loads(status_path.read_text())
|
|
if status['phase'] == 'RECONCILIATION_REQUIRED':
|
|
raise AssertionError(json.dumps(status))
|
|
if status['phase'] == 'REBOOT_REQUIRED':
|
|
return status
|
|
ledger = json.loads((owner.ROOT/'ledger.json').read_text())
|
|
if (ledger.get('receipt') or {}).get('phase') == 'CONFLICT':
|
|
run('journalctl','--no-pager','-u','archipelago-d09-lifecycle.service',
|
|
'-u',ledger['execution']['unit'],check=False)
|
|
raise AssertionError(json.dumps(ledger))
|
|
status = wait(completed, 'actual OS apply')
|
|
def candidate_started():
|
|
value = json.loads((owner.ROOT/'ledger.json').read_text())
|
|
receipt = value.get('receipt') or {}
|
|
if receipt.get('phase') == 'CONFLICT':
|
|
raise AssertionError(json.dumps(value))
|
|
return receipt if receipt.get('phase') == 'AWAITING_OBSERVATION' else None
|
|
receipt = wait(candidate_started, 'candidate manager actual activation', timeout=90)
|
|
current = owner.service()
|
|
assert current['ActiveState']=='active' and current['InvocationID']==receipt['observed_invocation_id']
|
|
assert sha('/proc/'+current['MainPID']+'/exe')==binding['artifacts']['backend']
|
|
if NATIVE:
|
|
print('CANDIDATE_NATIVE_HEALTH',wait(native_health,'candidate native health',timeout=90),flush=True)
|
|
assert native_identity == {path:sha(path) for path in native_identity}
|
|
assert preserved == {path: sha(path) for path in preserved}
|
|
wallet = Path('/var/lib/archipelago/ota-synthetic-canaries/wallet.db')
|
|
wallet.write_bytes(wallet.read_bytes() + b'forward-only-after-OS-apply\n')
|
|
forward = sha(wallet)
|
|
try:
|
|
activation.activate(Path('/root/bootstrap-activation.json'), sha('/root/bootstrap-activation.json'))
|
|
raise AssertionError('post-apply bootstrap replay accepted')
|
|
except ValueError as exc:
|
|
assert 'existing activation intent' in str(exc)
|
|
assert sha(wallet) == forward
|
|
print('BOOTSTRAP_OS_ACTUAL_EFFECTS', json.dumps({'bootstrap':result,'os_phase':status['phase'],
|
|
'os_status_sha256':wire.digest(wire.canonical(status)),
|
|
'preserved':preserved,'forward_wallet_sha256':forward,'accepted_native_artifact':NATIVE,'production_signature_approval':False}), flush=True)
|
|
write('/var/log/os-ota-stage/bootstrap-result.json', wire.canonical({'passed':True,'bootstrap':result,
|
|
'os_status':status,'preserved':preserved,'forward_wallet_sha256':forward,'native_identity':native_identity,
|
|
'accepted_native_artifact':NATIVE,'production_signature_approval':False,'client_reply_lost':lost_reply,
|
|
'os_state_root':str(executor.ROOT),'application_data_uid':Path('/var/lib/archipelago').stat().st_uid}))
|
|
write('/var/log/os-ota-stage/result.json', wire.canonical({'passed':True,
|
|
'scope':'bootstrap -> actual D09 owner/worker -> installed Debian OS effects',
|
|
'accepted_native_artifact':NATIVE,'production_signature_approval':False,
|
|
'postboot_confirmed':False,'phase':status['phase'],'d09_phase':receipt['phase'],'client_reply_lost':lost_reply,
|
|
'os_state_root':str(executor.ROOT),'application_data_uid':Path('/var/lib/archipelago').stat().st_uid}))
|