#!/usr/bin/env python3 """Apply the actual prepared OS stage; cold boot remains a separate invocation.""" import json import os from pathlib import Path 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' sys.path.insert(0, '/opt/ota-source/scripts') import os_update_executor as executor evidence = Path('/var/log/os-ota-stage') binding = json.loads((evidence/'binding.json').read_text()) plan = json.loads((evidence/'plan.json').read_text()) before = json.loads((evidence/'canaries-before.json').read_text()) canaries = Path('/var/lib/archipelago/ota-synthetic-canaries') hashes = lambda: {p.name: executor.file_hash(p) for p in canaries.iterdir()} assert before == hashes() # Interrupt the real invoking process after durable completion but before it can # return a reply. Re-entry must observe completion, never rerun maintainer scripts. log_path=evidence/'interrupted-apply.log' if log_path.exists(): log_path.rename(log_path.with_name('interrupted-apply-retained-'+str(time.time_ns())+'.log')) log = log_path.open('x') child = subprocess.Popen([sys.executable, '-B', '-c', ''' import json,sys,time sys.path.insert(0,'/opt/ota-source/scripts') import os_update_executor as e b=json.load(open('/var/log/os-ota-stage/binding.json')) e.stage(b,'APPLY','apply') time.sleep(600) '''], stdout=log, stderr=subprocess.STDOUT) try: deadline = time.monotonic()+1500 while time.monotonic() < deadline: status = executor.Executor(binding['os_update']).status if status['phase'] == 'REBOOT_REQUIRED': break if child.poll() is not None: raise RuntimeError('actual stage failed; inspect interrupted-apply.log') time.sleep(.2) else: raise TimeoutError('actual stage did not complete') finally: if child.poll() is None: child.kill() child.wait() log.close() assert child.returncode == -9 assert status['phase'] == 'REBOOT_REQUIRED' assert before == hashes() with (canaries/'wallet.db').open('a') as stream: stream.write('FORWARD-AFTER-OS-APPLY-sequence-18\n') after = hashes() (evidence/'canaries-after.json').write_text(json.dumps(after, sort_keys=True)) inventory = executor.inventory() assert executor.stage(binding, 'APPLY', 'apply') == status assert executor.inventory() == inventory and hashes() == after grub = Path('/boot/grub/grub.cfg').read_text() assert plan['boot']['running_release'] in grub and plan['boot']['expected_release'] in grub result = dict(phase=status['phase'], repeat_idempotent=True, canaries_preserved=True, forward_wallet_write_preserved=True, retained_old_grub_entry=True, target_kernel=plan['boot']['expected_release'], boot_id=executor.boot_id(), interrupted_after_stage_exit=child.returncode, production_approved=False, native_D09_transport=False) (evidence/'apply-result.json').write_text(json.dumps(result, indent=2)+'\n') print(json.dumps(result, indent=2))