Eliminate USB requirement for WinPE PXE boot, add image upload script

- Add startnet.cmd: FlatSetupLoader.exe + Boot.tag/Media.tag eliminates
  physical USB requirement for WinPE PXE deployment
- Add Upload-Image.ps1: PowerShell script to robocopy MCL cached images
  to PXE server via SMB (Deploy, Tools, Sources)
- Add gea-shopfloor-mce image type across playbook, webapp, startnet
- Change webapp import to move (not copy) for upload sources to save disk
- Add Samba symlink following config for shared image directories
- Add Media.tag creation task in playbook for drive detection
- Update prepare-boot-tools.sh with Blancco config/initramfs patching
- Add grub-efi-amd64-bin to download-packages.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-12 16:40:27 -05:00
parent f4c158a5ac
commit 1a5c4f7124
7 changed files with 696 additions and 38 deletions

164
Upload-Image.ps1 Normal file
View File

@@ -0,0 +1,164 @@
#
# Upload-Image.ps1 — Copy Media Creator Lite cached image to the PXE server
#
# Copies Deploy/, Tools/, and Sources (from Boot/Sources.zip) to the
# PXE server's image-upload share using robocopy with authentication.
#
# Usage:
# .\Upload-Image.ps1 (uses default MCL cache path)
# .\Upload-Image.ps1 -CachePath "D:\MCL\Cache" (custom cache location)
# .\Upload-Image.ps1 -Server 10.9.100.1 (custom server IP)
#
# After upload, use the PXE webapp (http://10.9.100.1:9009) to import
# the uploaded content into the desired image type.
#
param(
[string]$CachePath = "C:\ProgramData\GEAerospace\MediaCreator\Cache",
[string]$Server = "10.9.100.1",
[string]$User = "pxe-upload",
[string]$Pass = "pxe",
[switch]$IncludeDell10
)
$Share = "\\$Server\image-upload"
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " PXE Server Image Uploader" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# --- Validate source paths ---
$DeployPath = Join-Path $CachePath "Deploy"
$ToolsPath = Join-Path (Split-Path $CachePath -Parent) "Tools"
# Tools is a sibling of Cache in the MCL directory structure
if (-not (Test-Path $ToolsPath -PathType Container)) {
# Fallback: try Tools inside CachePath parent's parent
$ToolsPath = Join-Path (Split-Path (Split-Path $CachePath -Parent) -Parent) "Tools"
}
if (-not (Test-Path $ToolsPath -PathType Container)) {
$ToolsPath = "C:\ProgramData\GEAerospace\MediaCreator\Tools"
}
$SourcesZip = Join-Path $CachePath "Boot\Sources.zip"
if (-not (Test-Path $DeployPath -PathType Container)) {
Write-Host "ERROR: Deploy directory not found at $DeployPath" -ForegroundColor Red
Write-Host " Provide the correct cache path: .\Upload-Image.ps1 -CachePath ""D:\Path\To\Cache""" -ForegroundColor Yellow
exit 1
}
Write-Host " Cache Path: $CachePath"
Write-Host " Deploy: $DeployPath" -ForegroundColor $(if (Test-Path $DeployPath) { "Green" } else { "Red" })
Write-Host " Tools: $ToolsPath" -ForegroundColor $(if (Test-Path $ToolsPath) { "Green" } else { "Yellow" })
Write-Host " Sources.zip: $SourcesZip" -ForegroundColor $(if (Test-Path $SourcesZip) { "Green" } else { "Yellow" })
Write-Host " Server: $Server"
if (-not $IncludeDell10) {
Write-Host " Excluding: Dell_10 drivers (use -IncludeDell10 to include)" -ForegroundColor Gray
}
Write-Host ""
# --- Connect to SMB share ---
Write-Host "Connecting to $Share ..." -ForegroundColor Gray
# Remove any stale connection
net use $Share /delete 2>$null | Out-Null
$netResult = net use $Share /user:$User $Pass 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Could not connect to $Share" -ForegroundColor Red
Write-Host $netResult -ForegroundColor Red
Write-Host ""
Write-Host "Make sure:" -ForegroundColor Yellow
Write-Host " - The PXE server is running at $Server" -ForegroundColor Yellow
Write-Host " - This PC is on the 10.9.100.x network" -ForegroundColor Yellow
Write-Host " - Samba is running on the PXE server" -ForegroundColor Yellow
exit 1
}
Write-Host "Connected." -ForegroundColor Green
Write-Host ""
$failed = $false
# --- Step 1: Copy Deploy/ ---
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[1/3] Copying Deploy/ ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
$robocopyArgs = @($DeployPath, "$Share\Deploy", "/E", "/R:3", "/W:5", "/NP", "/ETA")
if (-not $IncludeDell10) {
$robocopyArgs += @("/XD", "Dell_10")
}
& robocopy @robocopyArgs
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Deploy copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
# --- Step 2: Copy Tools/ ---
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[2/3] Copying Tools/ ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
if (Test-Path $ToolsPath -PathType Container) {
robocopy $ToolsPath "$Share\Tools" /E /R:3 /W:5 /NP /ETA
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Tools copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
} else {
Write-Host "SKIPPED: Tools directory not found at $ToolsPath" -ForegroundColor Yellow
}
# --- Step 3: Extract and copy Sources/ ---
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "[3/3] Extracting and copying Sources/ ..." -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
if (Test-Path $SourcesZip) {
$TempExtract = Join-Path $env:TEMP "SourcesExtract"
Write-Host " Extracting Sources.zip..."
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
Expand-Archive $SourcesZip -DestinationPath $TempExtract -Force
# Handle nested Sources folder (zip may contain Sources/ at root)
$TempSources = $TempExtract
if ((Test-Path (Join-Path $TempExtract "Sources")) -and -not (Test-Path (Join-Path $TempExtract "Diskpart"))) {
$TempSources = Join-Path $TempExtract "Sources"
}
robocopy $TempSources "$Share\Sources" /E /R:3 /W:5 /NP /ETA
if ($LASTEXITCODE -ge 8) {
Write-Host "ERROR: Sources copy failed (exit code $LASTEXITCODE)" -ForegroundColor Red
$failed = $true
}
# Clean up temp extraction
Remove-Item -Recurse -Force $TempExtract -ErrorAction SilentlyContinue
} else {
Write-Host "SKIPPED: Sources.zip not found at $SourcesZip" -ForegroundColor Yellow
}
# --- Disconnect ---
net use $Share /delete 2>$null | Out-Null
# --- Summary ---
Write-Host ""
if ($failed) {
Write-Host "========================================" -ForegroundColor Red
Write-Host " Upload completed with errors." -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Red
} else {
Write-Host "========================================" -ForegroundColor Green
Write-Host " Upload complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
}
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host " 1. Open the PXE webapp: http://$Server`:9009" -ForegroundColor White
Write-Host " 2. Go to Image Import" -ForegroundColor White
Write-Host " 3. Select source 'image-upload' and target image type" -ForegroundColor White
Write-Host " 4. Click Import" -ForegroundColor White
Write-Host ""

