62 lines
3.9 KiB
Python
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))
|