diff --git a/Upload-Image.ps1 b/Upload-Image.ps1
new file mode 100644
index 0000000..bead22f
--- /dev/null
+++ b/Upload-Image.ps1
@@ -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 ""
diff --git a/download-packages.sh b/download-packages.sh
index 433113b..c9b47c1 100755
--- a/download-packages.sh
+++ b/download-packages.sh
@@ -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.)
diff --git a/playbook/pxe_server_setup.yml b/playbook/pxe_server_setup.yml
index bd0b190..aee3935 100644
--- a/playbook/pxe_server_setup.yml
+++ b/playbook/pxe_server_setup.yml
@@ -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"
diff --git a/playbook/startnet.cmd b/playbook/startnet.cmd
new file mode 100644
index 0000000..70808cf
--- /dev/null
+++ b/playbook/startnet.cmd
@@ -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
diff --git a/prepare-boot-tools.sh b/prepare-boot-tools.sh
index d1b3cba..29c862b 100755
--- a/prepare-boot-tools.sh
+++ b/prepare-boot-tools.sh
@@ -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||10.9.100.1|' "$CFGTMP/preferences.xml"
- sed -i 's||blancco-reports|' "$CFGTMP/preferences.xml"
- # Enable auto-backup of reports to the network share
- sed -i 's|false|true|' "$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'',
+ b'blancco'
+)
+data = data.replace(
+ b'',
+ b'blancco'
+)
+data = data.replace(
+ b'',
+ b'10.9.100.1'
+)
+data = data.replace(
+ b'',
+ b'blancco-reports'
+)
+
+# Enable auto-backup
+data = data.replace(
+ b'false',
+ b'true'
+)
+
+# Enable bootable report
+data = data.replace(
+ b'\n false\n ',
+ b'\n true\n '
+)
+
+# 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'', 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
diff --git a/startnet-template.cmd b/startnet-template.cmd
new file mode 100644
index 0000000..70808cf
--- /dev/null
+++ b/startnet-template.cmd
@@ -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
diff --git a/webapp/app.py b/webapp/app.py
index 11e3143..944404b 100644
--- a/webapp/app.py
+++ b/webapp/app.py
@@ -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(