View File

@@ -27,6 +27,8 @@ PLAYBOOK_PACKAGES=(
cron
wimtools
p7zip-full
grub-efi-amd64-bin
grub-common
)
# Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.)

View File

@@ -37,6 +37,7 @@
- gea-standard
- gea-engineer
- gea-shopfloor
- gea-shopfloor-mce
- ge-standard
- ge-engineer
- ge-shopfloor-lockdown
@@ -95,6 +96,33 @@
debug:
msg: "Using {{ pxe_iface }} for DHCP/TFTP"
- name: "Expand root partition and filesystem to use full disk"
args:
executable: /bin/bash
shell: |
# Find the root LV device
ROOT_DEV=$(findmnt -n -o SOURCE /)
ROOT_DISK=$(lsblk -n -o PKNAME $(readlink -f "$ROOT_DEV") | tail -1)
if [ -z "$ROOT_DISK" ]; then
echo "Could not determine root disk, skipping"
exit 0
fi
# Find the partition number for the LVM PV
PV_PART=$(pvs --noheadings -o pv_name | tr -d ' ' | head -1)
if [ -z "$PV_PART" ]; then
echo "No LVM PV found, skipping"
exit 0
fi
PART_NUM=$(echo "$PV_PART" | grep -o '[0-9]*$')
echo "Expanding /dev/${ROOT_DISK} partition ${PART_NUM} (${PV_PART})..."
growpart "/dev/${ROOT_DISK}" "${PART_NUM}" 2>&1 || true
pvresize "$PV_PART" 2>&1
lvextend -l +100%FREE "$ROOT_DEV" 2>&1 || true
resize2fs "$ROOT_DEV" 2>&1
echo "Disk: $(df -h / | tail -1)"
register: disk_expand
changed_when: "'CHANGED' in disk_expand.stdout or 'resized' in disk_expand.stdout"
- name: "Configure dnsmasq for DHCP and TFTP"
copy:
dest: /etc/dnsmasq.conf
@@ -158,9 +186,9 @@
menu GE Aerospace PXE Boot Menu
item --gap -- ---- Windows Deployment ----
item winpe Windows PE (Image Deployment)
item --gap -- ---- Utilities ----
item clonezilla Clonezilla Live (Disk Imaging)
item --gap -- ---- Utilities (Secure Boot must be DISABLED) ----
item blancco Blancco Drive Eraser
item clonezilla Clonezilla Live (Disk Imaging)
item memtest Memtest86+ (Memory Diagnostics)
item --gap -- ----
item reboot Reboot
@@ -168,6 +196,13 @@
choose --default winpe --timeout 30000 target && goto ${target}
:winpe
echo
echo Windows deployment requires Secure Boot to be ENABLED.
echo If you disabled it for Blancco/Clonezilla, re-enable it now.
echo
prompt --timeout 5000 Press any key to continue (auto-boot in 5s)... && goto winpe_boot || goto winpe_boot
:winpe_boot
kernel http://${server}/win11/wimboot gui
initrd http://${server}/win11/EFI/Microsoft/Boot/boot.stl EFI/Microsoft/Boot/Boot.stl
initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD
@@ -178,20 +213,34 @@
:clonezilla
set base http://${server}/clonezilla
kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs
kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs || goto secureboot_warn
initrd ${base}/initrd.img
boot
:blancco
set bbase http://${server}/blancco
kernel ${bbase}/vmlinuz-bde-linux archisobasedir=arch archiso_http_srv=http://${server}/blancco/ copytoram=y cow_spacesize=50% memtest=00 vmalloc=400M ip=dhcp quiet nomodeset libata.allow_tpm=1
initrd ${bbase}/intel-ucode.img ${bbase}/amd-ucode.img ${bbase}/config.img ${bbase}/initramfs-bde-linux.img
boot
chain http://${server}/blancco/grubx64.efi || goto secureboot_warn
:memtest
kernel http://${server}/memtest/memtest.efi
kernel http://${server}/memtest/memtest.efi || goto secureboot_warn
boot
:secureboot_warn
echo
echo ======================================================
echo This option requires Secure Boot to be DISABLED.
echo
echo 1. Reboot this machine
echo 2. Press F2 / Del to enter BIOS Setup
echo 3. Disable Secure Boot
echo 4. Save and exit BIOS
echo 5. PXE boot again and select this option
echo
echo Re-enable Secure Boot after completing the task.
echo ======================================================
echo
prompt Press any key to return to menu...
goto menu
:reboot
reboot
@@ -248,6 +297,25 @@
state: directory
mode: '0777'
- name: "Create image upload staging directory"
file:
path: /home/pxe/image-upload
state: directory
mode: '0777'
owner: pxe
group: pxe
- name: "Enable Samba symlink following (shared image dirs)"
blockinfile:
path: /etc/samba/smb.conf
backup: yes
marker: "# {mark} MANAGED - GLOBAL SYMLINKS"
insertafter: '\[global\]'
block: |
follow symlinks = yes
wide links = yes
unix extensions = no
- name: "Configure Samba shares"
blockinfile:
path: /etc/samba/smb.conf
@@ -257,22 +325,48 @@
path = {{ samba_share }}
browseable = yes
read only = no
guest ok = yes
guest ok = no
valid users = pxe-upload
force user = root
[clonezilla]
path = /srv/samba/clonezilla
browseable = yes
read only = no
guest ok = yes
guest ok = no
valid users = pxe-upload
force user = root
comment = Clonezilla backup images
[blancco-reports]
path = /srv/samba/blancco-reports
browseable = yes
read only = no
guest ok = yes
guest ok = no
valid users = pxe-upload blancco
force user = root
comment = Blancco Drive Eraser reports
[image-upload]
path = /home/pxe/image-upload
browseable = yes
read only = no
guest ok = no
valid users = pxe-upload
force user = pxe
force group = pxe
comment = PXE image upload staging area
- name: "Create Samba users (pxe-upload and blancco)"
shell: |
id pxe-upload >/dev/null 2>&1 || useradd -M -s /usr/sbin/nologin pxe-upload
echo -e 'pxe\npxe' | smbpasswd -a pxe-upload -s
id blancco >/dev/null 2>&1 || useradd -M -s /usr/sbin/nologin blancco
echo -e 'blancco\nblancco' | smbpasswd -a blancco -s
args:
executable: /bin/bash
changed_when: false
- name: "Create image-type top-level directories"
file:
path: "{{ samba_share }}/{{ item }}"
@@ -289,6 +383,14 @@
- "{{ image_types }}"
- "{{ deploy_subdirs }}"
- name: "Create Media.tag for FlatSetupLoader.exe drive detection"
copy:
content: ""
dest: "{{ samba_share }}/{{ item }}/Deploy/Control/Media.tag"
mode: '0644'
force: no
loop: "{{ image_types }}"
- name: "Copy WinPE & boot files from USB (skipped if not present)"
copy:
src: "{{ usb_root }}/{{ item.src }}"
@@ -303,6 +405,20 @@
- { src: "boot.wim", dest: "sources/boot.wim" }
ignore_errors: yes
- name: "Inject startnet.cmd into boot.wim (virtual BOOT/MEDIA volumes)"
shell: |
WIM="{{ web_root }}/win11/sources/boot.wim"
STARTNET="{{ usb_mount }}/startnet.cmd"
if [ -f "$WIM" ] && [ -f "$STARTNET" ]; then
echo "add $STARTNET /Windows/System32/startnet.cmd" | wimupdate "$WIM" 1
echo "Updated startnet.cmd in boot.wim"
else
echo "Skipped: boot.wim or startnet.cmd not found"
fi
args:
executable: /bin/bash
ignore_errors: yes
- name: "Copy iPXE binaries from USB (skipped if not present)"
copy:
src: "{{ usb_root }}/{{ item }}"
@@ -320,6 +436,25 @@
- blancco
- memtest
- name: "Create TFTP blancco directory for GRUB boot"
file:
path: "{{ tftp_dir }}/blancco"
state: directory
mode: '0755'
- name: "Symlink Blancco boot files to TFTP (GRUB loads via TFTP)"
file:
src: "{{ web_root }}/blancco/{{ item }}"
dest: "{{ tftp_dir }}/blancco/{{ item }}"
state: link
force: yes
loop:
- vmlinuz-bde-linux
- intel-ucode.img
- amd-ucode.img
- config.img
- initramfs-bde-linux.img
- name: "Check for WinPE deployment content on USB"
stat:
path: "{{ usb_root }}/images"

