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(