68 lines
3.0 KiB
Python
68 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Reproducible source owner package; no installation or admission authority.
|
|
|
|
Fixed runtime destinations only. Native ELF/signature and external policy inputs
|
|
are separately pinned release inputs. Refuses overwriting an existing artifact.
|
|
"""
|
|
import hashlib
|
|
import ast
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tarfile
|
|
|
|
ROOT=Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0,str(ROOT/'scripts'))
|
|
|
|
|
|
def modules(root):
|
|
# Read the provisioner's declaration without executing privileged modules.
|
|
tree=ast.parse((root/'scripts/provision-d09-lifecycle.py').read_text())
|
|
declared=None
|
|
for node in tree.body:
|
|
if isinstance(node,ast.Assign) and any(isinstance(t,ast.Name) and t.id=='MODULES' for t in node.targets):
|
|
declared=ast.literal_eval(node.value)
|
|
if not declared:
|
|
raise ValueError('missing provisioner module declaration')
|
|
names=set(declared)
|
|
for pattern in ('c01_*.py','d09_*.py','host_*.py'):
|
|
names.update(p.name for p in (root/'scripts').glob(pattern))
|
|
return sorted(names)
|
|
|
|
|
|
def package(destination):
|
|
files={}
|
|
for name in modules(ROOT):
|
|
files['usr/libexec/'+name]=(ROOT/'scripts'/name,0o644)
|
|
if name.startswith(('c01_','host_')) or name=='runtime_owner.py':
|
|
files['usr/lib/archipelago/'+name]=(ROOT/'scripts'/name,0o644)
|
|
for source,target in {'d09-lifecycle-owner.py':'archipelago-d09-owner',
|
|
'd09-lifecycle-client.py':'archipelago-d09ctl','provision-d09-lifecycle.py':'archipelago-d09-provision',
|
|
'dashboard-public-guard.py':'archipelago-d09-public-guard'}.items():
|
|
files['usr/libexec/'+target]=(ROOT/'scripts'/source,0o755)
|
|
for source,target in {'prepare-terminal-workspace.sh':'archipelago-d09-prepare-workspace',
|
|
'60-terminal-workspace.conf':'archipelago-d09-terminal-workspace.conf'}.items():
|
|
files['usr/libexec/'+target]=(ROOT/'image-recipe/configs'/source,0o755 if source.endswith('.sh') else 0o644)
|
|
# Pre-read all inputs so an absent helper cannot leave a partial artifact.
|
|
payload={}
|
|
for target,(source,mode) in files.items():
|
|
if source.is_symlink() or not source.is_file():
|
|
raise ValueError(f'not a regular package input: {source}')
|
|
payload[target]=(source.read_bytes(),mode)
|
|
manifest={}
|
|
with tarfile.open(destination,'x') as archive:
|
|
for target,(raw,mode) in sorted(payload.items()):
|
|
manifest[target]=hashlib.sha256(raw).hexdigest()
|
|
info=tarfile.TarInfo(target);info.mode=mode;info.uid=info.gid=info.mtime=0;info.size=len(raw)
|
|
archive.addfile(info,io.BytesIO(raw))
|
|
raw=json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()
|
|
info=tarfile.TarInfo('D09-C01-SOURCE-SHA256.json');info.mode=0o444;info.size=len(raw)
|
|
archive.addfile(info,io.BytesIO(raw))
|
|
return manifest
|
|
|
|
|
|
if __name__=='__main__':
|
|
if len(sys.argv)!=2: raise SystemExit('one new artifact pathname required')
|
|
package(Path(sys.argv[1]))
|