Adds the Blueforce Field OS provisioning stack on top of the existing fleet architecture, without rewriting the working systems (installer, diagnostics, Ansible, WireGuard/SSH/xRDP/RustDesk, Docker policies, monitoring, Docusaurus). ISO build (the previous builder was a skeleton that produced no medium): - boot layout is read from the base ISO at build time via `xorriso -report_el_torito as_mkisofs` and rebuilt with `-as mkisofs`; `-boot_image any replay` cannot work on Ubuntu live ISOs because the GRUB2 MBR lives in the source image's first 16 sectors and the EFI system partition is an appended partition, neither of which is a file in the ISO9660 tree - preflight now asserts El Torito boot metadata positively: xorriso exits 0 on a non-ISO input, which previously let an arbitrary file pass --check - artifact produced and verified: dist/Blueforce-Field-OS-1.0.0-amd64.iso, 6 481 917 952 bytes, SHA256 cb0cc56f...d431296, BIOS+UEFI entries preserved, /autoinstall.yaml at the media root, casper/minimal.squashfs byte-identical to the base - recorded baseline deviation: the verified medium on the build host is the Desktop flavour, so the manifest carries a baseline_deviation block instead of hiding it; Server remains one flag away Offline and identity: - offline APT contract with concrete package requirements, pin format and an audit table; builds fail closed while pins are UNPINNED - autoinstall keeps interactive-sections: [storage]: the operator confirms the target disk, and no automatic wipe is ever performed - firstboot terminal wizard for the 8-digit dealer id, three-state lifecycle (PROVISIONED_OFFLINE -> ENROLLED -> READY) with fail-closed gates, so a missing internet link or WireGuard handshake cannot fail provisioning but can never produce a false READY Fleet and migration: - five read-only Ansible playbooks for Field OS version, provisioning, enrollment, remote channels and offline readiness, all wave-scoped with a separate production override - Windows pre-migration inventory and data export extended to the full brief scope, read-only, never collecting credentials Tests: tests/run-all.sh -> 8/8 pass, exit 0. Boot acceptance (UEFI, Legacy BIOS, Secure Boot), air-gapped install, real enrollment endpoint and Windows rollback remain LAB work and are listed in IMPLEMENTATION-REPORT.md.
163 lines
9.7 KiB
Bash
Executable file
163 lines
9.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Validate migration tooling safety without PowerShell, QEMU, disks, or network access.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
FAIL=0
|
|
|
|
fail() { printf 'FAIL: %s\n' "$*" >&2; FAIL=1; }
|
|
pass() { printf 'OK: %s\n' "$*"; }
|
|
require_file() { [[ -f "$1" ]] && pass "present: ${1#$REPO_ROOT/}" || fail "missing: ${1#$REPO_ROOT/}"; }
|
|
|
|
WINDOWS_DIR="$REPO_ROOT/scripts/migration/windows"
|
|
ANYDESK="$REPO_ROOT/scripts/install/optional/anydesk/install-anydesk.sh"
|
|
HW_CHECK="$REPO_ROOT/scripts/diagnostics/bf-live-hw-check"
|
|
QEMU_PLAN="$REPO_ROOT/tests/qemu-field-os-test-plan.md"
|
|
|
|
require_file "$WINDOWS_DIR/BF-WindowsPreMigrationInventory.ps1"
|
|
require_file "$WINDOWS_DIR/BF-WindowsDataExport.ps1"
|
|
require_file "$ANYDESK"
|
|
require_file "$HW_CHECK"
|
|
require_file "$QEMU_PLAN"
|
|
|
|
for script in "$WINDOWS_DIR"/*.ps1; do
|
|
[[ -f "$script" ]] || continue
|
|
grep -Fq "[ValidatePattern('^[0-9]{8}$')]" "$script" || fail "8-digit DealerId validation missing: ${script#$REPO_ROOT/}"
|
|
grep -Fq '[string]$OutputDirectory' "$script" || fail "OutputDirectory parameter missing: ${script#$REPO_ROOT/}"
|
|
grep -q 'BF-$DealerId-windows-inventory' "$script" || fail "required BF output basename missing: ${script#$REPO_ROOT/}"
|
|
grep -qi 'ConvertTo-Json' "$script" || fail "JSON output missing: ${script#$REPO_ROOT/}"
|
|
grep -q 'Markdown' "$script" || fail "Markdown output missing: ${script#$REPO_ROOT/}"
|
|
# Turkish report headers need a UTF-8 BOM so Windows PowerShell 5.1 parses the file correctly.
|
|
[[ "$(head -c 3 "$script" | od -An -tx1 | tr -d ' \n')" == 'efbbbf' ]] || fail "UTF-8 BOM missing (Windows PowerShell 5.1 compatibility): ${script#$REPO_ROOT/}"
|
|
done
|
|
|
|
grep -q -- '-NoSoftware' "$WINDOWS_DIR/BF-WindowsPreMigrationInventory.ps1" || fail 'inventory must support -NoSoftware'
|
|
grep -q -- '-NoSoftware' "$WINDOWS_DIR/BF-WindowsDataExport.ps1" || fail 'export must support -NoSoftware'
|
|
|
|
# Migration scripts are inventory/export only: no credential APIs, writes, service/network/registry mutation, or downloads.
|
|
if grep -RniE '(Get-Credential|CredentialManager|Windows\.Security\.Credentials|cmdkey|vaultcmd|ConvertTo-SecureString|Read-Host.*password)' "$WINDOWS_DIR"; then
|
|
fail 'credential API or password collection found in Windows migration scripts'
|
|
else
|
|
pass 'no credential API or password collection in Windows scripts'
|
|
fi
|
|
if grep -RniE '(Set-Service|Start-Service|Stop-Service|Restart-Service|New-Net|Set-Net|Remove-Net|Disable-Net|Enable-Net|netsh|Set-ItemProperty|New-ItemProperty|Remove-ItemProperty|reg(\.exe)?[[:space:]]+(add|delete)|Invoke-WebRequest|curl|wget|Start-BitsTransfer)' "$WINDOWS_DIR"; then
|
|
fail 'forbidden mutation or download primitive found in Windows migration scripts'
|
|
else
|
|
pass 'Windows scripts have no mutation or download primitive'
|
|
fi
|
|
|
|
# The export manifest must stay metadata-only: no copy, move, archive or delete primitive.
|
|
if grep -RniE '(Copy-Item|Move-Item|Remove-Item|Compress-Archive|Expand-Archive|robocopy|xcopy)' "$WINDOWS_DIR"/*.ps1; then
|
|
fail 'copy/move/archive/delete primitive found in Windows migration scripts'
|
|
else
|
|
pass 'Windows scripts have no copy/move/archive/delete primitive'
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Migration scope: every section the migration brief requires must be present,
|
|
# still read-only, and still named from the dealer number (not the hostname).
|
|
# ---------------------------------------------------------------------------
|
|
INVENTORY="$WINDOWS_DIR/BF-WindowsPreMigrationInventory.ps1"
|
|
EXPORT="$WINDOWS_DIR/BF-WindowsDataExport.ps1"
|
|
WINDOWS_README="$WINDOWS_DIR/README.md"
|
|
MIGRATION_POINTER="$REPO_ROOT/migration/README.md"
|
|
|
|
require_file "$INVENTORY"
|
|
require_file "$EXPORT"
|
|
require_file "$WINDOWS_README"
|
|
require_file "$MIGRATION_POINTER"
|
|
|
|
require_marker() { grep -Fq "$2" "$1" || fail "missing '$2' in ${1#$REPO_ROOT/}"; }
|
|
|
|
# Network scope: IP config, DNS, gateway(s), routes, profiles, proxy.
|
|
for marker in 'Win32_NetworkAdapterConfiguration' 'DNSServerSearchOrder' 'DefaultIPGateway' \
|
|
'Win32_IP4RouteTable' 'default_gateways' 'network_profiles' 'ProxyServer'; do
|
|
require_marker "$INVENTORY" "$marker"
|
|
done
|
|
# Software scope: installed software, services, scheduled tasks, drivers, Windows version.
|
|
for marker in 'installed_software' 'Win32_Service' 'Get-ScheduledTask' 'Win32_PnPSignedDriver' \
|
|
"'DisplayVersion'" 'Win32_QuickFixEngineering' "'UBR'"; do
|
|
require_marker "$INVENTORY" "$marker"
|
|
done
|
|
# Device scope: COM/serial ports, USB hardware IDs.
|
|
for marker in 'Win32_SerialPort' 'SERIALCOMM' 'hardware_id' 'Win32_USBController'; do
|
|
require_marker "$INVENTORY" "$marker"
|
|
done
|
|
# Migration-critical applications: Docker, VPN clients, remote-access IDs, MEG paths.
|
|
for marker in 'com.docker.service' 'WireGuard' 'OpenVPN' 'conf.dpapi' '.ovpn' \
|
|
'rustdesk_id' 'anydesk_id' 'Get-MegSnapshot' 'registry_paths'; do
|
|
require_marker "$INVENTORY" "$marker"
|
|
done
|
|
# Identity: dealer-number naming, existing hostname recorded, target hostname.
|
|
for marker in 'current_hostname' 'target_hostname' 'dealer_id_known' 'READ-ONLY CONTRACT' \
|
|
'DealerIdUnknown' 'WARNING: dealer number unknown' 'executed_external_tools'; do
|
|
require_marker "$INVENTORY" "$marker"
|
|
done
|
|
# Secrets stay out of scope, including the RustDesk private key file.
|
|
for marker in 'id_ed25519' 'private keys' 'Windows ProductId'; do
|
|
require_marker "$INVENTORY" "$marker"
|
|
done
|
|
grep -Fq -- '-Check' "$INVENTORY" || fail 'inventory must support -Check (read-only dry run)'
|
|
grep -Fq -- '-Check' "$EXPORT" || fail 'export must support -Check (read-only dry run)'
|
|
|
|
# Export manifest stays metadata-only and review-gated.
|
|
for marker in 'windows-pre-migration-data-export-manifest' 'candidate_folders' 'removable_media_roots' \
|
|
'mapped_network_drives' 'Win32_NetworkConnection' 'Win32_Printer' 'meg_data_directories' \
|
|
'copies_performed' 'manual-copy-after-owner-approval' 'READ-ONLY CONTRACT' 'DealerIdUnknown'; do
|
|
require_marker "$EXPORT" "$marker"
|
|
done
|
|
|
|
# Requested-name -> repo-name deviation must stay documented (docs/32 B-6).
|
|
for marker in 'bf-win-inventory.ps1' 'bf-win-network-export.ps1' 'bf-win-software-export.ps1' \
|
|
'bf-win-device-export.ps1' 'BF-WindowsPreMigrationInventory.ps1' 'BF-WindowsDataExport.ps1' \
|
|
'ExecutionPolicy' 'scripts/migration/windows' 'read-only'; do
|
|
require_marker "$WINDOWS_README" "$marker"
|
|
done
|
|
grep -Fq 'scripts/migration/windows' "$MIGRATION_POINTER" || fail 'migration/README.md must point at scripts/migration/windows'
|
|
grep -Fq 'docs/32' "$MIGRATION_POINTER" || fail 'migration/README.md must reference the docs/32 audit entry'
|
|
grep -Fiq 'migration/windows' "$MIGRATION_POINTER" || fail 'migration/README.md must name the requested migration/windows path'
|
|
pass 'migration scope, naming and location deviations are documented and asserted'
|
|
|
|
[[ "$(head -n 1 "$ANYDESK")" == '#!/usr/bin/env bash' ]] || fail 'AnyDesk skeleton requires Bash shebang'
|
|
grep -q '^set -euo pipefail$' "$ANYDESK" || fail 'AnyDesk skeleton requires strict mode'
|
|
bash -n "$ANYDESK" && pass 'AnyDesk skeleton parses' || fail 'AnyDesk skeleton parse error'
|
|
grep -q 'ANYDESK_LICENSE_CONFIRMED=true' "$ANYDESK" || fail 'AnyDesk license confirmation gate missing'
|
|
if grep -niE '(curl|wget|apt(-get)?[[:space:]].*install|dnf[[:space:]].*install|zypper[[:space:]].*install|license(_|[[:space:]])?(key|file)|[A-Z0-9]{4}(-[A-Z0-9]{4}){3,})' "$ANYDESK"; then
|
|
fail 'AnyDesk skeleton must not download, install, or embed a license'
|
|
else
|
|
pass 'AnyDesk skeleton has no download, install, or embedded license'
|
|
fi
|
|
|
|
grep -q '^#!/usr/bin/env bash' "$HW_CHECK" || fail 'hardware check requires Bash shebang'
|
|
grep -q '^set -euo pipefail$' "$HW_CHECK" || fail 'hardware check requires strict mode'
|
|
bash -n "$HW_CHECK" && pass 'hardware check parses' || fail 'hardware check parse error'
|
|
grep -q -- '--json' "$HW_CHECK" || fail 'hardware check --json missing'
|
|
grep -q -- '--markdown' "$HW_CHECK" || fail 'hardware check --markdown missing'
|
|
for verdict in SUPPORTED WARNING BLOCKED; do grep -q "$verdict" "$HW_CHECK" || fail "hardware verdict missing: $verdict"; done
|
|
# Every check the migration gate relies on must be present in the JSON payload.
|
|
for hwcheck in architecture ram disk disk_type smart nic nic_link serial usb boot_mode secure_boot tpm virt cpu_virt; do
|
|
grep -Fq "\"name\":\"$hwcheck\"" "$HW_CHECK" || fail "hardware check missing: $hwcheck"
|
|
done
|
|
# Verdict thresholds must be documented in the output, not just implied by code.
|
|
grep -Fq '| Kontrol | BLOCKED | WARNING | SUPPORTED |' "$HW_CHECK" || fail 'hardware check threshold table missing from markdown output'
|
|
for threshold in '68719476736' '2048' '4096' '64 GiB' '4096 MiB' 'vmx/svm'; do
|
|
grep -Fq -- "$threshold" "$HW_CHECK" || fail "hardware threshold not documented: $threshold"
|
|
done
|
|
pass 'hardware check covers arch/ram/disk/media/smart/nic/link/serial/usb/boot/secure-boot/tpm/virt and documents thresholds'
|
|
if grep -niE '(apt(-get)?[[:space:]].*install|dnf[[:space:]].*install|zypper[[:space:]].*install|mkfs|wipefs|dd[[:space:]].*of=|rm[[:space:]]+-rf|systemctl[[:space:]]+(start|stop|restart|enable|disable)|ip[[:space:]]+(link|addr|route)[[:space:]]+(add|del|set)|nmcli[[:space:]].*(up|down|modify))' "$HW_CHECK"; then
|
|
fail 'hardware check must not install, wipe, alter services, or alter networking'
|
|
else
|
|
pass 'hardware check is non-mutating by static pattern'
|
|
fi
|
|
|
|
for test_id in $(seq 1 18); do
|
|
grep -q "T${test_id}" "$QEMU_PLAN" || fail "QEMU/LAB plan missing T${test_id}"
|
|
done
|
|
grep -qiE 'CI.*statik|statik.*CI' "$QEMU_PLAN" || fail 'QEMU/LAB plan must distinguish CI/static coverage'
|
|
grep -qiE 'QEMU|KVM' "$QEMU_PLAN" || fail 'QEMU/LAB plan must cover QEMU/KVM'
|
|
|
|
if [[ "$FAIL" -ne 0 ]]; then
|
|
printf 'check-migration-static: FAILED\n' >&2
|
|
exit 1
|
|
fi
|
|
printf 'check-migration-static: all passed\n'
|