Files
archy/scripts/tests/package_c01_normal_fixture.py

215 lines
12 KiB
Python

#!/usr/bin/env python3
"""Freeze normal-start source, package a non-libtest build, and record drift.
Only repository source and explicitly selected distro ELF dependencies are copied.
No runtime homes, container stores, credentials or host mounts are inputs.
"""
import argparse
import hashlib
import gzip
import io
import json
import os
from pathlib import Path
import stat
import struct
import shutil
import subprocess
import tarfile
def sha(path):
return hashlib.file_digest(path.open('rb'), 'sha256').hexdigest()
def record(path, value):
path.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n')
def inventory(root):
names = subprocess.check_output([
'git', '-C', str(root), 'ls-files', '-z', '--cached', '--others',
'--exclude-standard', '--', 'core', 'scripts', 'image-recipe/configs',
'tests', 'apps'], text=True).split('\0')
return sorted(set(n for n in names if n and (root / n).is_file()))
def freeze(root, out):
out.mkdir(mode=0o700)
destination = out / 'source'
destination.mkdir()
manifest = {}
for name in inventory(root):
source = root / name
if source.is_symlink():
raise RuntimeError('source alias requires review: ' + name)
data = source.read_bytes()
target = destination / name
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
target.chmod(0o555 if source.stat().st_mode & 0o111 else 0o444)
manifest[name] = hashlib.sha256(data).hexdigest()
record(out / 'source-sha256.json', manifest)
changed = [n for n in manifest if not (root / n).is_file() or sha(root / n) != manifest[n]]
changed += sorted(set(inventory(root)) - set(manifest))
record(out / 'freeze-check.json', {'changed_during_copy': changed})
if changed:
raise RuntimeError('source changed during freeze; retain attempt and retry')
record(out / 'source-identity.json', {
'repository': str(root),
'head': subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip(),
'branch': subprocess.check_output(['git', '-C', str(root), 'branch', '--show-current'], text=True).strip(),
'manifest_sha256': sha(out / 'source-sha256.json')})
(out / 'containers.conf').write_text('[containers]\nkeyring=false\n')
record(out / 'accommodations.json', {
'outer_keyring': 'disabled for exhausted rootless kernel key quota; not keyring isolation qualification',
'outer_proc': 'unmask=ALL for nested rootless proc mounting',
'nested_runtime': 'historical fixture uses vfs, network none, no ping sysctl, seccomp unconfined',
'tmux': 'copied public distro ELF and loader; exact package manifest retained'})
with tarfile.open(out / 'source.tar', 'x') as archive:
for name in manifest:
archive.add(destination / name, arcname=name, recursive=False)
with (out/'source.tar').open('rb') as source,(out/'source.tar.gz').open('xb') as destination:
with gzip.GzipFile(filename='',fileobj=destination,mode='wb',mtime=0) as compressed:
shutil.copyfileobj(source,compressed)
def artifact(binary, out):
# The runner builds --bin archipelago, never cargo test / a guarded entry.
target = out / 'normal-elf'
subprocess.run(['strip', '--strip-unneeded', '-o', str(target), str(binary)], check=True)
def allocated(path):
data=path.read_bytes()
assert data[:6]==b'\x7fELF\x02\x01', 'reviewed ELF64 little-endian artifact required'
header=struct.unpack_from('<16sHHIQQQIHHHHHH',data)
section_offset,section_size,count,names_index=header[6],header[11],header[12],header[13]
sections=[struct.unpack_from('<IIQQQQIIQQ',data,section_offset+i*section_size) for i in range(count)]
strings=sections[names_index];strings=data[strings[4]:strings[4]+strings[5]]
result={}
for name,kind,flags,address,offset,size,*_ in sections:
if flags&2:
label=strings[name:].split(b'\0',1)[0].decode()
result[label]=[kind,flags,address,size,hashlib.sha256(data[offset:offset+size]).hexdigest() if kind!=8 else None]
return {'machine':header[2],'entry':header[4],'sections':result}
before,after=allocated(binary),allocated(target)
assert before==after, 'strip changed allocated native sections/entry'
record(out/'strip-derivation.json',{'operation':'strip --strip-unneeded','allocated_sections_unchanged':True,
'before_bytes':binary.stat().st_size,'after_bytes':target.stat().st_size,'loadable_identity':after})
dependencies = {Path(w) for w in subprocess.check_output(['ldd', str(binary)], text=True).split()
if w.startswith('/')}
files = {'usr/local/bin/archipelago': target}
for path in dependencies:
source = path.resolve(strict=True)
for p in (source, *source.parents):
meta = p.stat()
if meta.st_uid != 0 or meta.st_mode & 0o022:
raise RuntimeError('unprotected distro dependency: ' + str(p))
files[str(path).lstrip('/')] = source
manifest = {}
with tarfile.open(out / 'normal-native.tar', 'x') as archive:
for name, source in sorted(files.items()):
data = source.read_bytes()
manifest[name] = {'source': str(source), 'sha256': hashlib.sha256(data).hexdigest()}
info = tarfile.TarInfo(name)
info.size, info.mode = len(data), 0o755
archive.addfile(info, io.BytesIO(data))
record(out / 'native-artifact-sha256.json', manifest)
with (out/'normal-native.tar').open('rb') as source,(out/'normal-native.tar.gz').open('xb') as destination:
with gzip.GzipFile(filename='',fileobj=destination,mode='wb',mtime=0) as compressed:
shutil.copyfileobj(source,compressed)
with gzip.open(out/'normal-native.tar.gz','rb') as restored:
require_hash=hashlib.file_digest(restored,'sha256').hexdigest()
assert require_hash==sha(out/'normal-native.tar'), 'compressed native artifact derivation'
record(out/'native-compression.json',{'uncompressed_bytes':(out/'normal-native.tar').stat().st_size,
'compressed_bytes':(out/'normal-native.tar.gz').stat().st_size,'uncompressed_sha256':require_hash,
'compressed_sha256':sha(out/'normal-native.tar.gz')})
record(out / 'native-derivation.json', {'compiled': str(binary), 'compiled_sha256': sha(binary),
'packaged_sha256': sha(target), 'source_manifest_sha256': sha(out / 'source-sha256.json'),
'kind': 'cargo build --locked --offline --bin archipelago (not libtest)'})
def verify(root, out):
manifest = json.loads((out / 'source-sha256.json').read_text())
frozen_changes = [n for n, digest in manifest.items() if sha(out / 'source' / n) != digest]
live_changes = [n for n, digest in manifest.items() if not (root / n).is_file() or sha(root / n) != digest]
record(out / 'source-after.json', {'frozen_changes': frozen_changes, 'live_changes': live_changes,
'live_additions': sorted(set(inventory(root)) - set(manifest))})
if frozen_changes:
raise RuntimeError('frozen inputs changed')
def replay(previous, out):
"""Retry immutable prior build after an infrastructure failure, not live code."""
manifest = json.loads((previous / 'source-sha256.json').read_text())
for name, digest in manifest.items():
if sha(previous / 'source' / name) != digest:
raise RuntimeError('prior frozen source drift: ' + name)
derivation = json.loads((previous / 'native-derivation.json').read_text())
assert sha(previous / 'source-sha256.json') == derivation['source_manifest_sha256']
assert sha(previous / 'normal-elf') == derivation['packaged_sha256']
out.mkdir(mode=0o700)
shutil.copytree(previous / 'source', out / 'source')
for name in ('source.tar', 'source-sha256.json', 'source-identity.json', 'normal-native.tar',
'normal-elf', 'native-artifact-sha256.json', 'native-derivation.json',
'tmux-runtime.tar', 'tmux-runtime-sha256.json'):
shutil.copyfile(previous / name, out / name)
(out / 'containers.conf').write_text('[containers]\nkeyring=false\n')
record(out / 'replay.json', {'previous_attempt': str(previous),
'source': 'same frozen source, not current worktree',
'keyring': 'creation disabled due to rootless quota; not keyring isolation qualification',
'package_driver_sha256': sha(Path(__file__))})
def reuse_native(previous, out):
"""Reuse only when all non-fixture/embedded build inputs are byte identical.
Embedded owner/coordinator modules must match the native build exactly.
Never relabel the binary's original source manifest as the new fixture one.
"""
old = json.loads((previous / 'source-sha256.json').read_text())
new = json.loads((out / 'source-sha256.json').read_text())
allowed = {'tests/regression/c01-compose.py', 'tests/regression/c01-normal-systemd.py',
'tests/regression/c01-normal-acceptance.py',
'tests/regression/c01-history-migration.py',
'tests/regression/c01-running-services.py',
'tests/regression/c01-appointed-oci.py',
# Verified bundle tools execute externally; no Rust include_str input.
'scripts/c01_oci_job.py','scripts/import-uat-oci.py','scripts/build-iso-uat.py',
'scripts/tests/test_c01_oci_job.py','scripts/tests/test_uat_oci.py',
# External cold-migration coordinator, not embedded by c01_supervisor.rs.
'scripts/c01_migration.py', 'scripts/tests/test_c01_migration_approval.py',
'tests/regression/c01-role-views.py',
'tests/regression/c01-prerequisite-systemd.py', 'scripts/c01_access.py',
'scripts/c01_access_kernel.py',
'scripts/tests/test_c01_retirement_pins.py',
'scripts/tests/prepare_c01_nts_dependencies.py',
'tests/regression/c01-power-systemd.py', 'scripts/test-c01-normal-systemd-isolated.sh',
# External USER unit, read from the frozen deployment package; not
# embedded in Rust/CPython. Its distinct source manifest remains pinned.
'image-recipe/configs/archipelago-runtime-podman.service',
'image-recipe/configs/archipelago-c01-network.service',
'image-recipe/configs/70-c01-supervisor.conf.in',
'scripts/tests/package_c01_normal_fixture.py'}
changed = sorted(name for name in old.keys() | new.keys() if old.get(name) != new.get(name))
assert set(changed) <= allowed, 'native build inputs changed: ' + str(set(changed) - allowed)
original = json.loads((previous / 'native-derivation.json').read_text())
assert sha(previous / 'normal-elf') == original['packaged_sha256']
for name in ('normal-native.tar','normal-elf','native-artifact-sha256.json',
'tmux-runtime.tar','tmux-runtime-sha256.json'):
shutil.copyfile(previous / name, out / name)
for name in ('normal-native.tar.gz','native-compression.json','strip-derivation.json'):
if (previous/name).exists():shutil.copyfile(previous/name,out/name)
record(out / 'native-derivation.json', {**original,
'fixture_source_manifest_sha256': sha(out / 'source-sha256.json'),
'immutable_native_from': str(previous), 'nonembedded_changes': changed})
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('mode', choices=['freeze', 'artifact', 'verify', 'replay', 'reuse-native'])
parser.add_argument('input', type=Path)
parser.add_argument('receipt', type=Path)
args = parser.parse_args()
{'freeze': freeze, 'artifact': artifact, 'verify': verify, 'replay': replay,
'reuse-native': reuse_native}[args.mode](args.input.resolve(), args.receipt.resolve())