Live-USB durch eigenständige Anwendung ersetzen
Some checks failed
Tests / test (push) Has been cancelled

This commit is contained in:
2026-10-15 10:26:33 +02:00
parent 692bbbb815
commit 1f91db25b5
23 changed files with 205 additions and 1228 deletions

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
# Build in a new directory so old profiles and work directories are never reused.
set -Eeuo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
DEST=${1:?Usage: bash scripts/build-iso.sh /absolute/path/to/new-build-directory}
[[ ! -e "$DEST" ]] || { echo 'Destination must not already exist.' >&2; exit 2; }
mkdir -p "$DEST"
DEST=$(cd "$DEST" && pwd)
bash "$ROOT/scripts/create-profile.sh" "$DEST/profile"
python3 "$ROOT/scripts/release-manifest.py" --verify-profile "$DEST/profile"
sudo mkarchiso -v -w "$DEST/work" -o "$DEST/out" "$DEST/profile"
python3 "$ROOT/scripts/release-manifest.py" --verify-profile "$DEST/profile"
shopt -s nullglob
isos=("$DEST"/out/*.iso)
[[ ${#isos[@]} == 1 ]] || { echo 'Expected exactly one ISO.' >&2; exit 1; }
bsdtar -tf "${isos[0]}" > "$DEST/iso-files.txt"
grep -qx 'arch/boot/x86_64/vmlinuz-linux' "$DEST/iso-files.txt"
grep -qx 'arch/boot/x86_64/initramfs-linux.img' "$DEST/iso-files.txt"
[[ $(stat -c %s "${isos[0]}") -lt 4294967295 ]] || { echo 'ISO exceeds FAT32 file size limit.' >&2; exit 1; }
sha256sum "${isos[0]}" > "$DEST/out/SHA256SUMS"
cp "$DEST/profile/airootfs/etc/csr-intake/release.json" "$DEST/out/release.json"
echo "ISO validated in $DEST/out. Physical UEFI/BIOS boot and hardware tests still required."

View File

@@ -3,7 +3,7 @@ set -Eeuo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
export CSR_BUILD_ID
CSR_BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-id)
for component in scanner controller gui; do
for component in scanner gui; do
cargo fmt --check --manifest-path "$ROOT/$component/Cargo.toml"
cargo clippy --locked --manifest-path "$ROOT/$component/Cargo.toml" --all-targets -- -D warnings
cargo test --locked --manifest-path "$ROOT/$component/Cargo.toml"
@@ -12,4 +12,5 @@ done
python3 -m unittest discover -s "$ROOT/tests" -p 'test_*.py'
bash "$ROOT/tests/smoke.sh"
python3 "$ROOT/scripts/release-manifest.py" --create "$ROOT/build/release.json"
echo "Validated release: $CSR_BUILD_ID"
bash "$ROOT/scripts/build-standalone.sh"
echo "Validated standalone release: $CSR_BUILD_ID"

View File

@@ -32,6 +32,7 @@ mkdir -p "$TMP/app" "$(dirname "$OUT")"
install -m755 "$GUI" "$TMP/app/intake-gui"
install -m755 "$SCANNER" "$TMP/app/pc-intake"
install -m644 "$MANIFEST" "$TMP/app/release.json"
python3 "$ROOT/scripts/release-manifest.py" --verify-release "$TMP/app"
(
cd "$TMP/app"
sha256sum intake-gui pc-intake release.json > SHA256SUMS
@@ -85,10 +86,37 @@ esac
extract_bundle
data_dir=\${XDG_DATA_HOME:-\${HOME:-\$PWD}/.local/share}/csr-intake
expect_data=0
has_data=0
for argument in "\$@"; do
[ "\$argument" = '--data' ] && has_data=1
if [ "\$expect_data" -eq 1 ]; then
data_dir=\$argument
expect_data=0
elif [ "\$argument" = '--data' ]; then
has_data=1
expect_data=1
fi
done
[ "\$expect_data" -eq 0 ] || {
echo 'Missing directory after --data.' >&2
exit 2
}
workshop_config=\$data_dir/config/workshop.json
if [ -f "\$workshop_config" ]; then
keyboard_layout=\$(python3 - "\$workshop_config" <<'PY' 2>/dev/null || true
import json
import sys
layout = json.load(open(sys.argv[1], encoding='utf-8')).get('keyboard_layout', '')
if layout in ('de', 'us'):
print(layout)
PY
)
if [ -n "\$keyboard_layout" ]; then
export XKB_DEFAULT_LAYOUT=\$keyboard_layout
fi
fi
export CSR_SCANNER="\$app_dir/pc-intake"
if [ "\$has_data" -eq 1 ]; then
exec "\$app_dir/intake-gui" "\$@"

View File

@@ -1,62 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
DEST=${1:-$ROOT/build/profile}
[[ -d /usr/share/archiso/configs/releng ]] || { echo 'Install archiso first.' >&2; exit 1; }
[[ ! -e $DEST ]] || { echo "Profile already exists: $DEST" >&2; exit 1; }
bash "$ROOT/scripts/build-release.sh"
mkdir -p "$(dirname "$DEST")"
cp -a /usr/share/archiso/configs/releng "$DEST"
for pkg in cage seatd xorg-xwayland mesa vulkan-radeon vulkan-intel libglvnd fontconfig ttf-dejavu networkmanager curl ca-certificates sqlite smartmontools ffmpeg gcc glmark2 mpv pciutils util-linux e2fsprogs stress-ng memtester alsa-utils v4l-utils dmidecode python usbutils xdg-utils firefox; do
grep -qxF "$pkg" "$DEST/packages.x86_64" || echo "$pkg" >> "$DEST/packages.x86_64"
done
mkdir -p "$DEST/airootfs/usr/local/bin" "$DEST/airootfs/usr/local/lib/pc-intake" "$DEST/airootfs/etc/systemd/system/multi-user.target.wants"
install -m755 "$ROOT/scanner/target/release/pc-intake" "$DEST/airootfs/usr/local/bin/pc-intake"
install -m755 "$ROOT/controller/target/release/intake-controller" "$DEST/airootfs/usr/local/bin/intake-controller"
install -m755 "$ROOT/gui/target/release/intake-gui" "$DEST/airootfs/usr/local/bin/intake-gui"
install -m755 "$ROOT/scripts/intake-session.sh" "$DEST/airootfs/usr/local/bin/intake-session"
# The GUI must run in a real logind session, not a root system service attached to a TTY.
install -m755 "$ROOT/live/intake-prepare.sh" "$DEST/airootfs/usr/local/bin/intake-prepare"
install -m755 "$ROOT/live/intake-login.sh" "$DEST/airootfs/usr/local/bin/intake-login"
install -m644 "$ROOT/live/intake-prepare.service" "$DEST/airootfs/etc/systemd/system/intake-prepare.service"
mkdir -p "$DEST/airootfs/etc/systemd/system/getty@tty1.service.d"
install -m644 "$ROOT/live/autologin.conf" "$DEST/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf"
ln -s ../intake-prepare.service "$DEST/airootfs/etc/systemd/system/multi-user.target.wants/intake-prepare.service"
rm -f "$DEST/airootfs/etc/systemd/system/getty@tty1.service"
rm -f "$DEST/airootfs/etc/systemd/system/multi-user.target.wants/pc-intake.service"
rm -f "$DEST/airootfs/etc/systemd/system/pc-intake.service"
# Archiso can override copied file modes. Record explicit permissions too.
cat >> "$DEST/profiledef.sh" <<'EOF'
file_permissions+=(
["/usr/local/bin/intake-session"]="0:0:755"
["/usr/local/bin/pc-intake"]="0:0:755"
["/usr/local/bin/intake-controller"]="0:0:755"
["/usr/local/bin/intake-gui"]="0:0:755"
["/usr/local/bin/intake-prepare"]="0:0:755"
["/usr/local/bin/intake-login"]="0:0:755"
)
EOF
for path in usr/local/bin/intake-session usr/local/bin/pc-intake usr/local/bin/intake-controller usr/local/bin/intake-gui usr/local/bin/intake-prepare usr/local/bin/intake-login; do
test -x "$DEST/airootfs/$path" || { echo "FATAL: missing executable $path" >&2; exit 1; }
done
# Optional operator-approved provisioning at build time; ISO secrets are extractable.
if [[ ${CSR_EMBED_INVENTORY_CONFIG:-0} == 1 ]]; then
: "${CSR_PRIVATE_CONFIG_DIR:?Set CSR_PRIVATE_CONFIG_DIR to local private folder}"
test -f "$CSR_PRIVATE_CONFIG_DIR/inventory.json"
test -f "$CSR_PRIVATE_CONFIG_DIR/inventory-api-key"
mkdir -p "$DEST/airootfs/etc/csr-intake"
install -m600 "$CSR_PRIVATE_CONFIG_DIR/inventory.json" "$DEST/airootfs/etc/csr-intake/inventory.json"
install -m600 "$CSR_PRIVATE_CONFIG_DIR/inventory-api-key" "$DEST/airootfs/etc/csr-intake/inventory-api-key"
cat >> "$DEST/profiledef.sh" <<'PERMS'
file_permissions+=( ["/etc/csr-intake/inventory.json"]="0:0:600" ["/etc/csr-intake/inventory-api-key"]="0:0:600" )
PERMS
fi
echo "Profile created at $DEST"
# Keep NetworkManager running in the live image for LAN/WLAN diagnostics.
mkdir -p "$DEST/airootfs/etc/systemd/system/multi-user.target.wants"
ln -sfn /usr/lib/systemd/system/NetworkManager.service "$DEST/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service"
mkdir -p "$DEST/airootfs/etc/csr-intake"
install -m644 "$ROOT/build/release.json" "$DEST/airootfs/etc/csr-intake/release.json"
python3 "$ROOT/scripts/release-manifest.py" --verify-profile "$DEST"

View File

@@ -1,109 +0,0 @@
#!/usr/bin/env bash
# DESTRUCTIVE: explicitly invoked, strongly confirmed, never automatic.
set -Eeuo pipefail
DEVICE=${1:-}
ISO=${2:-}
if [[ ! "$DEVICE" =~ ^/dev/[a-zA-Z0-9/_-]+$ || ! -f "$ISO" ]]; then
echo "Usage: sudo bash scripts/install-usb.sh /dev/sdb /absolute/path/to/pc-intake.iso" >&2; exit 2
fi
[[ $EUID -eq 0 ]] || { echo 'Run as root.' >&2; exit 2; }
for cmd in lsblk sgdisk mkfs.vfat mkfs.ext4 grub-install mount umount findmnt wipefs sync bsdtar blockdev partprobe udevadm; do
command -v "$cmd" >/dev/null || { echo "Missing: $cmd" >&2; exit 2; }
done
DEVICE=$(readlink -f "$DEVICE")
[[ -b $DEVICE && $(lsblk -dn -o TYPE "$DEVICE") == disk ]] || { echo 'Target must be an entire block disk.' >&2; exit 2; }
[[ $(lsblk -dn -o TRAN "$DEVICE" | tr -d ' ') == usb ]] || { echo 'Refusing a device that is not identified as USB transport.' >&2; exit 2; }
root_source=$(findmnt -n -o SOURCE / || true)
root_disk=$(lsblk -no PKNAME "$root_source" 2>/dev/null | head -1 || true)
[[ -z "$root_disk" || "/dev/$root_disk" != "$DEVICE" ]] || { echo 'Refusing root system disk.' >&2; exit 2; }
if lsblk -nr -o MOUNTPOINT "$DEVICE" | grep -q '[^[:space:]]'; then
echo 'USB contains mounted partitions; unmount them before continuing.' >&2; lsblk "$DEVICE"; exit 2
fi
size_bytes=$(blockdev --getsize64 "$DEVICE")
((size_bytes >= 12*1024*1024*1024)) || { echo 'USB must be at least 12 GiB.' >&2; exit 2; }
iso_bytes=$(stat -c %s "$ISO")
((iso_bytes < 4294967295)) || { echo 'ISO exceeds the FAT32 single-file limit.' >&2; exit 2; }
ISO_LIST=$(mktemp)
TMP=
cleanup(){
if [[ -n $TMP ]]; then
umount "$TMP/boot" 2>/dev/null || true
umount "$TMP/data" 2>/dev/null || true
rmdir "$TMP/boot" "$TMP/data" "$TMP" 2>/dev/null || true
fi
rm -f "$ISO_LIST"
}
trap cleanup EXIT
bsdtar -tf "$ISO" > "$ISO_LIST" 2>/dev/null || {
echo 'ISO cannot be read as an archive.' >&2
exit 2
}
grep -qx 'arch/boot/x86_64/vmlinuz-linux' "$ISO_LIST" || {
echo 'ISO layout does not match GRUB config (kernel missing).' >&2
exit 2
}
grep -qx 'arch/boot/x86_64/initramfs-linux.img' "$ISO_LIST" || {
echo 'ISO layout does not match GRUB config (initramfs missing).' >&2
exit 2
}
model=$(lsblk -dn -o MODEL "$DEVICE" | xargs)
serial=$(lsblk -dn -o SERIAL "$DEVICE" | xargs)
echo '===== DESTRUCTIVE USB INSTALL ====='
echo "Device: $DEVICE Model: $model Serial: $serial"
echo "Capacity: $(lsblk -dn -o SIZE "$DEVICE") ISO: $ISO"
echo 'ALL PARTITIONS AND DATA ON THIS DEVICE WILL BE ERASED.'
read -rp "Type exactly ERASE $DEVICE to continue: " answer
[[ "$answer" == "ERASE $DEVICE" ]] || { echo 'Cancelled. Nothing erased.'; exit 1; }
# GPT: p1 BIOS boot (2MiB), p2 ESP FAT32 (7GiB), p3 ext4 remainder.
sgdisk --zap-all "$DEVICE"
wipefs -a "$DEVICE" >/dev/null
sgdisk -n 1:2048:+2M -t 1:ef02 -c 1:BIOS_GRUB "$DEVICE"
sgdisk -n 2:0:+7G -t 2:ef00 -c 2:INTAKE_BOOT "$DEVICE"
sgdisk -n 3:0:0 -t 3:8300 -c 3:INTAKE_DATA "$DEVICE"
if [[ "$DEVICE" =~ [0-9]$ ]]; then p2="${DEVICE}p2"; p3="${DEVICE}p3"; else p2="${DEVICE}2"; p3="${DEVICE}3"; fi
for i in {1..10}; do
blockdev --rereadpt "$DEVICE" 2>/dev/null || true
partprobe "$DEVICE" 2>/dev/null || true
udevadm settle
p2_size=$(blockdev --getsize64 "$p2" 2>/dev/null || echo 0)
p3_size=$(blockdev --getsize64 "$p3" 2>/dev/null || echo 0)
((p2_size >= 7*1024*1024*1024 && p3_size >= 4*1024*1024*1024)) && break
sleep 1
done
((p2_size >= 7*1024*1024*1024 && p3_size >= 4*1024*1024*1024)) || {
echo 'Kernel still reports stale partition sizes; reconnect the USB and retry.' >&2
exit 1
}
mkfs.vfat -F32 -n INTAKE_BOOT "$p2"
mkfs.ext4 -F -L INTAKE_DATA "$p3"
TMP=$(mktemp -d)
mkdir -p "$TMP/boot" "$TMP/data"
mount "$p2" "$TMP/boot"; mount "$p3" "$TMP/data"
mkdir -p "$TMP/boot/boot/grub" "$TMP/data/reports" "$TMP/data/database" "$TMP/data/checklists" "$TMP/data/reference"
if ! cp "$ISO" "$TMP/boot/pc-intake.iso" || ! sync -f "$TMP/boot"; then
current_size=$(blockdev --getsize64 "$DEVICE" 2>/dev/null || echo 0)
if ((current_size == 0)); then
echo 'USB medium disappeared while writing. Replace the stick or use another USB port.' >&2
else
echo 'USB write failed. Check the stick and the kernel log before retrying.' >&2
fi
exit 1
fi
cat > "$TMP/boot/boot/grub/grub.cfg" <<'GRUBCFG'
set timeout=5
set default=0
menuentry 'PC Intake Live USB (offline technician mode)' {
search --no-floppy --label INTAKE_BOOT --set=root
set isofile='/pc-intake.iso'
loopback loop $isofile
linux (loop)/arch/boot/x86_64/vmlinuz-linux img_dev=/dev/disk/by-label/INTAKE_BOOT img_loop=$isofile archisobasedir=arch loglevel=3
initrd (loop)/arch/boot/x86_64/initramfs-linux.img
}
menuentry 'Reboot' { reboot }
menuentry 'Power off' { halt }
GRUBCFG
grub-install --target=x86_64-efi --efi-directory="$TMP/boot" --boot-directory="$TMP/boot/boot" --removable --no-nvram --recheck
grub-install --target=i386-pc --boot-directory="$TMP/boot/boot" --recheck "$DEVICE"
sync
echo "USB is prepared. Results will be saved to $p3 (INTAKE_DATA)."
echo 'Unmount complete; boot a test PC with UEFI or legacy BIOS support.'

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
DATA=/mnt/intake-data
mkdir -p "$DATA"
if ! mountpoint -q "$DATA"; then
disk=$(blkid -L INTAKE_DATA || true)
if [[ -z "$disk" ]]; then echo 'FATAL: INTAKE_DATA partition missing. No scan will run.'; exec bash; fi
mount -o rw,nosuid,nodev "$disk" "$DATA" || { echo 'FATAL: cannot mount persistent storage'; exec bash; }
fi
# First boot: provision Inventory config to the persistent data partition.
mkdir -p "$DATA/config"
if [[ -d /etc/csr-intake ]]; then
for f in inventory.json inventory-api-key; do
if [[ -f "/etc/csr-intake/$f" && ! -e "$DATA/config/$f" ]]; then
install -m600 "/etc/csr-intake/$f" "$DATA/config/$f"
fi
done
fi
export XDG_RUNTIME_DIR=/run/pc-intake-runtime
mkdir -p "$XDG_RUNTIME_DIR"
chmod 0700 "$XDG_RUNTIME_DIR"
export WLR_RENDERER_ALLOW_SOFTWARE=1
if command -v cage >/dev/null && [[ -x /usr/local/bin/intake-gui ]]; then
echo "Launching graphical intake station..."
cage -s -- /usr/local/bin/intake-gui --data "$DATA"
echo "Graphical interface exited. Starting terminal fallback." >&2
fi
exec /usr/local/bin/intake-controller --data "$DATA"

View File

@@ -21,7 +21,13 @@ import urllib.error
import urllib.parse
import urllib.request
DEFAULT_DATA = Path('/mnt/intake-data')
def default_data_dir():
root = os.environ.get('XDG_DATA_HOME')
if root:
return Path(root) / 'csr-intake'
return Path.home() / '.local/share/csr-intake'
DEFAULT_DATA = default_data_dir()
def identity_value(value):
value = str(value or '').strip().casefold()
@@ -45,7 +51,7 @@ def hardware_id(report):
if serial and vendor:
payload = json.dumps([vendor, serial], ensure_ascii=True, separators=(',', ':'))
return 'dmi-serial-sha256:' + hashlib.sha256(payload.encode()).hexdigest()
raise ValueError('Keine zuverlässige Hardware-Kennung im Bericht. Mit der aktuellen USB-Version Hardware erneut erfassen; alte Berichte ohne Kennung werden nicht automatisch zugeordnet.')
raise ValueError('Keine zuverlässige Hardware-Kennung im Bericht. Mit der aktuellen Anwendung Hardware erneut erfassen; alte Berichte ohne Kennung werden nicht automatisch zugeordnet.')
def hardware_aliases(report):
primary = hardware_id(report)
@@ -105,7 +111,7 @@ def api_request(base, key, method, suffix, payload=None, timeout=12, ca_file=Non
'Authorization': 'Api-Key ' + key,
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'pc-intake-usb/1.4',
'User-Agent': 'csr-intake/2.6',
})
context = ssl.create_default_context()
if ca_file:
@@ -127,7 +133,7 @@ def credentials(config):
raise ValueError('Inventory URL must be an HTTPS origin (e.g. https://inventory.example.org)')
key = os.environ.get('CSR_API_KEY', '')
if not key:
secret = Path(config.get('key_file', '/mnt/intake-data/config/inventory-api-key'))
secret = Path(config.get('key_file', DEFAULT_DATA / 'config/inventory-api-key'))
key = secret.read_text(encoding='utf-8').strip()
if not key:
raise ValueError('No API key configured')
@@ -304,7 +310,7 @@ def queue_inspection(root,path,phase):
ident=inspection['identity']
identity={'system_uuid':ident.get('uuid'),'serial_number':ident.get('serial'),
'manufacturer':ident.get('manufacturer'),'model':ident.get('model')}
# An immutable report provides chassis information, not the USB host's hardware.
# An immutable report provides the inspected computer's chassis information.
if inspection.get('report'): identity.update({k:inspection['report'].get(k,identity.get(k)) for k in ['system_uuid','serial_number','manufacturer','model','chassis_type','batteries']})
results={k:{'outcome':v['outcome'],'notes':v.get('notes',''),'history':v.get('history',[]),'evidence':v.get('evidence','')} for k,v in inspection['checks'].items()}
body={'hwid':hardware_id(identity),'phase':phase,'technician':inspection.get('technician',''),
@@ -328,7 +334,7 @@ def flush_inspections(root,config):
def run_station(root,action,report_path):
# Recover the last saved snapshot even if power was lost before a worker queued it.
# These files may belong to a different PC previously visited by this USB.
# These files may belong to a different PC inspected earlier on this workstation.
if action in ['boot','flush']:
for saved in sorted((root/'inspections').glob('*.json')):
inspection=json.loads(saved.read_text())

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Source fingerprint and artifact verification; never touches USB devices."""
"""Create and verify the reproducible standalone application manifest."""
import argparse
import hashlib
import json
@@ -8,7 +8,7 @@ import subprocess
import tomllib
ROOT = Path(__file__).resolve().parents[1]
COMPONENTS = {'gui': 'intake-gui', 'scanner': 'pc-intake', 'controller': 'intake-controller'}
COMPONENTS = {'gui': 'intake-gui', 'scanner': 'pc-intake'}
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
@@ -19,8 +19,7 @@ def source_id():
base = ROOT / component
paths.extend((base / 'src').rglob('*.rs'))
paths.extend(p for p in [base / 'Cargo.toml', base / 'Cargo.lock', base / 'build.rs'] if p.exists())
for folder in ['scripts', 'live']:
paths.extend(p for p in (ROOT / folder).iterdir() if p.is_file())
paths.extend(p for p in (ROOT / 'scripts').iterdir() if p.is_file())
h = hashlib.sha256()
for path in sorted(paths):
h.update(str(path.relative_to(ROOT)).encode() + b'\0')
@@ -41,38 +40,33 @@ def create(path):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(manifest, indent=2) + '\n')
def verify(profile):
root = profile / 'airootfs'
manifest = json.loads((root / 'etc/csr-intake/release.json').read_text())
def verify(release_dir):
manifest = json.loads((release_dir / 'release.json').read_text())
expected = set(COMPONENTS.values())
if manifest.get('schema_version') != 1:
raise ValueError('Unsupported release manifest schema')
if set(manifest.get('binaries', {})) != expected:
raise ValueError('Release manifest has missing or unexpected binaries')
for name, spec in manifest['binaries'].items():
binary = root / 'usr/local/bin' / name
binary = release_dir / name
if digest(binary) != spec['sha256']:
raise ValueError(f'Binary hash mismatch: {name}')
if not binary.stat().st_mode & 0o111:
raise ValueError(f'Binary is not executable: {name}')
version = subprocess.check_output([root / 'usr/local/bin/intake-gui', '--version'], text=True)
version = subprocess.check_output([release_dir / 'intake-gui', '--version'], text=True)
if manifest['build_id'] not in version:
raise ValueError('GUI build ID differs from release manifest')
required = ['networkmanager', 'curl', 'ca-certificates', 'python', 'usbutils', 'smartmontools', 'alsa-utils', 'ffmpeg', 'stress-ng', 'xdg-utils', 'firefox']
packages = (profile / 'packages.x86_64').read_text().splitlines()
for package in required:
if package not in packages:
raise ValueError(f'Missing live package: {package}')
for file in ['usr/local/bin/intake-login', 'usr/local/bin/intake-prepare', 'etc/systemd/system/getty@tty1.service.d/autologin.conf', 'etc/systemd/system/multi-user.target.wants/NetworkManager.service']:
p = root / file
if not p.exists() and not p.is_symlink():
raise ValueError(f'Missing live startup file: {file}')
print(f"Profile verified: {manifest['build_id']}")
print(f"Release verified: {manifest['build_id']}")
def main():
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--source-id', action='store_true')
group.add_argument('--create', type=Path)
group.add_argument('--verify-profile', type=Path)
group.add_argument('--verify-release', type=Path)
args = parser.parse_args()
if args.source_id: print(source_id())
elif args.create: create(args.create)
else: verify(args.verify_profile)
else: verify(args.verify_release)
if __name__ == '__main__': main()