Files
archy/tests/regression/node-prerequisite-packages.py

62 lines
3.9 KiB
Python

#!/usr/bin/python3
"""Actual node-specific dpkg transaction in a disposable systemd/ext4 realm."""
import hashlib
import json
import os
from pathlib import Path
import subprocess
import time
P=Path;PACKAGE=P('/opt/node-package-fixture');OUT=P('/evidence')
assert os.geteuid()==0 and P('/run/.containerenv').exists()
assert os.uname().nodename=='node-package-qualification'
plan=json.loads((PACKAGE/'plan.json').read_text());baseline=json.loads((PACKAGE/'baseline.json').read_text())
def sha(path):
with P(path).open('rb') as f:return hashlib.file_digest(f,'sha256').hexdigest()
def run(args,name,timeout=180):
p=subprocess.run(args,capture_output=True,text=True,timeout=timeout)
(OUT/(name+'.json')).write_text(json.dumps({'args':args,'exit':p.returncode,'stdout':p.stdout,'stderr':p.stderr},indent=2))
p.check_returncode();return p.stdout
def services():
return run(['systemctl','show','NetworkManager.service','polkit.service','dbus.service','systemd-logind.service',
'-p','Id','-p','ActiveState','-p','MainPID','-p','InvocationID'], 'services-'+str(time.monotonic_ns()))
assert sha('/var/lib/dpkg/status')==plan['dpkg_status_sha256']==baseline['dpkg_status_sha256']
before=sha('/var/lib/dpkg/status')
versions_before=run(['dpkg-query','-W','-f=${Package}\t${Version}\t${Status}\n'],'versions-before')
# Start real service daemons in this namespace. NM owns no physical interface.
run(['systemctl','start','dbus.service','NetworkManager.service','polkit.service','systemd-logind.service'],'start-services')
state_before=services()
run(['/usr/bin/python3','-c','import sys; print(sys.version)'],'python-before')
paths=[]
for name,item in plan['selected'].items():
path=PACKAGE/'debs'/P(item['filename']).name
assert path.stat().st_size==item['size'] and sha(path)==item['sha256']
paths.append(str(path))
assert sum(P(path).stat().st_size for path in paths)==plan['budget']['deb_exact_bytes']
# Export a boundary file to the outside observer before the first package hook.
(OUT/'transaction-start.json').write_text(json.dumps({'monotonic_ns':time.monotonic_ns(),'dpkg_status_sha256':before}))
started=time.monotonic()
run(['dpkg','--unpack',*paths],'dpkg-unpack',timeout=300)
run(['dpkg','--configure','-a'],'dpkg-configure',timeout=300)
elapsed=time.monotonic()-started
run(['dpkg','--audit'],'dpkg-audit')
versions_after=run(['dpkg-query','-W','-f=${Package}\t${Version}\t${Status}\n'],'versions-after')
parsed={line.split('\t')[0]:(line.split('\t')[1:]) for line in versions_after.splitlines()}
for name,item in plan['selected'].items():assert parsed[name]==[item['version'],'install ok installed'],name
run(['/usr/bin/python3','-c','from gi.repository import Gio,GLib; import ctypes; ctypes.CDLL("libpython3.13.so.1.0"); print(GLib.MAJOR_VERSION,GLib.MINOR_VERSION)'],'python-gi-after')
state_after=services()
for unit in ('NetworkManager.service','polkit.service','dbus.service','systemd-logind.service'):
run(['systemctl','is-active',unit],'active-'+unit)
run(['journalctl','--no-pager'],'all-service-journal')
hooks={}
for name in plan['selected']:
for path in P('/var/lib/dpkg/info').glob(name+'*'):
if path.suffix in ('.preinst','.postinst','.prerm','.postrm','.triggers') and path.is_file():hooks[str(path)]=sha(path)
result={'passed':True,'baseline_dpkg_status_sha256':before,'after_dpkg_status_sha256':sha('/var/lib/dpkg/status'),
'selected':{name:item['version'] for name,item in plan['selected'].items()},'compressed_deb_bytes':sum(P(p).stat().st_size for p in paths),
'transaction_seconds':elapsed,'maintainer_hook_sha256':hooks,'services_before':state_before,'services_after':state_after,
'kernel_unchanged':os.uname().release,'physical_power_operation':False,
'scope':'actual package transaction against captured node dpkg database and public Python/GLib payload/hook baseline; synthetic service configuration, no installed private data'}
(OUT/'transaction-result.json').write_text(json.dumps(result,indent=2))
print(json.dumps(result))