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 cron
wimtools wimtools
p7zip-full p7zip-full
grub-efi-amd64-bin
grub-common
) )
# Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.) # Packages installed during autoinstall late-commands (NetworkManager, WiFi, etc.)

View File

@@ -37,6 +37,7 @@
- gea-standard - gea-standard
- gea-engineer - gea-engineer
- gea-shopfloor - gea-shopfloor
- gea-shopfloor-mce
- ge-standard - ge-standard
- ge-engineer - ge-engineer
- ge-shopfloor-lockdown - ge-shopfloor-lockdown
@@ -95,6 +96,33 @@
debug: debug:
msg: "Using {{ pxe_iface }} for DHCP/TFTP" 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" - name: "Configure dnsmasq for DHCP and TFTP"
copy: copy:
dest: /etc/dnsmasq.conf dest: /etc/dnsmasq.conf
@@ -158,9 +186,9 @@
menu GE Aerospace PXE Boot Menu menu GE Aerospace PXE Boot Menu
item --gap -- ---- Windows Deployment ---- item --gap -- ---- Windows Deployment ----
item winpe Windows PE (Image Deployment) item winpe Windows PE (Image Deployment)
item --gap -- ---- Utilities ---- item --gap -- ---- Utilities (Secure Boot must be DISABLED) ----
item clonezilla Clonezilla Live (Disk Imaging)
item blancco Blancco Drive Eraser item blancco Blancco Drive Eraser
item clonezilla Clonezilla Live (Disk Imaging)
item memtest Memtest86+ (Memory Diagnostics) item memtest Memtest86+ (Memory Diagnostics)
item --gap -- ---- item --gap -- ----
item reboot Reboot item reboot Reboot
@@ -168,6 +196,13 @@
choose --default winpe --timeout 30000 target && goto ${target} choose --default winpe --timeout 30000 target && goto ${target}
:winpe :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 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/boot.stl EFI/Microsoft/Boot/Boot.stl
initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD
@@ -178,20 +213,34 @@
:clonezilla :clonezilla
set base http://${server}/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 initrd ${base}/initrd.img
boot boot
:blancco :blancco
set bbase http://${server}/blancco chain http://${server}/blancco/grubx64.efi || goto secureboot_warn
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
:memtest :memtest
kernel http://${server}/memtest/memtest.efi kernel http://${server}/memtest/memtest.efi || goto secureboot_warn
boot 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
reboot reboot
@@ -248,6 +297,25 @@
state: directory state: directory
mode: '0777' 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" - name: "Configure Samba shares"
blockinfile: blockinfile:
path: /etc/samba/smb.conf path: /etc/samba/smb.conf
@@ -257,22 +325,48 @@
path = {{ samba_share }} path = {{ samba_share }}
browseable = yes browseable = yes
read only = no read only = no
guest ok = yes guest ok = no
valid users = pxe-upload
force user = root
[clonezilla] [clonezilla]
path = /srv/samba/clonezilla path = /srv/samba/clonezilla
browseable = yes browseable = yes
read only = no read only = no
guest ok = yes guest ok = no
valid users = pxe-upload
force user = root
comment = Clonezilla backup images comment = Clonezilla backup images
[blancco-reports] [blancco-reports]
path = /srv/samba/blancco-reports path = /srv/samba/blancco-reports
browseable = yes browseable = yes
read only = no read only = no
guest ok = yes guest ok = no
valid users = pxe-upload blancco
force user = root
comment = Blancco Drive Eraser reports 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" - name: "Create image-type top-level directories"
file: file:
path: "{{ samba_share }}/{{ item }}" path: "{{ samba_share }}/{{ item }}"
@@ -289,6 +383,14 @@
- "{{ image_types }}" - "{{ image_types }}"
- "{{ deploy_subdirs }}" - "{{ 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)" - name: "Copy WinPE & boot files from USB (skipped if not present)"
copy: copy:
src: "{{ usb_root }}/{{ item.src }}" src: "{{ usb_root }}/{{ item.src }}"
@@ -303,6 +405,20 @@
- { src: "boot.wim", dest: "sources/boot.wim" } - { src: "boot.wim", dest: "sources/boot.wim" }
ignore_errors: yes 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)" - name: "Copy iPXE binaries from USB (skipped if not present)"
copy: copy:
src: "{{ usb_root }}/{{ item }}" src: "{{ usb_root }}/{{ item }}"
@@ -320,6 +436,25 @@
- blancco - blancco
- memtest - 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" - name: "Check for WinPE deployment content on USB"
stat: stat:
path: "{{ usb_root }}/images" 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/" cp "$TMPDIR/arch/x86_64/airootfs.sfs" "$OUT_DIR/blancco/arch/x86_64/"
echo " Extracted Blancco boot files." 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 if [ -f "$OUT_DIR/blancco/config.img" ]; then
echo " Patching config.img for network report storage..." echo " Patching config.img for network report storage..."
CFGTMP=$(mktemp -d) 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 cpio -id < "$OUT_DIR/blancco/config.img" 2>/dev/null
if [ -f "$CFGTMP/preferences.xml" ]; then if [ -f "$CFGTMP/preferences.xml" ]; then
# Set network share to PXE server's blancco-reports Samba share ORIG_SIZE=$(stat -c%s "$CFGTMP/preferences.xml")
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"
# Repack config.img python3 << 'PYEOF'
ls -1 | cpio -o -H newc > "$OUT_DIR/blancco/config.img" 2>/dev/null import sys
echo " Reports will auto-save to \\\\10.9.100.1\\blancco-reports"
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 fi
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
rm -rf "$CFGTMP" rm -rf "$CFGTMP"
fi 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 else
echo " Could not extract boot files from ISO." echo " Could not extract boot files from ISO."
fi 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-standard",
"gea-engineer", "gea-engineer",
"gea-shopfloor", "gea-shopfloor",
"gea-shopfloor-mce",
"ge-standard", "ge-standard",
"ge-engineer", "ge-engineer",
"ge-shopfloor-lockdown", "ge-shopfloor-lockdown",
@@ -82,6 +83,7 @@ FRIENDLY_NAMES = {
"gea-standard": "GE Aerospace Standard", "gea-standard": "GE Aerospace Standard",
"gea-engineer": "GE Aerospace Engineer", "gea-engineer": "GE Aerospace Engineer",
"gea-shopfloor": "GE Aerospace Shop Floor", "gea-shopfloor": "GE Aerospace Shop Floor",
"gea-shopfloor-mce": "GE Aerospace Shop Floor MCE",
"ge-standard": "GE Legacy Standard", "ge-standard": "GE Legacy Standard",
"ge-engineer": "GE Legacy Engineer", "ge-engineer": "GE Legacy Engineer",
"ge-shopfloor-lockdown": "GE Legacy Shop Floor Lockdown", "ge-shopfloor-lockdown": "GE Legacy Shop Floor Lockdown",
@@ -213,8 +215,9 @@ def find_upload_sources():
return sources return sources
def _import_deploy(src_deploy, dst_deploy, target=""): def _import_deploy(src_deploy, dst_deploy, target="", move=False):
"""Copy Deploy directory contents, merging shared subdirs into _shared.""" """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 # Build list of scoped shared dirs for this target
scoped_shared = [] scoped_shared = []
prefix_key = "" prefix_key = ""
@@ -224,20 +227,23 @@ def _import_deploy(src_deploy, dst_deploy, target=""):
prefix_key = prefix prefix_key = prefix
break 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) os.makedirs(dst_deploy, exist_ok=True)
for item in os.listdir(src_deploy): for item in os.listdir(src_deploy):
src_item = os.path.join(src_deploy, item) src_item = os.path.join(src_deploy, item)
dst_item = os.path.join(dst_deploy, item) dst_item = os.path.join(dst_deploy, item)
if not os.path.isdir(src_item): if not os.path.isdir(src_item):
shutil.copy2(src_item, dst_item) _transfer(src_item, dst_item)
continue continue
# Global shared (e.g., Out-of-box Drivers) — one copy for all # Global shared (e.g., Out-of-box Drivers) — one copy for all
if item in SHARED_DEPLOY_GLOBAL: if item in SHARED_DEPLOY_GLOBAL:
shared_dest = os.path.join(SHARED_DIR, item) shared_dest = os.path.join(SHARED_DIR, item)
os.makedirs(shared_dest, exist_ok=True) 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) _replace_with_symlink(dst_item, shared_dest)
continue continue
@@ -245,14 +251,14 @@ def _import_deploy(src_deploy, dst_deploy, target=""):
if item in scoped_shared: if item in scoped_shared:
shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}") shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}")
os.makedirs(shared_dest, exist_ok=True) 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) _replace_with_symlink(dst_item, shared_dest)
continue continue
# Normal copy # Normal transfer
if os.path.exists(dst_item): if os.path.exists(dst_item):
shutil.rmtree(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): 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) os.symlink(target_path, link_path)
def _merge_tree(src, dst): def _merge_tree(src, dst, move=False):
"""Recursively merge src tree into dst, overwriting existing files.""" """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): for item in os.listdir(src):
s = os.path.join(src, item) s = os.path.join(src, item)
d = os.path.join(dst, item) d = os.path.join(dst, item)
if os.path.isdir(s): if os.path.isdir(s):
if os.path.isdir(d): if os.path.isdir(d):
_merge_tree(s, d) _merge_tree(s, d, move=move)
else: else:
if os.path.exists(d): if os.path.exists(d):
os.remove(d) os.remove(d)
shutil.copytree(s, d) _transfer_tree(s, d)
else: else:
os.makedirs(os.path.dirname(d), exist_ok=True) os.makedirs(os.path.dirname(d), exist_ok=True)
shutil.copy2(s, d) _transfer(s, d)
def allowed_import_source(source): def allowed_import_source(source):
@@ -659,6 +668,11 @@ def images_import():
os.makedirs(dest, exist_ok=True) os.makedirs(dest, exist_ok=True)
src_items = os.listdir(source) 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 # Detect layout: if source has Deploy/, Sources/, Tools/ at top
# level, it's the full image root structure (USB-style). # level, it's the full image root structure (USB-style).
# Otherwise treat it as Deploy/ contents directly. # Otherwise treat it as Deploy/ contents directly.
@@ -673,18 +687,18 @@ def images_import():
shared_root = dirs shared_root = dirs
break break
# Full image root: copy Deploy contents + sibling dirs # Full image root: import Deploy contents + sibling dirs
for item in src_items: for item in src_items:
src_item = os.path.join(source, item) src_item = os.path.join(source, item)
if item == "Deploy": 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: elif os.path.isdir(src_item) and item in shared_root:
# Shared sibling: merge into _shared/{prefix}{item} # Shared sibling: merge into _shared/{prefix}{item}
# and symlink from image root # and symlink from image root
prefix_key = target.split("-")[0] + "-" prefix_key = target.split("-")[0] + "-"
shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}") shared_dest = os.path.join(SHARED_DIR, f"{prefix_key}{item}")
os.makedirs(shared_dest, exist_ok=True) 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) dst_item = os.path.join(root, item)
if os.path.islink(dst_item): if os.path.islink(dst_item):
os.remove(dst_item) os.remove(dst_item)
@@ -696,12 +710,12 @@ def images_import():
dst_item = os.path.join(root, item) dst_item = os.path.join(root, item)
if os.path.exists(dst_item): if os.path.exists(dst_item):
shutil.rmtree(dst_item) shutil.rmtree(dst_item)
shutil.copytree(src_item, dst_item) _transfer_tree(src_item, dst_item)
else: else:
shutil.copy2(src_item, os.path.join(root, item)) _transfer(src_item, os.path.join(root, item))
else: else:
# Flat layout: treat source as Deploy contents # 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}") audit("IMAGE_IMPORT", f"{source} -> {target}")
flash( flash(