105
playbook/startnet.cmd Normal file
View File

@@ -0,0 +1,105 @@
@echo off
echo Please wait while 'WinPE' is being processed. This may take a few seconds.
wpeinit
powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
:menu
cls
echo.
echo ========================================
echo WinPE Setup Menu
echo ========================================
echo.
echo Please select an option:
echo.
echo 1. GEA Standard
echo 2. GEA Engineer
echo 3. GEA Shopfloor
echo 4. GEA Shopfloor MCE
echo 5. GE Standard
echo 6. GE Engineer
echo 7. GE Shopfloor Lockdown
echo 8. GE Shopfloor MCE
echo.
echo ========================================
echo.
set /p choice=Enter your choice (1-8):
echo. > X:\Boot.tag
if "%choice%"=="1" goto gea-standard
if "%choice%"=="2" goto gea-engineer
if "%choice%"=="3" goto gea-shopfloor
if "%choice%"=="4" goto gea-shopfloor-mce
if "%choice%"=="5" goto ge-standard
if "%choice%"=="6" goto ge-engineer
if "%choice%"=="7" goto ge-shopfloor-lockdown
if "%choice%"=="8" goto ge-shopfloor-mce
echo Invalid choice. Please try again.
pause
goto menu
:gea-standard
echo.
echo Starting GEA Standard setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-standard /persistent:no
goto end
:gea-engineer
echo.
echo Starting GEA Engineer setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-engineer /persistent:no
goto end
:gea-shopfloor
echo.
echo Starting GEA Shopfloor setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor /persistent:no
goto end
:gea-shopfloor-mce
echo.
echo Starting GEA Shopfloor MCE setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor-mce /persistent:no
goto end
:ge-standard
echo.
echo Starting GE Standard setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-standard /persistent:no
goto end
:ge-engineer
echo.
echo Starting GE Engineer setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-engineer /persistent:no
goto end
:ge-shopfloor-lockdown
echo.
echo Starting GE Shopfloor Lockdown setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-shopfloor-lockdown /persistent:no
goto end
:ge-shopfloor-mce
echo.
echo Starting GE Shopfloor MCE setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-shopfloor-mce /persistent:no
goto end
:end

