72 lines
3.5 KiB
Python
72 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Preserve a newer wall clock through the actual native startup sequence."""
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
|
|
project = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(project / 'tools'))
|
|
from image_formats import digest
|
|
from vm_test import VM
|
|
|
|
work = Path(tempfile.mkdtemp(prefix='m12-clock.', dir=project / 'out'))
|
|
rootfs = (project / 'out/rootfs-development.tar').resolve(strict=True)
|
|
stage2 = 'etc/s6-linux-init/current/scripts/rc.init'
|
|
future = 1_893_456_000 # 2030-01-01 UTC, set only inside the disposable guest.
|
|
with tarfile.open(rootfs) as source, tarfile.open(work / 'clock.tar', 'w', format=tarfile.PAX_FORMAT) as output:
|
|
assert source.extractfile('usr/share/fds/image-profile').read().strip() == b'development'
|
|
epoch = int(source.extractfile('usr/share/fds/build-epoch').read())
|
|
assert epoch < future
|
|
found = False
|
|
for member in source:
|
|
if member.name == stage2:
|
|
member.name += '.real'
|
|
found = True
|
|
output.addfile(member, source.extractfile(member) if member.isfile() else None)
|
|
assert found
|
|
# Preserve the actual stage 2 and boottrace binary. This fixture only sets
|
|
# the guest kernel clock before the normal startup code adopts boot events.
|
|
data = f'#!/bin/bash\nset -euo pipefail\ndate -u -s @{future} >/dev/null\nexec /{stage2}.real "$@"\n'.encode()
|
|
member = tarfile.TarInfo(stage2)
|
|
member.mode = 0o755
|
|
member.size = len(data)
|
|
output.addfile(member, io.BytesIO(data))
|
|
(work / 'image').mkdir()
|
|
with (work / 'image.log').open('wb') as log:
|
|
subprocess.run([str(project / 'image/build-system-cartridge'), '--profile', 'development',
|
|
'--rootfs', str(work / 'clock.tar'), '--output-directory', str(work / 'image')],
|
|
check=True, stdout=log, stderr=subprocess.STDOUT)
|
|
image = work / 'image/system.img'
|
|
before = digest(image)
|
|
with VM(work, 'retained-clock', image) as vm:
|
|
vm.expect(rb'FDS> ')
|
|
assert vm.capture('id -u') == '1000'
|
|
status = json.loads(vm.capture('cat /run/fds/clock.json'))
|
|
assert status['source'] == 'retained_kernel_clock', status
|
|
assert status['previous_unix_seconds'] >= future, status
|
|
assert status['minimum_unix_seconds'] == status['previous_unix_seconds'], status
|
|
assert int(vm.capture('date -u +%s')) >= future
|
|
assert 'requires root' in vm.capture('fds-boottrace clock-floor', ok=False)
|
|
report = json.loads(vm.capture('fds --json boot-profile'))
|
|
assert report['clock'] == 'Linux CLOCK_BOOTTIME', report
|
|
assert 0 < report['durations_ns']['kernel-to-console'] < 120_000_000_000, report
|
|
(work / 'clock.json').write_text(json.dumps(status, indent=2) + '\n')
|
|
(work / 'boot-profile.json').write_text(json.dumps(report, indent=2) + '\n')
|
|
deadline = time.monotonic() + 60
|
|
while vm.capture('test -S /run/fds/control.sock && echo ready || echo pending') != 'ready':
|
|
assert time.monotonic() < deadline
|
|
vm.send('fds poweroff')
|
|
vm.expect(rb'reboot: Power down')
|
|
assert vm.child.wait(timeout=20) == 0
|
|
assert digest(image) == before
|
|
link = project / 'out/m12-clock-latest.next'
|
|
link.symlink_to(work.name)
|
|
link.replace(project / 'out/m12-clock-latest')
|
|
print(f'PASS: newer guest clock preserved, ordinary-user mutation rejected, monotonic boot measurement retained, native halt: {work}')
|
|
print('NOTE: the test changes only a disposable VM clock; physical Pi RTC behavior remains deferred')
|