Adds a local-install pipeline so Standard shopfloor PCs get Oracle, the
VC++ redists (2008-2022), and UDC installed during PXE imaging via Samba
instead of pulling ~215 MB per device from Azure blob over the corporate
WAN. Intune DSC then verifies (already-installed apps are skipped) and
the only Azure traffic on the happy path is ~11 KB of CustomScripts
wrapper polling.
New files:
- playbook/preinstall/preinstall.json — curated app list with PCTypes
filter and per-app detection rules. Install order puts VC++ 2008
LAST so its (formerly) reboot-triggering bootstrapper doesn't kill
the runner mid-loop. (2008 itself now uses extracted vc_red.msi with
REBOOT=ReallySuppress; the reorder is defense in depth.)
- playbook/shopfloor-setup/Shopfloor/00-PreInstall-MachineApps.ps1 —
the runner. Numbered 00- so it runs first in the baseline sequence.
Reads preinstall.json, filters by PCTYPE, polls for completion via
detection check (handles UDC's hung WPF process by killing it once
detection passes), uses synchronous WriteThrough logging that
survives hard reboots, preserves log history across runs.
- playbook/shopfloor-setup/Standard/Set-MachineNumber.{ps1,bat} — desktop
helper for SupportUser. Reads current UDC + eDNC machine numbers,
prompts via VB InputBox, validates digits-only, kills running UDC,
edits both C:\ProgramData\UDC\udc_settings.json and HKLM\…\GE Aircraft
Engines\DNC\General\MachineNo, relaunches UDC. Lets a tech assign a
real machine number to a mass-produced PC without admin/LAPS.
- playbook/sync-preinstall.sh — workstation helper to push installer
binaries from /home/camp/pxe-images/main/ to the live PXE Samba.
Changes:
- playbook/startnet.cmd + startnet-template.cmd — add xcopy to stage
preinstall bundle from Y:\preinstall\ to W:\PreInstall\ during the
WinPE imaging phase, gated on PCTYPE being set.
- playbook/pxe_server_setup.yml — create /srv/samba/enrollment/preinstall
+ installers/ directories and deploy preinstall.json there.
- playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 — bump AutoLogonCount
to 99 at start (defense against any installer triggering an immediate
reboot mid-dispatcher; final line still resets to 2 on successful
completion). Copy Set-MachineNumber.{ps1,bat} to SupportUser desktop
on Standard PCs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
787 lines
26 KiB
YAML
787 lines
26 KiB
YAML
---
|
|
- name: PXE Server Setup (Ubuntu with dnsmasq)
|
|
hosts: localhost
|
|
connection: local
|
|
become: yes
|
|
gather_facts: yes
|
|
|
|
pre_tasks:
|
|
- name: "Verify required packages are installed (pre-installed from offline .debs)"
|
|
command: dpkg -s {{ item }}
|
|
loop:
|
|
- dnsmasq
|
|
- apache2
|
|
- samba
|
|
- unzip
|
|
- ufw
|
|
- cron
|
|
- ansible
|
|
- wimtools
|
|
register: pkg_check
|
|
failed_when: false
|
|
changed_when: false
|
|
|
|
- name: "Warn about missing packages"
|
|
debug:
|
|
msg: "WARNING: {{ item.item }} is not installed! Install offline .debs first."
|
|
loop: "{{ pkg_check.results }}"
|
|
when: item.rc != 0
|
|
|
|
vars:
|
|
tftp_dir: "/srv/tftp"
|
|
web_root: "/var/www/html"
|
|
samba_share: "/srv/samba/winpeapps"
|
|
usb_mount: "/mnt/usb/playbook" # playbook location on USB
|
|
usb_root: "/mnt/usb" # CIDATA partition root
|
|
image_types:
|
|
- gea-standard
|
|
- gea-engineer
|
|
- gea-shopfloor
|
|
- ge-standard
|
|
- ge-engineer
|
|
- ge-shopfloor-lockdown
|
|
- ge-shopfloor-mce
|
|
shopfloor_types:
|
|
- gea-shopfloor
|
|
deploy_subdirs:
|
|
- Applications
|
|
- Control
|
|
- "Operating Systems"
|
|
- "Out-of-box Drivers"
|
|
- Packages
|
|
- Tools
|
|
|
|
tasks:
|
|
- name: "Gather minimal network facts"
|
|
ansible.builtin.setup:
|
|
filter:
|
|
- ansible_interfaces
|
|
- ansible_default_ipv4
|
|
|
|
- name: "Bring up all ethernet-like interfaces"
|
|
command: ip link set dev {{ item }} up
|
|
loop: "{{ ansible_interfaces | select('match','^e(th|n)') | list }}"
|
|
ignore_errors: yes
|
|
|
|
- name: "Find interface with 10.9.100.1 already configured"
|
|
set_fact:
|
|
preconfigured_iface: >-
|
|
{{ ansible_interfaces
|
|
| select('match','^e(th|n)')
|
|
| map('regex_replace','^(.*)$','ansible_\1')
|
|
| map('extract', hostvars[inventory_hostname])
|
|
| selectattr('ipv4','defined')
|
|
| selectattr('ipv4.address','equalto','10.9.100.1')
|
|
| map(attribute='device')
|
|
| list
|
|
| first
|
|
| default('') }}
|
|
ignore_errors: yes
|
|
|
|
- name: "Determine PXE interface"
|
|
set_fact:
|
|
pxe_iface: >-
|
|
{{ preconfigured_iface | default('',true)
|
|
or (ansible_interfaces
|
|
| select('match','^e(th|n)')
|
|
| reject('equalto','lo')
|
|
| reject('equalto', ansible_default_ipv4.interface | default(''))
|
|
| list
|
|
)
|
|
| first
|
|
| default(ansible_default_ipv4.interface | default(
|
|
ansible_interfaces | select('match','^e(th|n)') | first | default('eth0')
|
|
)) }}
|
|
|
|
- name: "Debug: final pxe_iface choice"
|
|
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
|
|
backup: yes
|
|
content: |
|
|
port=0
|
|
interface={{ pxe_iface }}
|
|
bind-interfaces
|
|
dhcp-range=10.9.100.10,10.9.100.100,12h
|
|
dhcp-option=3,10.9.100.1
|
|
dhcp-option=6,8.8.8.8
|
|
enable-tftp
|
|
tftp-root={{ tftp_dir }}
|
|
dhcp-boot=ipxe.efi
|
|
log-dhcp
|
|
|
|
- name: "Create TFTP directory"
|
|
file:
|
|
path: "{{ tftp_dir }}"
|
|
state: directory
|
|
mode: '0755'
|
|
owner: nobody
|
|
group: nogroup
|
|
|
|
- name: "Create Win11 directory structure"
|
|
file:
|
|
path: "{{ web_root }}/win11/{{ item }}"
|
|
state: directory
|
|
mode: '0755'
|
|
loop:
|
|
- "EFI/Boot"
|
|
- "EFI/Microsoft/Boot"
|
|
- "Boot"
|
|
- "sources"
|
|
|
|
- name: "Create Altiris iPXE directory"
|
|
file:
|
|
path: "{{ web_root }}/Altiris/iPXE"
|
|
state: directory
|
|
mode: '0755'
|
|
|
|
- name: "Create boot tool directories"
|
|
file:
|
|
path: "{{ web_root }}/{{ item }}"
|
|
state: directory
|
|
mode: '0755'
|
|
loop:
|
|
- clonezilla
|
|
- blancco
|
|
- memtest
|
|
|
|
- name: "Create GetPxeScript.aspx (iPXE boot menu)"
|
|
copy:
|
|
dest: "{{ web_root }}/Altiris/iPXE/GetPxeScript.aspx"
|
|
backup: yes
|
|
content: |
|
|
#!ipxe
|
|
|
|
set server 10.9.100.1
|
|
|
|
:menu
|
|
menu GE Aerospace PXE Boot Menu
|
|
item --gap -- ---- Windows Deployment ----
|
|
item winpe Windows PE (Image Deployment)
|
|
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
|
|
item exit Exit to BIOS
|
|
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
|
|
initrd http://${server}/win11/EFI/Boot/bootx64.efi EFI/Boot/bootx64.efi
|
|
initrd http://${server}/win11/Boot/boot.sdi Boot/boot.sdi
|
|
initrd http://${server}/win11/sources/boot.wim sources/boot.wim
|
|
boot
|
|
|
|
: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 || goto secureboot_warn
|
|
initrd ${base}/initrd.img
|
|
boot
|
|
|
|
:blancco
|
|
chain http://${server}/blancco/grubx64.efi || goto secureboot_warn
|
|
|
|
:memtest
|
|
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
|
|
|
|
:exit
|
|
exit
|
|
|
|
- name: "Ensure Apache listens on port 4433"
|
|
lineinfile:
|
|
path: /etc/apache2/ports.conf
|
|
line: "Listen 4433"
|
|
backup: yes
|
|
state: present
|
|
|
|
- name: "Create VirtualHost for Altiris iPXE on 4433"
|
|
copy:
|
|
dest: /etc/apache2/sites-available/altiris-ipxe.conf
|
|
backup: yes
|
|
content: |
|
|
<VirtualHost *:4433>
|
|
DocumentRoot {{ web_root }}
|
|
<Directory "{{ web_root }}/Altiris/iPXE">
|
|
Options Indexes FollowSymLinks
|
|
AllowOverride None
|
|
Require all granted
|
|
AddType text/plain .aspx
|
|
</Directory>
|
|
</VirtualHost>
|
|
|
|
- name: "Enable Altiris iPXE site"
|
|
command: a2ensite altiris-ipxe.conf
|
|
args:
|
|
creates: /etc/apache2/sites-enabled/altiris-ipxe.conf
|
|
|
|
- name: "Reload Apache to apply changes"
|
|
systemd:
|
|
name: apache2
|
|
state: reloaded
|
|
|
|
- name: "Create Samba share root"
|
|
file:
|
|
path: "{{ samba_share }}"
|
|
state: directory
|
|
mode: '0777'
|
|
|
|
- name: "Create Clonezilla backup share directory"
|
|
file:
|
|
path: /srv/samba/clonezilla
|
|
state: directory
|
|
mode: '0777'
|
|
|
|
- name: "Create Blancco reports share directory"
|
|
file:
|
|
path: /srv/samba/blancco-reports
|
|
state: directory
|
|
mode: '0777'
|
|
|
|
- name: "Create enrollment packages directory"
|
|
file:
|
|
path: /srv/samba/enrollment
|
|
state: directory
|
|
mode: '0777'
|
|
|
|
- name: "Deploy shopfloor setup scripts to enrollment share"
|
|
copy:
|
|
src: "{{ usb_mount }}/shopfloor-setup/"
|
|
dest: /srv/samba/enrollment/shopfloor-setup/
|
|
mode: '0755'
|
|
directory_mode: '0755'
|
|
ignore_errors: yes
|
|
|
|
- name: "Create preinstall bundle directory on enrollment share"
|
|
file:
|
|
path: "{{ item }}"
|
|
state: directory
|
|
mode: '0755'
|
|
loop:
|
|
- /srv/samba/enrollment/preinstall
|
|
- /srv/samba/enrollment/preinstall/installers
|
|
|
|
- name: "Deploy preinstall.json (installer binaries staged separately)"
|
|
copy:
|
|
src: "{{ usb_mount }}/preinstall/preinstall.json"
|
|
dest: /srv/samba/enrollment/preinstall/preinstall.json
|
|
mode: '0644'
|
|
ignore_errors: yes
|
|
|
|
- name: "Create BIOS update directory on enrollment share"
|
|
file:
|
|
path: /srv/samba/enrollment/BIOS
|
|
state: directory
|
|
mode: '0755'
|
|
|
|
- name: "Deploy BIOS check script and manifest"
|
|
copy:
|
|
src: "{{ usb_mount }}/shopfloor-setup/BIOS/{{ item }}"
|
|
dest: /srv/samba/enrollment/BIOS/{{ item }}
|
|
mode: '0644'
|
|
loop:
|
|
- check-bios.cmd
|
|
- models.txt
|
|
ignore_errors: yes
|
|
|
|
- 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
|
|
backup: yes
|
|
block: |
|
|
[winpeapps]
|
|
path = {{ samba_share }}
|
|
browseable = yes
|
|
read only = no
|
|
guest ok = no
|
|
valid users = pxe-upload
|
|
force user = root
|
|
|
|
[clonezilla]
|
|
path = /srv/samba/clonezilla
|
|
browseable = yes
|
|
read only = no
|
|
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 = no
|
|
valid users = pxe-upload blancco
|
|
force user = root
|
|
comment = Blancco Drive Eraser reports
|
|
|
|
[enrollment]
|
|
path = /srv/samba/enrollment
|
|
browseable = yes
|
|
read only = no
|
|
guest ok = no
|
|
valid users = pxe-upload
|
|
force user = root
|
|
comment = GCCH bulk enrollment packages
|
|
|
|
[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
|
|
oplocks = no
|
|
level2 oplocks = no
|
|
strict sync = yes
|
|
|
|
- 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 }}"
|
|
state: directory
|
|
mode: '0777'
|
|
loop: "{{ image_types }}"
|
|
|
|
- name: "Create Deploy subdirectories for each image type"
|
|
file:
|
|
path: "{{ samba_share }}/{{ item.0 }}/Deploy/{{ item.1 }}"
|
|
state: directory
|
|
mode: '0777'
|
|
with_nested:
|
|
- "{{ 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: "Deploy shopfloor unattend.xml template"
|
|
copy:
|
|
src: "{{ usb_mount }}/FlatUnattendW10-shopfloor.xml"
|
|
dest: "{{ samba_share }}/{{ item }}/Deploy/FlatUnattendW10.xml"
|
|
mode: '0644'
|
|
force: no
|
|
loop: "{{ shopfloor_types }}"
|
|
ignore_errors: yes
|
|
|
|
- name: "Daily cron to create/refresh Media.tag for all images"
|
|
copy:
|
|
content: |
|
|
# Create Media.tag in any image with Deploy/Control/ and refresh existing ones
|
|
@reboot root for d in {{ samba_share }}/*/Deploy/Control; do [ -d "$d" ] && touch "$d/Media.tag"; done
|
|
0 0 * * * root for d in {{ samba_share }}/*/Deploy/Control; do [ -d "$d" ] && touch "$d/Media.tag"; done
|
|
dest: /etc/cron.d/media-tag-refresh
|
|
mode: '0644'
|
|
|
|
- name: "Copy WinPE & boot files from USB (skipped if not present)"
|
|
copy:
|
|
src: "{{ usb_root }}/{{ item.src }}"
|
|
dest: "{{ web_root }}/win11/{{ item.dest }}"
|
|
mode: '0644'
|
|
loop:
|
|
- { src: "wimboot", dest: "wimboot" }
|
|
- { src: "boot.stl", dest: "EFI/Microsoft/Boot/boot.stl" }
|
|
- { src: "BCD", dest: "EFI/Microsoft/Boot/BCD" }
|
|
- { src: "bootx64.efi", dest: "EFI/Boot/bootx64.efi" }
|
|
- { src: "boot.sdi", dest: "Boot/boot.sdi" }
|
|
- { 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 }}"
|
|
dest: "{{ tftp_dir }}/{{ item }}"
|
|
mode: '0755'
|
|
loop:
|
|
- ipxe.efi
|
|
ignore_errors: yes
|
|
|
|
- name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)"
|
|
shell: >
|
|
cp -r "{{ usb_root }}/boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || true
|
|
loop:
|
|
- clonezilla
|
|
- blancco
|
|
- memtest
|
|
|
|
# --- Blancco PXE boot via Ubuntu kernel + switch_root ---
|
|
# Blancco's own kernel freezes on Dell Precision towers. Workaround:
|
|
# Boot Ubuntu kernel, download Blancco rootfs, overlay mount, switch_root.
|
|
|
|
- name: "Build Blancco PXE initramfs"
|
|
shell: |
|
|
set -e
|
|
WORK=$(mktemp -d)
|
|
mkdir -p "$WORK"/{bin,lib/modules,lib64,sbin,usr/share/udhcpc}
|
|
|
|
# Busybox (static)
|
|
cp /bin/busybox "$WORK/bin/" 2>/dev/null || apt-get install -y busybox-static >/dev/null && cp /bin/busybox "$WORK/bin/"
|
|
for cmd in sh awk cat chmod echo grep gunzip ifconfig ip ln losetup ls mkdir mknod mount reboot route sed sleep switch_root tar udhcpc umount wget cpio; do
|
|
ln -sf busybox "$WORK/bin/$cmd"
|
|
done
|
|
|
|
# NIC drivers (common server NICs)
|
|
KVER=$(uname -r)
|
|
KMOD="/lib/modules/$KVER/kernel/drivers/net/ethernet"
|
|
for drv in intel/e1000e/e1000e.ko.zst intel/igb/igb.ko.zst broadcom/tg3.ko.zst broadcom/bnx2.ko.zst broadcom/bnxt/bnxt_en.ko.zst broadcom/b44.ko.zst; do
|
|
if [ -f "$KMOD/$drv" ]; then
|
|
zstd -d "$KMOD/$drv" -o "$WORK/lib/modules/$(basename ${drv%.zst})" 2>/dev/null
|
|
fi
|
|
done
|
|
|
|
# Overlay module
|
|
OVMOD="/lib/modules/$KVER/kernel/fs/overlayfs/overlay.ko.zst"
|
|
if [ -f "$OVMOD" ]; then
|
|
zstd -d "$OVMOD" -o "$WORK/lib/modules/overlay.ko" 2>/dev/null
|
|
fi
|
|
|
|
# Init script
|
|
cp "{{ usb_root }}/playbook/blancco-init.sh" "$WORK/init"
|
|
chmod +x "$WORK/init"
|
|
|
|
# Build CPIO
|
|
cd "$WORK"
|
|
find . | cpio -o -H newc 2>/dev/null | gzip > "{{ web_root }}/blancco/kexec-initrd.img"
|
|
rm -rf "$WORK"
|
|
echo "Built kexec-initrd.img: $(stat -c %s '{{ web_root }}/blancco/kexec-initrd.img') bytes"
|
|
args:
|
|
creates: "{{ web_root }}/blancco/kexec-initrd.img"
|
|
|
|
- name: "Copy Ubuntu kernel for Blancco PXE boot"
|
|
copy:
|
|
src: "/boot/vmlinuz-{{ ansible_kernel }}"
|
|
dest: "{{ web_root }}/blancco/vmlinuz-ubuntu"
|
|
remote_src: yes
|
|
mode: '0644'
|
|
|
|
- name: "Build Ubuntu kernel modules tarball for Blancco"
|
|
shell: |
|
|
set -e
|
|
KVER=$(uname -r)
|
|
tar czf "{{ web_root }}/blancco/kmod.tar.gz" -C / "lib/modules/$KVER"
|
|
echo "Built kmod.tar.gz: $(du -h '{{ web_root }}/blancco/kmod.tar.gz' | cut -f1)"
|
|
args:
|
|
creates: "{{ web_root }}/blancco/kmod.tar.gz"
|
|
|
|
- name: "Deploy Blancco config and preferences (null-stripped)"
|
|
shell: |
|
|
# Strip null bytes from config.img files and deploy
|
|
if [ -f "{{ web_root }}/blancco/config.img" ]; then
|
|
WORK=$(mktemp -d)
|
|
cd "$WORK"
|
|
cpio -id < "{{ web_root }}/blancco/config.img" 2>/dev/null
|
|
tr -d '\000' < config.xml > "{{ web_root }}/blancco/config-clean.xml"
|
|
rm -rf "$WORK"
|
|
fi
|
|
# Deploy preferences from playbook (pre-configured with network share)
|
|
cp "{{ usb_root }}/playbook/blancco-preferences.xml" "{{ web_root }}/blancco/preferences.xml"
|
|
args:
|
|
creates: "{{ web_root }}/blancco/config-clean.xml"
|
|
|
|
- name: "Create Samba user for Blancco reports"
|
|
shell: |
|
|
id blancco 2>/dev/null || useradd -r -s /usr/sbin/nologin blancco
|
|
echo -e "blancco\nblancco" | smbpasswd -a -s blancco 2>/dev/null
|
|
args:
|
|
creates: /etc/samba/smbpasswd
|
|
|
|
- name: "Check for WinPE deployment content on USB"
|
|
stat:
|
|
path: "{{ usb_root }}/images"
|
|
register: usb_images_dir
|
|
|
|
- name: "Import WinPE deployment content from USB (if present)"
|
|
shell: >
|
|
cp -rn "{{ usb_root }}/images/{{ item }}/"* "{{ samba_share }}/{{ item }}/" 2>/dev/null || true
|
|
loop: "{{ image_types }}"
|
|
when: usb_images_dir.stat.exists
|
|
|
|
- name: "Restart and enable services"
|
|
systemd:
|
|
name: "{{ item }}"
|
|
state: restarted
|
|
enabled: yes
|
|
loop:
|
|
- dnsmasq
|
|
- apache2
|
|
- smbd
|
|
|
|
- name: "Allow necessary firewall ports (UFW)"
|
|
ufw:
|
|
rule: allow
|
|
port: "{{ item }}"
|
|
proto: "{{ 'udp' if item in ['67','69'] else 'tcp' }}"
|
|
loop:
|
|
- "22"
|
|
- "67"
|
|
- "69"
|
|
- "80"
|
|
- "4433"
|
|
- "445"
|
|
- "9009"
|
|
|
|
- name: "Enable UFW firewall"
|
|
ufw:
|
|
state: enabled
|
|
policy: deny
|
|
|
|
- name: "Schedule dnsmasq restart 15s after reboot"
|
|
copy:
|
|
dest: /etc/cron.d/dnsmasq-restart
|
|
mode: '0644'
|
|
content: |
|
|
@reboot root /bin/sleep 15 && /usr/bin/systemctl restart dnsmasq.service
|
|
|
|
# --- Web Management App (Flask) ---
|
|
- name: "Create webapp directory"
|
|
file:
|
|
path: /opt/pxe-webapp
|
|
state: directory
|
|
mode: '0755'
|
|
|
|
- name: "Copy webapp from USB"
|
|
shell: >
|
|
cp -r "{{ usb_root }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true
|
|
args:
|
|
creates: /opt/pxe-webapp/app.py
|
|
|
|
- name: "Install webapp Python dependencies (offline wheels)"
|
|
args:
|
|
executable: /bin/bash
|
|
shell: |
|
|
# Find the pip-wheels directory on the CIDATA mount
|
|
export WHEEL_DIR=""
|
|
for d in "{{ usb_root }}/pip-wheels" "{{ usb_mount }}/pip-wheels"; do
|
|
if [ -d "$d" ] && compgen -G "$d/*.whl" > /dev/null; then
|
|
export WHEEL_DIR="$(cd "$d" && pwd)"
|
|
break
|
|
fi
|
|
done
|
|
if [ -n "$WHEEL_DIR" ]; then
|
|
# Install wheels directly using Python zipfile (bypasses pip entirely)
|
|
# This avoids the pip3/distutils incompatibility between Ubuntu 22.04 debs and Python 3.12
|
|
python3 << 'PYEOF'
|
|
import zipfile, sysconfig, glob, os
|
|
site = sysconfig.get_path('platlib')
|
|
wheel_dir = os.environ['WHEEL_DIR']
|
|
for whl in sorted(glob.glob(os.path.join(wheel_dir, '*.whl'))):
|
|
name = os.path.basename(whl)
|
|
print(f' Installing {name}')
|
|
with zipfile.ZipFile(whl) as z:
|
|
z.extractall(site)
|
|
print('All wheels installed to ' + site)
|
|
PYEOF
|
|
else
|
|
# Fallback: try system pip (works if system has internet and compatible pip)
|
|
python3 -m pip install --break-system-packages flask lxml 2>/dev/null ||
|
|
pip3 install --break-system-packages flask lxml
|
|
fi
|
|
|
|
- name: "Create systemd service for PXE webapp"
|
|
copy:
|
|
dest: /etc/systemd/system/pxe-webapp.service
|
|
content: |
|
|
[Unit]
|
|
Description=PXE Server Web Management
|
|
After=network.target apache2.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=root
|
|
WorkingDirectory=/opt/pxe-webapp
|
|
Environment=SAMBA_SHARE={{ samba_share }}
|
|
Environment=CLONEZILLA_SHARE=/srv/samba/clonezilla
|
|
Environment=WEB_ROOT={{ web_root }}
|
|
Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
|
|
Environment=ENROLLMENT_SHARE=/srv/samba/enrollment
|
|
Environment=AUDIT_LOG=/var/log/pxe-webapp-audit.log
|
|
ExecStart=/usr/bin/python3 app.py
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
|
|
- name: "Enable and start PXE webapp service"
|
|
systemd:
|
|
name: pxe-webapp
|
|
state: started
|
|
enabled: yes
|
|
daemon_reload: yes
|
|
|
|
- name: "Configure unified Apache site (static files + webapp proxy)"
|
|
copy:
|
|
dest: /etc/apache2/sites-available/pxe-server.conf
|
|
content: |
|
|
Listen 9009
|
|
<VirtualHost *:80>
|
|
DocumentRoot {{ web_root }}
|
|
<Directory "{{ web_root }}">
|
|
Options Indexes FollowSymLinks
|
|
AllowOverride None
|
|
Require all granted
|
|
</Directory>
|
|
ProxyPreserveHost On
|
|
ProxyPass /manage http://127.0.0.1:9010/
|
|
ProxyPassReverse /manage http://127.0.0.1:9010/
|
|
</VirtualHost>
|
|
<VirtualHost *:9009>
|
|
Alias /static /opt/pxe-webapp/static
|
|
<Directory "/opt/pxe-webapp/static">
|
|
Require all granted
|
|
Options -Indexes
|
|
</Directory>
|
|
ProxyPreserveHost On
|
|
ProxyPass /static !
|
|
ProxyPass / http://127.0.0.1:9010/
|
|
ProxyPassReverse / http://127.0.0.1:9010/
|
|
</VirtualHost>
|
|
|
|
- name: "Enable Apache proxy modules"
|
|
command: a2enmod proxy proxy_http
|
|
args:
|
|
creates: /etc/apache2/mods-enabled/proxy.load
|
|
|
|
- name: "Disable default Apache site"
|
|
command: a2dissite 000-default.conf
|
|
args:
|
|
removes: /etc/apache2/sites-enabled/000-default.conf
|
|
|
|
- name: "Enable unified PXE server site"
|
|
command: a2ensite pxe-server.conf
|
|
args:
|
|
creates: /etc/apache2/sites-enabled/pxe-server.conf
|
|
|
|
- name: "Reload Apache after site changes"
|
|
systemd:
|
|
name: apache2
|
|
state: reloaded
|
|
|
|
- name: "Configure static IP for PXE interface"
|
|
copy:
|
|
dest: /etc/netplan/50-cloud-init.yaml
|
|
backup: yes
|
|
content: |
|
|
network:
|
|
version: 2
|
|
renderer: networkd
|
|
ethernets:
|
|
{{ pxe_iface }}:
|
|
dhcp4: no
|
|
addresses: [10.9.100.1/24]
|
|
notify: "Apply netplan"
|
|
|
|
handlers:
|
|
- name: "Apply netplan"
|
|
command: netplan apply
|