View File

@@ -105,7 +105,10 @@ if [ -n "$BLANCCO_ISO" ] && [ -f "$BLANCCO_ISO" ]; then
cp "$TMPDIR/arch/x86_64/airootfs.sfs" "$OUT_DIR/blancco/arch/x86_64/"
echo " Extracted Blancco boot files."
# Patch config.img to auto-save reports to PXE server Samba share
# --- Patch config.img (size-preserving) ---
# config.img is a CPIO archive containing preferences.xml (padded to 32768 bytes).
# The CPIO itself must remain exactly 194560 bytes (380 x 512-byte blocks).
# We use Python for byte-level replacement to preserve exact sizes.
if [ -f "$OUT_DIR/blancco/config.img" ]; then
echo " Patching config.img for network report storage..."
CFGTMP=$(mktemp -d)
@@ -113,19 +116,149 @@ if [ -n "$BLANCCO_ISO" ] && [ -f "$BLANCCO_ISO" ]; then
cpio -id < "$OUT_DIR/blancco/config.img" 2>/dev/null
if [ -f "$CFGTMP/preferences.xml" ]; then
# Set network share to PXE server's blancco-reports Samba share
sed -i 's|<hostname></hostname>|<hostname>10.9.100.1</hostname>|' "$CFGTMP/preferences.xml"
sed -i 's|<path></path>|<path>blancco-reports</path>|' "$CFGTMP/preferences.xml"
# Enable auto-backup of reports to the network share
sed -i 's|<auto_backup>false</auto_backup>|<auto_backup>true</auto_backup>|' "$CFGTMP/preferences.xml"
ORIG_SIZE=$(stat -c%s "$CFGTMP/preferences.xml")
# Repack config.img
ls -1 | cpio -o -H newc > "$OUT_DIR/blancco/config.img" 2>/dev/null
echo " Reports will auto-save to \\\\10.9.100.1\\blancco-reports"
python3 << 'PYEOF'
import sys
with open("preferences.xml", "rb") as f:
data = f.read()
orig_size = len(data)
# Set SMB share credentials and path
data = data.replace(
b'<username encrypted="false"></username>',
b'<username encrypted="false">blancco</username>'
)
data = data.replace(
b'<password encrypted="false"></password>',
b'<password encrypted="false">blancco</password>'
)
data = data.replace(
b'<hostname></hostname>',
b'<hostname>10.9.100.1</hostname>'
)
data = data.replace(
b'<path></path>',
b'<path>blancco-reports</path>'
)
# Enable auto-backup
data = data.replace(
b'<auto_backup>false</auto_backup>',
b'<auto_backup>true</auto_backup>'
)
# Enable bootable report
data = data.replace(
b'<bootable_report>\n <enabled>false</enabled>\n </bootable_report>',
b'<bootable_report>\n <enabled>true</enabled>\n </bootable_report>'
)
# Maintain exact file size by trimming trailing padding/whitespace
diff = len(data) - orig_size
if diff > 0:
# The file has trailing whitespace/padding before the final XML closing tags
# Trim from the padding area (spaces before closing comment or end of file)
end_pos = data.rfind(b'<!-- ')
if end_pos > 0:
comment_end = data.find(b' -->', end_pos)
if comment_end > 0:
data = data[:comment_end - diff] + data[comment_end:]
if len(data) > orig_size:
# Fallback: trim trailing whitespace
data = data.rstrip()
data = data + b'\n' * (orig_size - len(data))
elif diff < 0:
# Pad with spaces to maintain size
data = data[:-1] + b' ' * (-diff) + data[-1:]
if len(data) != orig_size:
print(f" WARNING: Size mismatch ({len(data)} vs {orig_size}), padding to match")
if len(data) > orig_size:
data = data[:orig_size]
else:
data = data + b'\x00' * (orig_size - len(data))
with open("preferences.xml", "wb") as f:
f.write(data)
print(f" preferences.xml: {orig_size} bytes (preserved)")
PYEOF
# Repack CPIO with exact 512-byte block alignment (194560 bytes)
ls -1 "$CFGTMP" | (cd "$CFGTMP" && cpio -o -H newc 2>/dev/null) | \
dd bs=512 conv=sync 2>/dev/null > "$OUT_DIR/blancco/config.img"
echo " Reports: SMB blancco@10.9.100.1/blancco-reports, bootable report enabled"
fi
cd "$SCRIPT_DIR"
rm -rf "$CFGTMP"
fi
# --- Patch initramfs to keep network interfaces up after copytoram ---
# Blancco uses copytoram=y which triggers archiso_pxe_common latehook to
# flush all network interfaces. Binary-patch the check from "y" to "N" so
# the condition never matches. IMPORTANT: full extract/repack BREAKS booting.
echo " Patching initramfs to preserve network after copytoram..."
python3 << PYEOF
import lzma, sys, os
initramfs = "$OUT_DIR/blancco/initramfs-bde-linux.img"
with open(initramfs, "rb") as f:
compressed = f.read()
# Decompress XZ stream
try:
raw = lzma.decompress(compressed)
except lzma.LZMAError:
print(" WARNING: Could not decompress initramfs (not XZ?), skipping patch")
sys.exit(0)
# Binary patch: change the copytoram check from "y" to "N"
old = b'"y" ]; then\n for curif in /sys/class/net'
new = b'"N" ]; then\n for curif in /sys/class/net'
if old not in raw:
# Try alternate pattern with different whitespace
old = b'"\${copytoram}" = "y"'
new = b'"\${copytoram}" = "N"'
if old in raw:
raw = raw.replace(old, new, 1)
# Recompress with same XZ settings as archiso
recompressed = lzma.compress(raw, format=lzma.FORMAT_XZ,
check=lzma.CHECK_CRC32,
preset=6)
with open(initramfs, "wb") as f:
f.write(recompressed)
print(" initramfs patched: copytoram network flush disabled")
else:
print(" WARNING: copytoram pattern not found in initramfs, skipping patch")
PYEOF
# --- Build GRUB EFI binary for Blancco chainload ---
# Broadcom iPXE can't pass initrd to Linux kernels in UEFI mode.
# Solution: iPXE chains to grubx64.efi, which loads kernel+initrd via TFTP.
GRUB_CFG="$SCRIPT_DIR/boot-tools/blancco/grub-blancco.cfg"
if [ -f "$GRUB_CFG" ]; then
if command -v grub-mkstandalone &>/dev/null; then
echo " Building grubx64.efi (GRUB chainload for Blancco)..."
grub-mkstandalone \
--format=x86_64-efi \
--output="$OUT_DIR/blancco/grubx64.efi" \
--modules="linux normal echo net efinet tftp chain sleep" \
"boot/grub/grub.cfg=$GRUB_CFG" 2>/dev/null
echo " Built grubx64.efi ($(du -h "$OUT_DIR/blancco/grubx64.efi" | cut -f1))"
else
echo " WARNING: grub-mkstandalone not found. Install grub-efi-amd64-bin:"
echo " sudo apt install grub-efi-amd64-bin grub-common"
echo " Then re-run this script to build grubx64.efi"
fi
else
echo " WARNING: grub-blancco.cfg not found at $GRUB_CFG"
fi
else
echo " Could not extract boot files from ISO."
fi

