Live-USB durch eigenständige Anwendung ersetzen
Some checks failed
Tests / test (push) Has been cancelled
Some checks failed
Tests / test (push) Has been cancelled
This commit is contained in:
@@ -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."
|
||||
@@ -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"
|
||||
|
||||
@@ -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" "\$@"
|
||||
|
||||
@@ -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"
|
||||
@@ -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.'
|
||||
@@ -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"
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user