105
startnet-template.cmd Normal file
View File

@@ -0,0 +1,105 @@
@echo off
echo Please wait while 'WinPE' is being processed. This may take a few seconds.
wpeinit
powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
:menu
cls
echo.
echo ========================================
echo WinPE Setup Menu
echo ========================================
echo.
echo Please select an option:
echo.
echo 1. GEA Standard
echo 2. GEA Engineer
echo 3. GEA Shopfloor
echo 4. GEA Shopfloor MCE
echo 5. GE Standard
echo 6. GE Engineer
echo 7. GE Shopfloor Lockdown
echo 8. GE Shopfloor MCE
echo.
echo ========================================
echo.
set /p choice=Enter your choice (1-8):
echo. > X:\Boot.tag
if "%choice%"=="1" goto gea-standard
if "%choice%"=="2" goto gea-engineer
if "%choice%"=="3" goto gea-shopfloor
if "%choice%"=="4" goto gea-shopfloor-mce
if "%choice%"=="5" goto ge-standard
if "%choice%"=="6" goto ge-engineer
if "%choice%"=="7" goto ge-shopfloor-lockdown
if "%choice%"=="8" goto ge-shopfloor-mce
echo Invalid choice. Please try again.
pause
goto menu
:gea-standard
echo.
echo Starting GEA Standard setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-standard /persistent:no
goto end
:gea-engineer
echo.
echo Starting GEA Engineer setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-engineer /persistent:no
goto end
:gea-shopfloor
echo.
echo Starting GEA Shopfloor setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor /persistent:no
goto end
:gea-shopfloor-mce
echo.
echo Starting GEA Shopfloor MCE setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\gea-shopfloor-mce /persistent:no
goto end
:ge-standard
echo.
echo Starting GE Standard setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-standard /persistent:no
goto end
:ge-engineer
echo.
echo Starting GE Engineer setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-engineer /persistent:no
goto end
:ge-shopfloor-lockdown
echo.
echo Starting GE Shopfloor Lockdown setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-shopfloor-lockdown /persistent:no
goto end
:ge-shopfloor-mce
echo.
echo Starting GE Shopfloor MCE setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\10.9.100.1\winpeapps\ge-shopfloor-mce /persistent:no
goto end
:end

View File

@@ -72,6 +72,7 @@ IMAGE_TYPES = [
"gea-standard",
"gea-engineer",
"gea-shopfloor",
"gea-shopfloor-mce",
"ge-standard",
"ge-engineer",
"ge-shopfloor-lockdown",
@@ -82,6 +83,7 @@ FRIENDLY_NAMES = {
"gea-standard": "GE Aerospace Standard",
"gea-engineer": "GE Aerospace Engineer",
"gea-shopfloor": "GE Aerospace Shop Floor",
"gea-shopfloor-mce": "GE Aerospace Shop Floor MCE",
"ge-standard": "GE Legacy Standard",
"ge-engineer": "GE Legacy Engineer",
"ge-shopfloor-lockdown": "GE Legacy Shop Floor Lockdown",
@@ -213,8 +215,9 @@ def find_upload_sources():
return sources
def _import_deploy(src_deploy, dst_deploy, target=""):
"""Copy Deploy directory contents, merging shared subdirs into _shared."""
def _import_deploy(src_deploy, dst_deploy, target="", move=False):
"""Import Deploy directory contents, merging shared subdirs into _shared.
When move=True, files are moved instead of copied (saves disk space)."""
# Build list of scoped shared dirs for this target
scoped_shared = []
prefix_key = ""
@@ -224,20 +227,23 @@ def _import_deploy(src_deploy, dst_deploy, target=""):
prefix_key = prefix
break
_transfer = shutil.move if move else shutil.copy2
_transfer_tree = shutil.move if move else shutil.copytree
os.makedirs(dst_deploy, exist_ok=True)
for item in os.listdir(src_deploy):
src_item = os.path.join(src_deploy, item)
dst_item = os.path.join(dst_deploy, item)
if not os.path.isdir(src_item):
shutil.copy2(src_item, dst_item)
_transfer(src_item, dst_item)
continue
# Global shared (e.g., Out-of-box Drivers) — one copy for all
if item in SHARED_DEPLOY_GLOBAL:
shared_dest = os.path.join(SHARED_DIR, item)
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest)
_merge_tree(src_item, shared_dest, move=move)
_replace_with_symlink(dst_item, shared_dest)
continue
@@ -245,14 +251,14 @@ def _import_deploy(src_deploy, dst_deploy, target=""):
if item in scoped_shared:
shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}")
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest)
_merge_tree(src_item, shared_dest, move=move)
_replace_with_symlink(dst_item, shared_dest)
continue
# Normal copy
# Normal transfer
if os.path.exists(dst_item):
shutil.rmtree(dst_item)
shutil.copytree(src_item, dst_item)
_transfer_tree(src_item, dst_item)
def _replace_with_symlink(link_path, target_path):
@@ -264,21 +270,24 @@ def _replace_with_symlink(link_path, target_path):
os.symlink(target_path, link_path)
def _merge_tree(src, dst):
"""Recursively merge src tree into dst, overwriting existing files."""
def _merge_tree(src, dst, move=False):
"""Recursively merge src tree into dst, overwriting existing files.
When move=True, files are moved instead of copied."""
_transfer = shutil.move if move else shutil.copy2
_transfer_tree = shutil.move if move else shutil.copytree
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
if os.path.isdir(d):
_merge_tree(s, d)
_merge_tree(s, d, move=move)
else:
if os.path.exists(d):
os.remove(d)
shutil.copytree(s, d)
_transfer_tree(s, d)
else:
os.makedirs(os.path.dirname(d), exist_ok=True)
shutil.copy2(s, d)
_transfer(s, d)
def allowed_import_source(source):
@@ -659,6 +668,11 @@ def images_import():
os.makedirs(dest, exist_ok=True)
src_items = os.listdir(source)
# Move files from network upload to save disk space; copy from USB
use_move = source == UPLOAD_DIR or source.startswith(UPLOAD_DIR + "/")
_transfer = shutil.move if use_move else shutil.copy2
_transfer_tree = shutil.move if use_move else shutil.copytree
# Detect layout: if source has Deploy/, Sources/, Tools/ at top
# level, it's the full image root structure (USB-style).
# Otherwise treat it as Deploy/ contents directly.
@@ -673,18 +687,18 @@ def images_import():
shared_root = dirs
break
# Full image root: copy Deploy contents + sibling dirs
# Full image root: import Deploy contents + sibling dirs
for item in src_items:
src_item = os.path.join(source, item)
if item == "Deploy":
_import_deploy(src_item, dest, target)
_import_deploy(src_item, dest, target, move=use_move)
elif os.path.isdir(src_item) and item in shared_root:
# Shared sibling: merge into _shared/{prefix}{item}
# and symlink from image root
prefix_key = target.split("-")[0] + "-"
shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}")
os.makedirs(shared_dest, exist_ok=True)
_merge_tree(src_item, shared_dest)
_merge_tree(src_item, shared_dest, move=use_move)
dst_item = os.path.join(root, item)
if os.path.islink(dst_item):
os.remove(dst_item)
@@ -696,12 +710,12 @@ def images_import():
dst_item = os.path.join(root, item)
if os.path.exists(dst_item):
shutil.rmtree(dst_item)
shutil.copytree(src_item, dst_item)
_transfer_tree(src_item, dst_item)
else:
shutil.copy2(src_item, os.path.join(root, item))
_transfer(src_item, os.path.join(root, item))
else:
# Flat layout: treat source as Deploy contents
_import_deploy(source, dest, target)
_import_deploy(source, dest, target, move=use_move)
audit("IMAGE_IMPORT", f"{source} -> {target}")
flash(