Every server has an imaging NIC (static, the only one that serves DHCP/TFTP/HTTP boot/SMB) and a GE corporate NIC (DHCP client, SSH and the webapp on 9009). The playbook guessed the PXE NIC and the installer put 172.16.9.1/24 on every en* NIC, so a two-NIC box could answer on the GE network. - New per-site vars pxe_nic_mac / corp_nic_mac (required, from playbook/site-vars.yml or -e; template site-vars.example.yml), stable names pxe_nic_name / corp_nic_name (default pxe0 / corp0, overridable), pxe_server_ip, pxe_subnet, DHCP range. pxe_imaging_mac / pxe_ge_mac are derived from them. The play stops before touching anything if a MAC is unset, malformed, duplicated or not on the box (lists the box's NICs). - Heuristic NIC detection and the single-NIC netplan task replaced by a two-NIC netplan template (match macaddress + set-name), installed by pxe-netplan-apply.sh: validate in a scratch root, back up, generate, rename, apply, restore the previous config on any failure. cloud-init network config disabled so it cannot rewrite it. - dnsmasq: /etc/dnsmasq.d/pxe-nic-only.conf from the vars (interface=, except-interface=, no-dhcp-interface=); dnsmasq.conf IP/range from vars. - UFW per interface: imaging NIC 22, 67/udp, 69/udp, 80, 4433, 445, 139, 137/udp, 138/udp, 9009; GE NIC 22 and 9009 only. Old any-interface rules and the stale 9011 rule removed after the new ones are in. - sysctl drop-in: IPv4/IPv6 forwarding off; ip_nonlocal_bind so Apache can bind the imaging IP before the address is up. - Samba [global]: interfaces = 127.0.0.0/8 <imaging NIC>, bind interfaces only. Apache 80 and 4433 on the imaging IP only; 9009 stays on all (TODO: the webapp has no auth). - autoinstall: DHCP only on en*, no static 172.16.9.1; apt geoip off and offline-install fallback now that the corp NIC may have a lease. - Blancco UEFI/Secure Boot: blancco_kver (7.0.0-34-generic) drives the kernel, initramfs and modules builds instead of the running kernel; outputs vmlinuz-ubuntu-70, kexec-initrd-70.img, kmod-<kver>.tar.gz as the menu and blancco-init.sh expect; creates: guards keyed on the full kver. Ubuntu's shimx64.efi.signed published as blancco/shimx64.efi. download-packages.sh adds shim-signed, linux-generic-hwe-24.04 and the pinned linux-image/modules/modules-extra for BLANCCO_KVER. Rendered with the live MACs and names, netplan generate output is identical to the live config; pxe-nic-only.conf and the Samba binding match live directive for directive.
1620 lines
66 KiB
YAML
1620 lines
66 KiB
YAML
---
|
|
- name: PXE Server Setup (Ubuntu with dnsmasq)
|
|
hosts: localhost
|
|
connection: local
|
|
become: yes
|
|
gather_facts: yes
|
|
|
|
pre_tasks:
|
|
# Per-site values (NIC MACs first of all). Optional file next to this
|
|
# playbook; -e still wins over it.
|
|
- name: "Look for per-site vars (playbook/site-vars.yml)"
|
|
stat:
|
|
path: "{{ playbook_dir }}/site-vars.yml"
|
|
register: site_vars_file
|
|
|
|
- name: "Load per-site vars"
|
|
include_vars:
|
|
file: "{{ playbook_dir }}/site-vars.yml"
|
|
when: site_vars_file.stat.exists
|
|
|
|
- name: "Verify required packages are installed (pre-installed from offline .debs)"
|
|
command: dpkg -s {{ item }}
|
|
loop:
|
|
- dnsmasq
|
|
- apache2
|
|
- samba
|
|
- unzip
|
|
- ufw
|
|
- cron
|
|
- ansible
|
|
- wimtools
|
|
- conntrack
|
|
- sbsigntool # sbverify: iPXE update signature checks
|
|
- p7zip-full # 7z: Dell driver packs, catalog cabs, Blancco ISO
|
|
- zip # driver zips PESetup unpacks
|
|
- curl # Updates page downloads
|
|
- shim-signed # shimx64.efi.signed: Blancco with Secure Boot on
|
|
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
|
|
# Secure Boot iPXE chain, fetched into boot-files/ by scripts/fetch-ipxe.sh
|
|
# (keep these pins in step with that script). Every UEFI client gets the
|
|
# dual-signed shim (Microsoft UEFI CA 2011 + 2023), which loads ipxe.efi
|
|
# from the TFTP root: the release's SNPONLY build (firmware network and
|
|
# USB drivers; the full ipxe.efi drives USB itself and kills USB
|
|
# keyboards at the menu). The old Broadcom-signed 1.21.1 build stays on
|
|
# the box as ipxe-bcm.efi, the rollback.
|
|
ipxe_tarball_sha256: "01a526d4cc791fc30362259c609d6c506cc64a7bdff51b9a5eb788354e17eee1" # ipxe v2.0.0 ipxeboot.tar.gz
|
|
ipxe_shim_sha256: "5eecca2780bd49c900565e124516a1bd666ec5e012825f34991b6ba1ef2fa6cf" # ipxe/shim ipxe-16.1 ipxe-shimx64.efi
|
|
ipxe_snponly_sha256: "b1e67c3e4a1e8708ddfd0079ad4505e3a02245acb55ee9a95437ab3c507be82a" # x86_64-sb/snponly.efi
|
|
ipxe_bios_sha256: "0fb21c695c82fd70d4f89732ddcd285063776e2bbd71de9a2c39e9cfe4b180b8" # x86_64/ipxe.pxe
|
|
# Zscaler Root CA: GE's Zscaler re-signs GitHub TLS, so the iPXE update
|
|
# check fails without it. Repo copy first, else the copy on the image share.
|
|
zscaler_ca_src: "{{ playbook_dir }}/certs/zscaler-root-ca.crt"
|
|
# _media/<pctype> links the base image's entries, so also look in the
|
|
# image roots (on a rebuild the _media views may not exist yet).
|
|
zscaler_ca_share_glob: "{{ samba_share }}/_media/*/Deploy/Applications/ZscalerCommercialCertificate-2048-SHA256.crt {{ samba_share }}/gea-*/Deploy/Applications/ZscalerCommercialCertificate-2048-SHA256.crt"
|
|
# --- The two NICs (site standard) ---
|
|
# Every server has two NICs: the imaging NIC (isolated imaging LAN,
|
|
# static pxe_server_ip, the ONLY NIC that serves DHCP/TFTP/HTTP boot/SMB)
|
|
# and the GE corporate NIC (DHCP client; SSH and the webapp on 9009 for
|
|
# admins, outbound updates through Zscaler). Both MACs are REQUIRED:
|
|
# set them in playbook/site-vars.yml on the USB (copy
|
|
# site-vars.example.yml) or with -e. The playbook stops before it
|
|
# touches anything if either is unset or not on this box. Read them
|
|
# with: ip -br link
|
|
pxe_nic_mac: ""
|
|
corp_nic_mac: ""
|
|
# Stable names, set by netplan (match macaddress + set-name). dnsmasq,
|
|
# UFW and Samba refer to these. A site may keep its existing names
|
|
# (e.g. pxe_nic_name: enx34c8d6b11010, corp_nic_name: enp0s31f6).
|
|
pxe_nic_name: "pxe0"
|
|
corp_nic_name: "corp0"
|
|
# Imaging LAN addressing. NOTE: 172.16.9.1 is also hard-coded in the
|
|
# iPXE menu, startnet.cmd, blancco-preferences.xml and the unattend
|
|
# files (see docs/SITE-CUSTOMIZATION.md); change those together.
|
|
pxe_server_ip: "172.16.9.1"
|
|
pxe_subnet: "172.16.9.0/24"
|
|
pxe_prefix: "{{ pxe_subnet.split('/')[1] }}"
|
|
pxe_dhcp_start: "172.16.9.10"
|
|
pxe_dhcp_end: "172.16.9.100"
|
|
pxe_dhcp_lease: "12h"
|
|
# Route metric for the corp NIC's DHCP default route.
|
|
corp_route_metric: 100
|
|
# NIC guard (pxe-nic-check): dnsmasq must never serve DHCP/PXE on the GE
|
|
# corporate network. These are derived from pxe_nic_mac / corp_nic_mac
|
|
# (one source of truth) and written to /etc/pxe-server/nic.conf; do not
|
|
# set them separately (the playbook fails if they disagree). A MAC of a
|
|
# bridge port resolves to the bridge.
|
|
pxe_imaging_mac: "{{ pxe_nic_mac }}"
|
|
pxe_ge_mac: "{{ corp_nic_mac }}"
|
|
# A foreign DHCP server handing out one of these domains (whole labels:
|
|
# wjs.geaerospace.net matches, notgeaerospace.net does not) = GE network.
|
|
pxe_corp_domain_suffixes: "geaerospace.net, ge.com"
|
|
# Only if the IMAGING switch is itself managed and sends LLDP/CDP: its
|
|
# LLDP system name / CDP device id / chassis MAC, comma separated, so
|
|
# it does not count as a corporate switch. 802.1X always counts.
|
|
pxe_nic_allowed_switches: ""
|
|
# UFW, per interface (see the UFW tasks). Imaging NIC: SSH, DHCP, TFTP,
|
|
# HTTP boot, iPXE menu, SMB (+ NetBIOS for older clients), webapp.
|
|
ufw_pxe_rules:
|
|
- { port: "22", proto: tcp }
|
|
- { port: "67", proto: udp }
|
|
- { port: "69", proto: udp }
|
|
- { port: "80", proto: tcp }
|
|
- { port: "4433", proto: tcp }
|
|
- { port: "445", proto: tcp }
|
|
- { port: "139", proto: tcp }
|
|
- { port: "137", proto: udp }
|
|
- { port: "138", proto: udp }
|
|
- { port: "9009", proto: tcp }
|
|
# GE corporate NIC: SSH and the webapp, nothing else.
|
|
ufw_corp_rules:
|
|
- { port: "22", proto: tcp }
|
|
- { port: "9009", proto: tcp }
|
|
# --- Blancco UEFI / Secure Boot path ---
|
|
# Blancco's UEFI entries boot a Canonical-signed Ubuntu HWE kernel
|
|
# (through Ubuntu's shim when Secure Boot is on), then switch_root into
|
|
# the Blancco rootfs. The kernel, its modules and the initramfs are
|
|
# built from this version, which must be installed on the box
|
|
# (linux-generic-hwe-24.04 from the offline packages; see
|
|
# scripts/download-packages.sh BLANCCO_KVER). The file suffix is
|
|
# major+minor ("7.0.0-34-generic" -> "70"), as GetPxeScript.aspx expects:
|
|
# vmlinuz-ubuntu-70, kexec-initrd-70.img; modules are
|
|
# kmod-<blancco_kver>.tar.gz (blancco-init.sh fetches kmod-$(uname -r)).
|
|
blancco_kver: "7.0.0-34-generic"
|
|
blancco_ksuffix: "{{ blancco_kver.split('.')[0] }}{{ blancco_kver.split('.')[1] }}"
|
|
image_types:
|
|
- gea-standard
|
|
- gea-engineer
|
|
- gea-shopfloor
|
|
shopfloor_types:
|
|
- gea-shopfloor
|
|
# Image variants that share the generic Win10/Win11 unattend
|
|
# (gea-standard and gea-engineer both deploy Win11 Pro 24H2 now; the same
|
|
# unattend file drives both because the RunSynchronous entries are
|
|
# OS-version-agnostic).
|
|
standard_types:
|
|
- gea-standard
|
|
- gea-engineer
|
|
deploy_subdirs:
|
|
- Applications
|
|
- Control
|
|
- "Operating Systems"
|
|
- "Out-of-box Drivers"
|
|
- Packages
|
|
- Tools
|
|
|
|
tasks:
|
|
# --- The two NICs: validate, then pin by MAC (netplan) ---
|
|
# Replaces the old guess (first en* without 172.16.9.1 / the default
|
|
# route), which on a two-NIC box could pick the GE NIC.
|
|
- name: "NICs: check the per-site NIC vars"
|
|
assert:
|
|
that:
|
|
- pxe_nic_mac | length > 0
|
|
- corp_nic_mac | length > 0
|
|
- pxe_nic_mac is match('^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$')
|
|
- corp_nic_mac is match('^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$')
|
|
- pxe_nic_mac | lower != corp_nic_mac | lower
|
|
- pxe_imaging_mac | lower == pxe_nic_mac | lower
|
|
- pxe_ge_mac | lower == corp_nic_mac | lower
|
|
- pxe_nic_name | length > 0 and pxe_nic_name | length <= 15
|
|
- corp_nic_name | length > 0 and corp_nic_name | length <= 15
|
|
- pxe_nic_name != corp_nic_name
|
|
fail_msg: >-
|
|
Set pxe_nic_mac (imaging NIC) and corp_nic_mac (GE corporate NIC)
|
|
to two different MACs (aa:bb:cc:dd:ee:ff) in playbook/site-vars.yml
|
|
(copy site-vars.example.yml) or with -e, then re-run. Do not set
|
|
pxe_imaging_mac / pxe_ge_mac yourself; they follow these. Names
|
|
(pxe_nic_name, corp_nic_name) must differ and be 1-15 chars.
|
|
Now: pxe_nic_mac='{{ pxe_nic_mac }}' corp_nic_mac='{{ corp_nic_mac }}'.
|
|
quiet: yes
|
|
|
|
- name: "NICs: list the physical NICs on this box"
|
|
shell: |
|
|
for d in /sys/class/net/*; do
|
|
[ -e "$d/device" ] || continue
|
|
echo "$(basename "$d") $(cat "$d/address")"
|
|
done
|
|
register: nic_list
|
|
changed_when: false
|
|
|
|
- name: "NICs: both MACs must be on this box"
|
|
vars:
|
|
box_macs: "{{ nic_list.stdout_lines | map('regex_replace', '^.* ', '') | map('lower') | list }}"
|
|
assert:
|
|
that:
|
|
- pxe_nic_mac | lower in box_macs
|
|
- corp_nic_mac | lower in box_macs
|
|
fail_msg: >-
|
|
{{ 'pxe_nic_mac ' ~ pxe_nic_mac ~ ' is not on this box. ' if pxe_nic_mac | lower not in box_macs else '' }}{{ 'corp_nic_mac ' ~ corp_nic_mac ~ ' is not on this box. ' if corp_nic_mac | lower not in box_macs else '' }}Nothing was changed (the network config would have left that NIC
|
|
unconfigured). Physical NICs here (name MAC):
|
|
{{ nic_list.stdout_lines | join('; ') }}.
|
|
Fix playbook/site-vars.yml and re-run.
|
|
quiet: yes
|
|
|
|
- name: "NICs: stop cloud-init from rewriting the network config"
|
|
copy:
|
|
dest: /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
|
|
mode: '0644'
|
|
content: |
|
|
# Managed by pxe_server_setup.yml: /etc/netplan/50-cloud-init.yaml
|
|
# is the playbook's two-NIC config, not cloud-init's.
|
|
network: {config: disabled}
|
|
|
|
- name: "NICs: deploy the netplan apply helper"
|
|
copy:
|
|
src: "{{ usb_mount }}/pxe-server-helpers/pxe-netplan-apply.sh"
|
|
dest: /usr/local/sbin/pxe-netplan-apply.sh
|
|
mode: '0755'
|
|
|
|
- name: "NICs: stage the two-NIC netplan config"
|
|
template:
|
|
src: templates/netplan-pxe.yaml.j2
|
|
dest: /etc/pxe-server-netplan.yaml
|
|
mode: '0600'
|
|
|
|
- name: "NICs: validate, install and apply netplan (rolls back on failure)"
|
|
command: >-
|
|
/usr/local/sbin/pxe-netplan-apply.sh /etc/pxe-server-netplan.yaml
|
|
{{ pxe_nic_name }} {{ pxe_nic_mac | lower }}
|
|
{{ corp_nic_name }} {{ corp_nic_mac | lower }}
|
|
register: netplan_apply
|
|
changed_when: netplan_apply.stdout_lines | last | default('') == 'CHANGED'
|
|
|
|
- name: "NICs: result"
|
|
debug:
|
|
msg: "{{ netplan_apply.stdout_lines }}"
|
|
|
|
- name: "Pin IP forwarding off (the imaging LAN is never routed)"
|
|
copy:
|
|
dest: /etc/sysctl.d/99-pxe-net.conf
|
|
mode: '0644'
|
|
content: |
|
|
# Managed by pxe_server_setup.yml. The server never routes between
|
|
# the imaging LAN and the GE corporate network.
|
|
net.ipv4.ip_forward = 0
|
|
net.ipv6.conf.all.forwarding = 0
|
|
net.ipv6.conf.default.forwarding = 0
|
|
# Apache listens on {{ pxe_server_ip }} only (ports 80/4433). Let it
|
|
# bind before networkd has put the address on {{ pxe_nic_name }} (no
|
|
# carrier at boot, or apache starting first), or apache2 fails to
|
|
# start and takes the webapp on 9009 down with it.
|
|
net.ipv4.ip_nonlocal_bind = 1
|
|
register: pxe_net_sysctl
|
|
|
|
- name: "Apply the network sysctls now (Apache restarts before the handlers)"
|
|
command: sysctl -p /etc/sysctl.d/99-pxe-net.conf
|
|
when: pxe_net_sysctl is changed
|
|
|
|
- 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: "Deploy dnsmasq dhcp-script for per-lease state cleanup"
|
|
copy:
|
|
src: "{{ usb_mount }}/pxe-server-helpers/pxe-dhcp-hook.sh"
|
|
dest: /usr/local/sbin/pxe-dhcp-hook.sh
|
|
mode: '0755'
|
|
|
|
- name: "Configure dnsmasq for DHCP and TFTP"
|
|
copy:
|
|
dest: /etc/dnsmasq.conf
|
|
backup: yes
|
|
content: |
|
|
port=0
|
|
listen-address={{ pxe_server_ip }}
|
|
bind-dynamic
|
|
# Interface pinning (serve {{ pxe_nic_name }} only, never
|
|
# {{ corp_nic_name }}) is in /etc/dnsmasq.d/pxe-nic-only.conf.
|
|
dhcp-range={{ pxe_dhcp_start }},{{ pxe_dhcp_end }},{{ pxe_dhcp_lease }}
|
|
# No default gateway (option 3) and no DNS (option 6) handed out:
|
|
# the PXE network is isolated and the PXE server does not forward
|
|
# internet traffic. Previously we set both, which made imaged PCs
|
|
# add a default route via 172.16.9.1 and prefer it over WiFi (lower
|
|
# interface metric). PPKG / Intune enrollment then black-holed
|
|
# internet-bound traffic. The fix used to be migrate-to-wifi.ps1
|
|
# disabling the wired NIC during first-logon, which created an
|
|
# eDNC race (10022 socket errors until the SYSTEM task re-enabled
|
|
# the wired NIC much later). Removing these options entirely lets
|
|
# Windows route internet via WiFi and same-subnet PXE/SMB traffic
|
|
# via wired, no migration script needed.
|
|
#
|
|
# Important: dnsmasq DEFAULTS to sending its own listening address as
|
|
# both router and DNS when these options are unset. Commenting them
|
|
# out is NOT the same as disabling - imaged PCs (and Blancco PXE
|
|
# clients) end up with {{ pxe_server_ip }} as gateway. The empty-value form
|
|
# below explicitly suppresses both options.
|
|
dhcp-option=3
|
|
dhcp-option=6
|
|
enable-tftp
|
|
tftp-root={{ tftp_dir }}
|
|
# Arch-aware NBP for Blancco/WinPE PXE clients:
|
|
# - legacy BIOS (vendorclass PXEClient:Arch:00000) -> ipxe.pxe, a
|
|
# BIOS full-feature iPXE build with an HTTP/TCP stack.
|
|
# - iPXE-running clients (userclass "iPXE") -> chain to the HTTP menu
|
|
# - every UEFI client -> ipxe-shim.efi, iPXE's Microsoft-signed shim
|
|
# (dual-signed, UEFI CA 2011 + 2023). Over TFTP it always loads
|
|
# ipxe.efi from the TFTP root, which is the iPXE snponly build
|
|
# (see the ipxe_* vars). Rollback: point the last line back at
|
|
# ipxe-bcm.efi (the old Broadcom-signed 1.21.1 build).
|
|
dhcp-vendorclass=set:bios,PXEClient:Arch:00000
|
|
dhcp-userclass=set:ipxe,iPXE
|
|
dhcp-boot=tag:bios,tag:!ipxe,ipxe.pxe
|
|
dhcp-boot=tag:ipxe,http://{{ pxe_server_ip }}:4433/Altiris/iPXE/GetPxeScript.aspx
|
|
dhcp-boot=tag:!bios,tag:!ipxe,ipxe-shim.efi
|
|
log-dhcp
|
|
# Per-lease state cleanup: flush conntrack + port-445 sockets for
|
|
# the client IP on add/del. Prevents "System error 53" when a PXE
|
|
# client re-images the same machine and hits a stale SMB session.
|
|
# Script runs as root by default (dnsmasq --dhcp-scriptuser default).
|
|
dhcp-script=/usr/local/sbin/pxe-dhcp-hook.sh
|
|
# Also call it when a client comes back to its existing lease (a
|
|
# re-image of the same PC), which dnsmasq otherwise reports only if
|
|
# the hostname or MAC changed. The hook acts on those only for PXE
|
|
# firmware / iPXE requests.
|
|
script-on-renewal
|
|
|
|
- name: "Pin dnsmasq to the imaging NIC (never DHCP/PXE on the GE NIC)"
|
|
template:
|
|
src: templates/dnsmasq-pxe-nic-only.conf.j2
|
|
dest: /etc/dnsmasq.d/pxe-nic-only.conf
|
|
mode: '0644'
|
|
backup: yes
|
|
|
|
# --- NIC guard: keep dnsmasq off the GE corporate network ---
|
|
# pxe-nic-check classifies each NIC from what it hears (LLDP/CDP/802.1X
|
|
# from managed switches, foreign DHCP offers and their domain). The
|
|
# dnsmasq drop-in runs it as ExecStartPre (--guard: dnsmasq refuses to
|
|
# start if the imaging NIC is on the GE network or next to another DHCP
|
|
# server); the timer re-checks every 5 min (--watch: stops dnsmasq) and
|
|
# writes /run/pxe-nic-status.json for the webapp. Operator notes and
|
|
# the floor tests: docs/pxe-nic-guard.md.
|
|
- name: "NIC guard: deploy pxe-nic-check"
|
|
copy:
|
|
src: "{{ usb_mount }}/pxe-server-helpers/pxe-nic-check"
|
|
dest: /usr/local/sbin/pxe-nic-check
|
|
mode: '0755'
|
|
|
|
- name: "NIC guard: create /etc/pxe-server"
|
|
file:
|
|
path: /etc/pxe-server
|
|
state: directory
|
|
mode: '0755'
|
|
|
|
- name: "NIC guard: write /etc/pxe-server/nic.conf"
|
|
copy:
|
|
dest: /etc/pxe-server/nic.conf
|
|
mode: '0644'
|
|
content: |
|
|
# Managed by pxe_server_setup.yml (vars pxe_nic_mac, corp_nic_mac,
|
|
# pxe_server_ip, pxe_corp_domain_suffixes). Read by
|
|
# /usr/local/sbin/pxe-nic-check.
|
|
[nic]
|
|
imaging_mac = {{ pxe_imaging_mac | lower }}
|
|
ge_mac = {{ pxe_ge_mac | lower }}
|
|
corp_domain_suffixes = {{ pxe_corp_domain_suffixes }}
|
|
own_ip = {{ pxe_server_ip }}
|
|
allowed_switches = {{ pxe_nic_allowed_switches }}
|
|
|
|
- name: "NIC guard: create dnsmasq drop-in directory"
|
|
file:
|
|
path: /etc/systemd/system/dnsmasq.service.d
|
|
state: directory
|
|
mode: '0755'
|
|
|
|
- name: "NIC guard: install dnsmasq ExecStartPre drop-in"
|
|
copy:
|
|
src: "{{ usb_mount }}/pxe-server-helpers/dnsmasq.service.d/nic-guard.conf"
|
|
dest: /etc/systemd/system/dnsmasq.service.d/nic-guard.conf
|
|
mode: '0644'
|
|
|
|
- name: "NIC guard: install watch service and timer"
|
|
copy:
|
|
src: "{{ usb_mount }}/pxe-server-helpers/{{ item }}"
|
|
dest: "/etc/systemd/system/{{ item }}"
|
|
mode: '0644'
|
|
loop:
|
|
- pxe-nic-check.service
|
|
- pxe-nic-check.timer
|
|
|
|
- name: "NIC guard: enable watch timer"
|
|
systemd:
|
|
name: pxe-nic-check.timer
|
|
state: started
|
|
enabled: yes
|
|
daemon_reload: yes
|
|
|
|
- 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: "Install GetPxeScript.aspx (iPXE boot menu)"
|
|
# The live menu, tracked as a file (playbook/ipxe/GetPxeScript.aspx).
|
|
# WinPE skips the Secure Boot warning when iPXE reads Secure Boot as on
|
|
# (iPXE 2.0 ${efi/SecureBoot}); Blancco starts through the Ubuntu shim
|
|
# (iPXE shim command) so it runs with Secure Boot on. The Blancco
|
|
# Releases page edits this file (test entry), so keep exactly one
|
|
# "item blancco" line and the :blancco_efi kernel+initrd block.
|
|
copy:
|
|
src: "{{ usb_mount }}/ipxe/GetPxeScript.aspx"
|
|
dest: "{{ web_root }}/Altiris/iPXE/GetPxeScript.aspx"
|
|
mode: '0644'
|
|
backup: yes
|
|
|
|
# Boot files (80) and the iPXE menu (4433) are served on the imaging IP
|
|
# only, never on the GE NIC. 9009 (webapp) stays on all addresses: admins
|
|
# reach it from the GE side (see pxe-server.conf below).
|
|
- name: "Apache: serve port 80 on the imaging IP only"
|
|
lineinfile:
|
|
path: /etc/apache2/ports.conf
|
|
regexp: '^\s*Listen\s+(\S+:)?80\s*$'
|
|
line: "Listen {{ pxe_server_ip }}:80"
|
|
backup: yes
|
|
state: present
|
|
|
|
- name: "Apache: serve port 4433 (iPXE menu) on the imaging IP only"
|
|
lineinfile:
|
|
path: /etc/apache2/ports.conf
|
|
regexp: '^\s*Listen\s+(\S+:)?4433\s*$'
|
|
line: "Listen {{ pxe_server_ip }}: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 share with internal taxonomy"
|
|
file:
|
|
path: "/srv/samba/enrollment/{{ item }}"
|
|
state: directory
|
|
mode: '0777'
|
|
loop:
|
|
- ""
|
|
- ppkgs
|
|
- scripts
|
|
- config
|
|
- shopfloor-setup
|
|
- pre-install
|
|
- pre-install/bios
|
|
- pre-install/installers
|
|
- installers-post
|
|
- installers-post/cmm
|
|
- blancco
|
|
- logs
|
|
|
|
- name: "Deploy enrollment share README"
|
|
copy:
|
|
dest: /srv/samba/enrollment/README.md
|
|
mode: '0644'
|
|
content: |
|
|
# Enrollment Share Layout
|
|
Single SMB share mounted by WinPE as Y: during imaging. Subdir layout:
|
|
- ppkgs/ GCCH bulk-enrollment PPKGs
|
|
- scripts/ run-enrollment.ps1, wait-for-internet.ps1, migrate-to-wifi.ps1
|
|
- config/ site-config.json, FlatUnattendW10*.xml, per-site overrides
|
|
- shopfloor-setup/ Per-PC-type post-imaging scripts
|
|
- pre-install/ WinPE-phase content (bios/, installers/, preinstall.json)
|
|
- installers-post/ Post-OOBE app installers (cmm/PCDMIS, etc.)
|
|
- blancco/ Blancco custom images / configs
|
|
- logs/ Client log uploads
|
|
|
|
- name: "Deploy PPKG enrollment packages to ppkgs/"
|
|
shell: |
|
|
set +e
|
|
# Copy any whole PPKGs (small enough to fit on FAT32)
|
|
cp -f {{ usb_root }}/enrollment/*.ppkg /srv/samba/enrollment/ppkgs/ 2>/dev/null
|
|
# Reassemble any split files (foo.ppkg.part.00, .01, ... -> foo.ppkg)
|
|
for first in {{ usb_root }}/enrollment/*.part.00; do
|
|
[ -e "$first" ] || continue
|
|
base="${first%.part.00}"
|
|
name="$(basename "$base")"
|
|
echo "Reassembling $name from chunks..."
|
|
cat "${base}.part."* > "/srv/samba/enrollment/ppkgs/$name"
|
|
done
|
|
ls -lh /srv/samba/enrollment/ppkgs/*.ppkg 2>/dev/null
|
|
ignore_errors: yes
|
|
|
|
- name: "Deploy enrollment scripts to scripts/"
|
|
copy:
|
|
src: "{{ item.src }}"
|
|
dest: "/srv/samba/enrollment/scripts/{{ item.dest }}"
|
|
mode: '0644'
|
|
loop:
|
|
- { src: "{{ usb_mount }}/shopfloor-setup/run-enrollment.ps1", dest: "run-enrollment.ps1" }
|
|
- { src: "{{ usb_mount }}/wait-for-internet.ps1", dest: "wait-for-internet.ps1" }
|
|
- { src: "{{ usb_mount }}/migrate-to-wifi.ps1", dest: "migrate-to-wifi.ps1" }
|
|
- { src: "{{ usb_mount }}/winpe-status-push.ps1", dest: "winpe-status-push.ps1" }
|
|
ignore_errors: yes
|
|
|
|
- name: "Deploy site-config.json to config/"
|
|
copy:
|
|
src: "{{ usb_mount }}/shopfloor-setup/site-config.json"
|
|
dest: /srv/samba/enrollment/config/site-config.json
|
|
mode: '0644'
|
|
ignore_errors: yes
|
|
|
|
- name: "Deploy Dell driver packs to shared Out-of-box Drivers"
|
|
args:
|
|
executable: /bin/bash
|
|
shell: |
|
|
set +e
|
|
SRC="{{ usb_root }}/drivers"
|
|
DEST="/srv/samba/winpeapps/_shared/Out-of-box Drivers/Dell_11"
|
|
if [ ! -d "$SRC" ]; then
|
|
echo "No drivers/ on USB - skipping"
|
|
exit 0
|
|
fi
|
|
mkdir -p "$DEST"
|
|
# Copy everything except split chunks
|
|
rsync -a --exclude='*.part.*' "$SRC/" "$DEST/"
|
|
# Reassemble any split driver files
|
|
while IFS= read -r first; do
|
|
base="${first%.part.00}"
|
|
rel="${base#$SRC/}"
|
|
out="$DEST/$rel"
|
|
mkdir -p "$(dirname "$out")"
|
|
echo "Reassembling $rel from chunks..."
|
|
cat "${base}.part."* > "$out"
|
|
done < <(find "$SRC" -name '*.part.00')
|
|
echo "Deployed Dell drivers from USB"
|
|
ignore_errors: yes
|
|
|
|
- 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: "Deploy preinstall.json to pre-install/"
|
|
copy:
|
|
src: "{{ usb_mount }}/preinstall/preinstall.json"
|
|
dest: /srv/samba/enrollment/pre-install/preinstall.json
|
|
mode: '0644'
|
|
ignore_errors: yes
|
|
|
|
# Oracle Client 11.2 is the first app the preinstall runner executes, so it
|
|
# has to be present on-disk before the first client images. The 686 MB
|
|
# Oracle_OracleDatabase_11r2_V03.zip doesn't belong in git; it rides on the
|
|
# USB under oracle/ alongside the BIOS exes. Install-Oracle11r2.cmd is
|
|
# tracked at playbook/preinstall/oracle/ and arrives via usb_mount.
|
|
- name: "Create pre-install/installers/oracle/ staging dir"
|
|
file:
|
|
path: /srv/samba/enrollment/pre-install/installers/oracle
|
|
state: directory
|
|
mode: '0755'
|
|
ignore_errors: yes
|
|
|
|
- name: "Deploy Install-Oracle11r2.cmd to pre-install/installers/oracle/"
|
|
copy:
|
|
src: "{{ usb_mount }}/preinstall/oracle/Install-Oracle11r2.cmd"
|
|
dest: /srv/samba/enrollment/pre-install/installers/oracle/Install-Oracle11r2.cmd
|
|
mode: '0755'
|
|
ignore_errors: yes
|
|
|
|
# Per-file overrides for the .ora config dropped by the wrapper post-install.
|
|
# The zip's client/ora/ contains dated defaults; shipping an updated
|
|
# tnsnames.ora alongside the wrapper lets us refresh the DB catalog without
|
|
# repackaging the 686 MB zip. sqlnet.ora and ldap.ora can be added here the
|
|
# same way if they ever need to diverge from the zip's bundled copies.
|
|
- name: "Deploy updated tnsnames.ora override to pre-install/installers/oracle/"
|
|
copy:
|
|
src: "{{ usb_mount }}/preinstall/oracle/tnsnames.ora"
|
|
dest: /srv/samba/enrollment/pre-install/installers/oracle/tnsnames.ora
|
|
mode: '0644'
|
|
ignore_errors: yes
|
|
|
|
- name: "Deploy Oracle 11.2 zip (686 MB) from USB to pre-install/installers/oracle/"
|
|
shell: >
|
|
if [ -f "{{ usb_root }}/oracle/Oracle_OracleDatabase_11r2_V03.zip" ]; then
|
|
cp -f "{{ usb_root }}/oracle/Oracle_OracleDatabase_11r2_V03.zip" \
|
|
/srv/samba/enrollment/pre-install/installers/oracle/
|
|
echo "Deployed Oracle 11.2 zip ($(stat -c %s /srv/samba/enrollment/pre-install/installers/oracle/Oracle_OracleDatabase_11r2_V03.zip) bytes)"
|
|
else
|
|
echo "WARNING: {{ usb_root }}/oracle/Oracle_OracleDatabase_11r2_V03.zip not on USB - preinstall runner will fail on Oracle step until this file is staged at /srv/samba/enrollment/pre-install/installers/oracle/"
|
|
fi
|
|
ignore_errors: yes
|
|
|
|
- name: "Ensure winpeapps/_shared/BIOS directory exists"
|
|
file:
|
|
path: /srv/samba/winpeapps/_shared/BIOS
|
|
state: directory
|
|
mode: '0755'
|
|
|
|
- name: "Ensure driver store is owned by pxe (download-drivers.py pushes here over ssh as the pxe user; root/webapp still write it fine)"
|
|
file:
|
|
path: "/srv/samba/winpeapps/_shared/Out-of-box Drivers"
|
|
state: directory
|
|
owner: pxe
|
|
group: pxe
|
|
mode: '0775'
|
|
|
|
- name: "Deploy BIOS check script + manifest to winpeapps/_shared/BIOS/"
|
|
# Path matches what startnet.cmd reads at WinPE boot:
|
|
# net use B: \\172.16.9.1\winpeapps\_shared
|
|
# if exist B:\BIOS\check-bios.cmd ...
|
|
# Earlier deploy targeted enrollment/pre-install/bios/ (different share)
|
|
# which startnet.cmd never read, so BIOS_STATUS perma-stuck on
|
|
# "No BIOS check (share unavailable)". Corrected 2026-04-28.
|
|
copy:
|
|
src: "{{ usb_mount }}/shopfloor-setup/BIOS/{{ item }}"
|
|
dest: "/srv/samba/winpeapps/_shared/BIOS/{{ item }}"
|
|
mode: '0644'
|
|
loop:
|
|
- check-bios.cmd
|
|
- models.txt
|
|
- hardware-report.ps1
|
|
ignore_errors: yes
|
|
|
|
- name: "Deploy Dell Flash64W.exe to winpeapps/_shared/BIOS/"
|
|
copy:
|
|
src: "{{ usb_root }}/bios/Flash64W.exe"
|
|
dest: /srv/samba/winpeapps/_shared/BIOS/Flash64W.exe
|
|
mode: '0644'
|
|
ignore_errors: yes
|
|
|
|
- name: "Deploy BIOS update binaries from USB to winpeapps/_shared/BIOS/"
|
|
shell: >
|
|
if [ -d "{{ usb_root }}/bios" ]; then
|
|
cp -f {{ usb_root }}/bios/*.exe /srv/samba/winpeapps/_shared/BIOS/ 2>/dev/null || true
|
|
count=$(find /srv/samba/winpeapps/_shared/BIOS -name '*.exe' | wc -l)
|
|
echo "Deployed $count BIOS binaries"
|
|
else
|
|
echo "No bios/ on USB - skipping"
|
|
fi
|
|
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
|
|
# Modern Samba silently disables wide links WITHOUT this. The shared
|
|
# image dirs are served via symlinks into _shared, so it is required
|
|
# or WinPE clients cannot follow them.
|
|
allow insecure wide links = yes
|
|
|
|
# Runs after the symlinks block so it lands first under [global], as on
|
|
# the live server.
|
|
- name: "Bind Samba to the imaging NIC only"
|
|
blockinfile:
|
|
path: /etc/samba/smb.conf
|
|
backup: yes
|
|
marker: "# {mark} MANAGED - PXE NIC BIND"
|
|
insertafter: '\[global\]'
|
|
block: |
|
|
# Bind Samba to the PXE LAN only. The second NIC reaches the GE corporate
|
|
# network, and these shares (blancco-reports, enrollment, image-upload,
|
|
# clonezilla) use shared, well-known imaging passwords and are writable by
|
|
# design. Without this they are offered to the corporate network the moment
|
|
# the cable is plugged in.
|
|
interfaces = 127.0.0.0/8 {{ pxe_nic_name }}
|
|
bind interfaces only = yes
|
|
|
|
- name: "Samba SMB session handling for WinPE re-image robustness"
|
|
blockinfile:
|
|
path: /etc/samba/smb.conf
|
|
backup: yes
|
|
marker: "# {mark} MANAGED - PXE REIMAGE FIX"
|
|
insertafter: "# END MANAGED - GLOBAL SYMLINKS"
|
|
block: |
|
|
# Reduce the chance a WinPE client rebooting mid-imaging leaves a
|
|
# stale session on the server that blocks its next connection
|
|
# attempt with "System error 53 network path not found". Combined
|
|
# with /etc/sysctl.d/99-pxe-conntrack.conf (shorter nf_conntrack
|
|
# TCP timeouts) this keeps the conntrack + smbd state in sync with
|
|
# the short-lived flows that PXE imaging produces.
|
|
socket options = TCP_NODELAY SO_KEEPALIVE IPTOS_LOWDELAY
|
|
keepalive = 30
|
|
# deadtime=0 (disabled): WinPE maps the enrollment share early then
|
|
# idles for minutes during the WIM apply. A non-zero deadtime drops
|
|
# that idle session, so the post-apply staging copies failed (bay
|
|
# left with only site-config.json). 0 = never auto-disconnect idle.
|
|
deadtime = 0
|
|
|
|
- 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
|
|
|
|
[winpeapps_bios]
|
|
path = /srv/samba/winpeapps/_shared/BIOS
|
|
browseable = no
|
|
read only = yes
|
|
guest ok = no
|
|
valid users = pxe-upload
|
|
comment = Dell BIOS update bundle (read-only, served to WinPE)
|
|
|
|
- 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: "Deploy nf_conntrack TCP timeout sysctl for PXE workload"
|
|
copy:
|
|
src: "{{ usb_mount }}/pxe-server-helpers/99-pxe-conntrack.conf"
|
|
dest: /etc/sysctl.d/99-pxe-conntrack.conf
|
|
mode: '0644'
|
|
notify: reload sysctl
|
|
|
|
- name: "Load nf_conntrack at boot so the conntrack sysctl applies"
|
|
# systemd-sysctl runs before ufw loads nf_conntrack, so without this the
|
|
# net.netfilter.* keys do not exist yet at boot and the 1 h timeout is
|
|
# silently skipped (the kernel default of 5 days stays in force).
|
|
copy:
|
|
dest: /etc/modules-load.d/pxe-conntrack.conf
|
|
content: "nf_conntrack\n"
|
|
mode: '0644'
|
|
|
|
- name: "Deploy SMB diagnostic + soft-reset helper scripts"
|
|
copy:
|
|
src: "{{ usb_mount }}/pxe-server-helpers/{{ item }}"
|
|
dest: "/usr/local/sbin/{{ item }}"
|
|
mode: '0755'
|
|
loop:
|
|
- smb-diag.sh
|
|
- smb-soft-reset.sh
|
|
|
|
- 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: yes - repo is source of truth. force: no let the live shopfloor
|
|
# unattend go stale (missing the Fetch + Verify-And-Heal staging steps),
|
|
# and a playbook run never repaired it. Keep it in sync like the standard
|
|
# /engineer unattend below.
|
|
force: yes
|
|
loop: "{{ shopfloor_types }}"
|
|
ignore_errors: yes
|
|
|
|
# gea-standard and gea-engineer each get their OWN unattend. They are not
|
|
# interchangeable: the engineer file carries FirstLogonCommands Order 11,
|
|
# which installs the Engineering Layer, and nothing else differs. Deploying
|
|
# one shared template to both - which this task used to do - silently
|
|
# stripped that entry from gea-engineer on every playbook run.
|
|
#
|
|
# force: yes because drift between these files and the share is what caused
|
|
# the Win10/Win11 search-cleanup regression (d49f516). That only works if
|
|
# the repo copies are genuinely current: on 2026-08-19 this template was
|
|
# found to be missing specialize Orders 39-48 - including Order 39, which
|
|
# registers the bulk-enrollment orchestrator - so a playbook run would have
|
|
# removed enrollment from every standard and engineer bay. Both files are
|
|
# now reconciled against the live share. Re-check before trusting force: yes.
|
|
- name: "Deploy standard/engineer unattend.xml (per-type template)"
|
|
copy:
|
|
src: "{{ usb_mount }}/{{ item.src }}"
|
|
dest: "{{ samba_share }}/{{ item.dest }}/Deploy/FlatUnattendW10.xml"
|
|
mode: '0644'
|
|
force: yes
|
|
loop:
|
|
- { src: 'FlatUnattendW10.xml', dest: 'gea-standard' }
|
|
- { src: 'FlatUnattendW10-engineer.xml', dest: 'gea-engineer' }
|
|
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: "Install the Secure Boot iPXE chain into the TFTP root (pinned, verified)"
|
|
# Not ignore_errors: without these files no client can boot. Files come
|
|
# from boot-files/ via scripts/fetch-ipxe.sh; each is checked against
|
|
# its pin before anything is replaced, and each is put in place by
|
|
# rename so a client that is booting never reads half a file.
|
|
shell: |
|
|
set -euo pipefail
|
|
U="{{ usb_root }}"; T="{{ tftp_dir }}"
|
|
check() { echo "$2 $1" | sha256sum -c --status || { echo "sha256 mismatch or missing: $1" >&2; exit 1; }; }
|
|
check "$U/ipxeboot.tar.gz" "{{ ipxe_tarball_sha256 }}"
|
|
check "$U/ipxe-shimx64.efi" "{{ ipxe_shim_sha256 }}"
|
|
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
|
|
tar -xzf "$U/ipxeboot.tar.gz" -C "$W" ipxeboot/x86_64-sb/snponly.efi ipxeboot/x86_64/ipxe.pxe
|
|
check "$W/ipxeboot/x86_64-sb/snponly.efi" "{{ ipxe_snponly_sha256 }}"
|
|
check "$W/ipxeboot/x86_64/ipxe.pxe" "{{ ipxe_bios_sha256 }}"
|
|
put() { install -m 0644 -o root -g root "$1" "$T/.$2.new" && mv -f "$T/.$2.new" "$T/$2"; }
|
|
put "$U/ipxe-shimx64.efi" ipxe-shim.efi
|
|
put "$W/ipxeboot/x86_64-sb/snponly.efi" ipxe.efi
|
|
put "$W/ipxeboot/x86_64/ipxe.pxe" ipxe.pxe
|
|
# Rollback boot file: the old Broadcom-signed build (boot-files/ipxe.efi).
|
|
if [ -f "$U/ipxe.efi" ]; then put "$U/ipxe.efi" ipxe-bcm.efi; fi
|
|
echo "installed ipxe-shim.efi, ipxe.efi (snponly), ipxe.pxe$( [ -f "$U/ipxe.efi" ] && echo ', ipxe-bcm.efi')"
|
|
args:
|
|
executable: /bin/bash
|
|
register: ipxe_chain
|
|
changed_when: true
|
|
|
|
- name: "Show the installed iPXE chain"
|
|
debug:
|
|
msg: "{{ ipxe_chain.stdout }}"
|
|
|
|
- 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.
|
|
# Kernel, modules and initramfs all come from blancco_kver (the HWE
|
|
# kernel), not the server's running kernel: the server may run 6.8 while
|
|
# Blancco boots 7.0. Output names carry the version so a changed
|
|
# blancco_kver rebuilds instead of being skipped by the creates: guards.
|
|
|
|
- name: "Blancco: is the Blancco kernel ({{ blancco_kver }}) installed?"
|
|
stat:
|
|
path: "{{ item }}"
|
|
loop:
|
|
- "/boot/vmlinuz-{{ blancco_kver }}"
|
|
- "/lib/modules/{{ blancco_kver }}/modules.dep"
|
|
register: blancco_kernel_files
|
|
|
|
- name: "Blancco: kernel check result"
|
|
set_fact:
|
|
blancco_kernel_ok: "{{ blancco_kernel_files.results | map(attribute='stat.exists') | min }}"
|
|
|
|
- name: "Blancco: warn when the Blancco kernel is missing"
|
|
debug:
|
|
msg: >-
|
|
WARNING: /boot/vmlinuz-{{ blancco_kver }} or
|
|
/lib/modules/{{ blancco_kver }} is missing, so the Blancco UEFI
|
|
files (vmlinuz-ubuntu-{{ blancco_ksuffix }},
|
|
kexec-initrd-{{ blancco_ksuffix }}.img,
|
|
kmod-{{ blancco_kver }}.tar.gz) are NOT built. Install
|
|
linux-image-{{ blancco_kver }} + linux-modules-extra-{{ blancco_kver }}
|
|
(scripts/download-packages.sh, BLANCCO_KVER) or set blancco_kver
|
|
to an installed version (ls /lib/modules), then re-run.
|
|
when: not blancco_kernel_ok
|
|
|
|
- name: "Build Blancco PXE initramfs (kexec-initrd-{{ blancco_ksuffix }}.img)"
|
|
# The narrow hand-picked NIC driver list used before 2026-04-22 produced
|
|
# a 2 MB initramfs that hung on "waiting for network interface" for any
|
|
# hardware outside e1000e/igb/tg3/bnx2/bnxt_en/b44. This rewrite sweeps
|
|
# the full drivers/net/ tree (ethernet + phy + mdio + usb + fddi + wan)
|
|
# plus overlay / squashfs / loop / ptp / libphy / mii deps, runs depmod
|
|
# so blancco-init.sh can use modprobe with proper dependency resolution,
|
|
# and produces a ~20 MB initramfs. Size isn't a concern - HTTP loads it
|
|
# in under two seconds at gigabit, versus the many minutes of hangtime
|
|
# the narrow build cost us when a NIC was unsupported.
|
|
args:
|
|
executable: /bin/bash
|
|
# Stamp named after the full version: 7.0.0-34 -> 7.0.0-40 keeps the
|
|
# file name (suffix 70) but must rebuild.
|
|
creates: "{{ web_root }}/blancco/.kexec-initrd-{{ blancco_ksuffix }}.{{ blancco_kver }}.built"
|
|
when: blancco_kernel_ok
|
|
shell: |
|
|
set -e
|
|
WORK=$(mktemp -d)
|
|
KVER="{{ blancco_kver }}"
|
|
OUT="{{ web_root }}/blancco/kexec-initrd-{{ blancco_ksuffix }}.img"
|
|
mkdir -p "$WORK"/{bin,lib/modules/$KVER/kernel,lib64,sbin,usr/share/udhcpc,etc,run,proc,sys,dev}
|
|
|
|
# Busybox (static) - bundled on USB at playbook/busybox-static
|
|
if [ -f /bin/busybox ]; then
|
|
cp /bin/busybox "$WORK/bin/busybox"
|
|
elif [ -f "{{ usb_root }}/playbook/busybox-static" ]; then
|
|
cp "{{ usb_root }}/playbook/busybox-static" "$WORK/bin/busybox"
|
|
else
|
|
echo "ERROR: No busybox available (not at /bin/busybox or on USB)"
|
|
exit 1
|
|
fi
|
|
chmod +x "$WORK/bin/busybox"
|
|
# All applets blancco-init.sh uses: modprobe, insmod, dmesg, find, env
|
|
# and export added vs the old narrow list.
|
|
for cmd in sh ash awk cat chmod cp dd echo grep gunzip ifconfig ip insmod ln losetup ls mkdir mknod modprobe mount mv reboot rm rmdir route sed sleep switch_root tar udhcpc umount wget cpio dmesg env export find; do
|
|
ln -sf busybox "$WORK/bin/$cmd"
|
|
done
|
|
|
|
# Full drivers/net/ tree - ethernet + phy + mdio + usb + fddi + wan.
|
|
# Preserve path under /lib/modules/$KVER so depmod can resolve deps.
|
|
NETDIR=/lib/modules/$KVER/kernel/drivers/net
|
|
for sub in ethernet mdio phy usb fddi wan; do
|
|
if [ -d "$NETDIR/$sub" ]; then
|
|
mkdir -p "$WORK/lib/modules/$KVER/kernel/drivers/net/$sub"
|
|
cp -r "$NETDIR/$sub/"* "$WORK/lib/modules/$KVER/kernel/drivers/net/$sub/" 2>/dev/null || true
|
|
fi
|
|
done
|
|
# Overlay + squashfs + loop + usb + hid + ptp + net/core
|
|
for modpath in fs/overlayfs fs/squashfs drivers/block drivers/usb/core drivers/usb/host drivers/hid drivers/ptp net/core; do
|
|
if [ -e "/lib/modules/$KVER/kernel/$modpath" ]; then
|
|
mkdir -p "$WORK/lib/modules/$KVER/kernel/$(dirname $modpath)"
|
|
cp -r "/lib/modules/$KVER/kernel/$modpath" "$WORK/lib/modules/$KVER/kernel/$modpath" 2>/dev/null || true
|
|
fi
|
|
done
|
|
# Pull in every module the copied ones depend on, from the host's
|
|
# modules.dep. A fixed list misses them: drivers/net/mii.ko was named
|
|
# here but ships as mii.ko.zst, so the test skipped it and usbnet,
|
|
# r8152 (USB-C to RJ45 adapters) and cdc_ether failed with "Unknown
|
|
# symbol mii_*"; igb likewise lacked dca and i2c-algo-bit.
|
|
python3 - "$KVER" "$WORK" <<'PYDEP'
|
|
import os, shutil, sys
|
|
kver, work = sys.argv[1], sys.argv[2]
|
|
src = f"/lib/modules/{kver}"
|
|
dst = f"{work}/lib/modules/{kver}"
|
|
dep = {}
|
|
for line in open(f"{src}/modules.dep"):
|
|
k, v = line.split(":", 1)
|
|
dep[k] = v.split()
|
|
have = {os.path.relpath(os.path.join(r, f), dst)
|
|
for r, _, fs in os.walk(dst) for f in fs if ".ko" in f}
|
|
todo, added = list(have), 0
|
|
while todo:
|
|
for d in dep.get(todo.pop(), []):
|
|
if d not in have:
|
|
have.add(d); todo.append(d); added += 1
|
|
os.makedirs(os.path.dirname(f"{dst}/{d}"), exist_ok=True)
|
|
shutil.copy2(f"{src}/{d}", f"{dst}/{d}")
|
|
print(f"added {added} dependency modules")
|
|
PYDEP
|
|
|
|
# NIC firmware the drivers request at probe (r8152/r8169 rtl_nic
|
|
# patches, tg3, bnx2/bnx2x, e100). The kernel loads .zst firmware.
|
|
for fw in rtl_nic tigon bnx2 bnx2x e100; do
|
|
if [ -d "/lib/firmware/$fw" ]; then
|
|
mkdir -p "$WORK/lib/firmware"
|
|
cp -r "/lib/firmware/$fw" "$WORK/lib/firmware/"
|
|
fi
|
|
done
|
|
|
|
# Decompress zstd modules in-place (busybox insmod can't handle .zst)
|
|
find "$WORK/lib/modules" -name '*.ko.zst' -print0 | \
|
|
xargs -0 -I {} sh -c 'zstd -d --rm -o "${1%.zst}" "$1" 2>/dev/null' _ {} || true
|
|
|
|
# Preserve modules.builtin / modules.order, regenerate modules.dep so
|
|
# modprobe can resolve dependencies inside the initramfs.
|
|
cp /lib/modules/$KVER/modules.builtin "$WORK/lib/modules/$KVER/" 2>/dev/null || true
|
|
cp /lib/modules/$KVER/modules.order "$WORK/lib/modules/$KVER/" 2>/dev/null || true
|
|
(cd "$WORK" && depmod -b . $KVER) 2>/dev/null || echo "depmod warning (non-fatal)"
|
|
|
|
# Init script
|
|
cp "{{ usb_root }}/playbook/blancco-init.sh" "$WORK/init"
|
|
chmod +x "$WORK/init"
|
|
|
|
# Build CPIO
|
|
cd "$WORK"
|
|
# Build beside the target and rename: a PC may be booting the old one.
|
|
find . | cpio -o -H newc --quiet | gzip -1 > "$OUT.tmp"
|
|
mv -f "$OUT.tmp" "$OUT"
|
|
cd /
|
|
rm -rf "$WORK"
|
|
rm -f "{{ web_root }}"/blancco/.kexec-initrd-{{ blancco_ksuffix }}.*.built
|
|
touch "{{ web_root }}/blancco/.kexec-initrd-{{ blancco_ksuffix }}.{{ blancco_kver }}.built"
|
|
echo "Built $(basename "$OUT") for $KVER: $(stat -c %s "$OUT") bytes"
|
|
ignore_errors: yes
|
|
|
|
- name: "Copy Ubuntu kernel for Blancco PXE boot (vmlinuz-ubuntu-{{ blancco_ksuffix }})"
|
|
copy:
|
|
src: "/boot/vmlinuz-{{ blancco_kver }}"
|
|
dest: "{{ web_root }}/blancco/vmlinuz-ubuntu-{{ blancco_ksuffix }}"
|
|
remote_src: yes
|
|
mode: '0644'
|
|
when: blancco_kernel_ok
|
|
|
|
# Secure Boot on: iPXE 2.0 starts the Canonical-signed kernel above
|
|
# through Ubuntu's Microsoft-signed shim (GetPxeScript.aspx: shim
|
|
# http://${server}/blancco/shimx64.efi). From the shim-signed package.
|
|
- name: "Blancco: find Ubuntu's signed shim"
|
|
stat:
|
|
path: /usr/lib/shim/shimx64.efi.signed
|
|
register: ubuntu_shim
|
|
|
|
- name: "Blancco: publish Ubuntu's signed shim as blancco/shimx64.efi"
|
|
copy:
|
|
src: /usr/lib/shim/shimx64.efi.signed
|
|
dest: "{{ web_root }}/blancco/shimx64.efi"
|
|
remote_src: yes
|
|
mode: '0644'
|
|
when: ubuntu_shim.stat.exists
|
|
|
|
- name: "Blancco: warn when the signed shim is missing"
|
|
debug:
|
|
msg: >-
|
|
WARNING: /usr/lib/shim/shimx64.efi.signed not found (package
|
|
shim-signed). Blancco with Secure Boot ON will not start; with
|
|
Secure Boot off it still boots (the menu's shim line fails over).
|
|
when: not ubuntu_shim.stat.exists
|
|
|
|
- name: "Create TFTP blancco directory"
|
|
file:
|
|
path: "{{ tftp_dir }}/blancco"
|
|
state: directory
|
|
owner: nobody
|
|
group: nogroup
|
|
mode: '0755'
|
|
|
|
- name: "Create TFTP symlinks for Blancco kernel/initrd (GRUB HTTP times out on large files; TFTP is reliable)"
|
|
file:
|
|
src: "{{ web_root }}/blancco/{{ item }}"
|
|
dest: "{{ tftp_dir }}/blancco/{{ item }}"
|
|
state: link
|
|
force: yes
|
|
owner: nobody
|
|
group: nogroup
|
|
loop:
|
|
- vmlinuz-bde-linux
|
|
- initramfs-bde-linux.img
|
|
- intel-ucode.img
|
|
- amd-ucode.img
|
|
- config.img
|
|
|
|
- name: "Build Ubuntu kernel modules tarball for Blancco (kmod-{{ blancco_kver }}.tar.gz)"
|
|
when: blancco_kernel_ok
|
|
shell: |
|
|
set -e
|
|
KVER="{{ blancco_kver }}"
|
|
OUT="{{ web_root }}/blancco/kmod-$KVER.tar.gz"
|
|
# Ship the host's iwlwifi firmware with its modules. The UEFI path
|
|
# runs this Ubuntu kernel on Blancco's rootfs, and Blancco 7.20 only
|
|
# carries API 89 ucode for AX201/AX210/AX211, which the 6.8 iwlwifi
|
|
# (max API 86) cannot load, so WiFi licensing never came up. Files
|
|
# only, no lib/ entry: on the Blancco rootfs lib is a symlink to
|
|
# usr/lib and a directory entry would replace it.
|
|
cd /
|
|
# blancco-init.sh fetches kmod-$(uname -r).tar.gz (the booted kernel).
|
|
tar czf "$OUT.tmp" -C / "lib/modules/$KVER" $(ls -d lib/firmware/iwlwifi-*)
|
|
mv -f "$OUT.tmp" "$OUT"
|
|
echo "Built $(basename "$OUT"): $(du -h "$OUT" | cut -f1)"
|
|
args:
|
|
creates: "{{ web_root }}/blancco/kmod-{{ blancco_kver }}.tar.gz"
|
|
|
|
- name: "Extract config-clean.xml from config.img (strip null bytes)"
|
|
shell: |
|
|
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
|
|
args:
|
|
creates: "{{ web_root }}/blancco/config-clean.xml"
|
|
|
|
# Idempotent file copy (was bundled into the previous shell task with a
|
|
# 'creates:' gate, so playbook re-runs never picked up edits to the
|
|
# source XML - stale prefs survived deploys).
|
|
- name: "Deploy Blancco preferences.xml from playbook source"
|
|
copy:
|
|
src: "{{ usb_root }}/playbook/blancco-preferences.xml"
|
|
dest: "{{ web_root }}/blancco/preferences.xml"
|
|
mode: '0644'
|
|
|
|
# blancco-init.sh fetches this at boot and runs it as a service in the
|
|
# Blancco rootfs: erase progress for the webapp's /imaging dashboard.
|
|
- name: "Deploy Blancco progress reporter"
|
|
copy:
|
|
src: "{{ usb_root }}/playbook/blancco-progress.py"
|
|
dest: "{{ web_root }}/blancco/blancco-progress.py"
|
|
mode: '0644'
|
|
|
|
# Hard gate: blancco-init.sh in initramfs drops to shell if the served
|
|
# preferences.xml is malformed or missing the SMB target. Fail the
|
|
# playbook here too so the problem surfaces at deploy time, not at the
|
|
# next erasure attempt.
|
|
- name: "Validate deployed preferences.xml is well-formed + targets the right SMB share"
|
|
shell: |
|
|
set -e
|
|
python3 -c 'import xml.etree.ElementTree as ET; ET.parse("{{ web_root }}/blancco/preferences.xml")'
|
|
grep -q '<hostname>{{ pxe_server_ip }}</hostname>' "{{ web_root }}/blancco/preferences.xml"
|
|
grep -q '<path>blancco-reports</path>' "{{ web_root }}/blancco/preferences.xml"
|
|
changed_when: false
|
|
|
|
- name: "Ensure Samba user for Blancco reports exists (idempotent)"
|
|
shell: |
|
|
id blancco >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin blancco
|
|
pdbedit -L 2>/dev/null | grep -q '^blancco:' || (echo -e "blancco\nblancco" | smbpasswd -a -s blancco)
|
|
changed_when: false
|
|
|
|
- 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
|
|
|
|
# Firewall per interface. Imaging NIC: everything imaging needs. GE
|
|
# corporate NIC: SSH and the webapp only. New rules go in first, then the
|
|
# old any-interface rules are removed, so SSH is never closed mid-run.
|
|
- name: "UFW: allow imaging services on the imaging NIC only"
|
|
ufw:
|
|
rule: allow
|
|
direction: in
|
|
interface: "{{ pxe_nic_name }}"
|
|
port: "{{ item.port }}"
|
|
proto: "{{ item.proto }}"
|
|
loop: "{{ ufw_pxe_rules }}"
|
|
loop_control:
|
|
label: "{{ pxe_nic_name }} {{ item.port }}/{{ item.proto }}"
|
|
|
|
- name: "UFW: allow SSH and the webapp on the GE corporate NIC"
|
|
ufw:
|
|
rule: allow
|
|
direction: in
|
|
interface: "{{ corp_nic_name }}"
|
|
port: "{{ item.port }}"
|
|
proto: "{{ item.proto }}"
|
|
loop: "{{ ufw_corp_rules }}"
|
|
loop_control:
|
|
label: "{{ corp_nic_name }} {{ item.port }}/{{ item.proto }}"
|
|
|
|
- name: "UFW: remove the old any-interface rules"
|
|
ufw:
|
|
rule: allow
|
|
port: "{{ item.port }}"
|
|
proto: "{{ item.proto }}"
|
|
delete: yes
|
|
loop:
|
|
- { port: "22", proto: tcp }
|
|
- { port: "67", proto: udp }
|
|
- { port: "69", proto: udp }
|
|
- { port: "80", proto: tcp }
|
|
- { port: "4433", proto: tcp }
|
|
- { port: "445", proto: tcp }
|
|
- { port: "9009", proto: tcp }
|
|
loop_control:
|
|
label: "any {{ item.port }}/{{ item.proto }}"
|
|
|
|
- name: "UFW: remove the stale 9011 rule (old webapp port)"
|
|
ufw:
|
|
rule: allow
|
|
direction: in
|
|
interface: "{{ pxe_nic_name }}"
|
|
port: "9011"
|
|
proto: tcp
|
|
delete: yes
|
|
|
|
- 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
|
|
|
|
# --- Updates pages (Dell drivers/BIOS, iPXE, Blancco) ---
|
|
# The webapp downloads from downloads.dell.com and GitHub through the
|
|
# corporate NIC and unpacks with 7z/zip; sbverify checks iPXE signatures.
|
|
- name: "Install tools the webapp Updates pages need (if missing)"
|
|
apt:
|
|
name:
|
|
- sbsigntool
|
|
- p7zip-full
|
|
- zip
|
|
- unzip
|
|
- curl
|
|
state: present
|
|
ignore_errors: yes
|
|
|
|
- name: "Find the Zscaler Root CA (playbook copy, else the image share)"
|
|
shell: |
|
|
for f in "{{ zscaler_ca_src }}" {{ zscaler_ca_share_glob }}; do
|
|
if [ -f "$f" ] && grep -q "BEGIN CERTIFICATE" "$f"; then
|
|
echo "$f"
|
|
exit 0
|
|
fi
|
|
done
|
|
register: zscaler_ca
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: "Install the Zscaler Root CA into the system trust store"
|
|
copy:
|
|
src: "{{ zscaler_ca.stdout_lines[0] }}"
|
|
dest: /usr/local/share/ca-certificates/zscaler-root-ca.crt
|
|
owner: root
|
|
group: root
|
|
mode: '0644'
|
|
when: zscaler_ca.stdout_lines | length > 0
|
|
register: zscaler_ca_copy
|
|
|
|
- name: "Rebuild the system CA bundle"
|
|
command: update-ca-certificates
|
|
when: zscaler_ca_copy is changed
|
|
|
|
- name: "Warn when no Zscaler Root CA was found"
|
|
debug:
|
|
msg: "WARNING: no Zscaler Root CA at {{ zscaler_ca_src }} or {{ zscaler_ca_share_glob }}; GitHub (iPXE update check) will fail TLS until it is installed."
|
|
when: zscaler_ca.stdout_lines | length == 0
|
|
|
|
- name: "Create webapp Updates state directory"
|
|
file:
|
|
path: /var/lib/pxe-webapp/updates
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: '0755'
|
|
|
|
# --- 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
|
|
# Lock TZ so report/backup mtimes and audit-log timestamps render
|
|
# in Eastern time regardless of how the host's /etc/localtime ends
|
|
# up. Without this, a Python process started before timedatectl
|
|
# finishes can cache UTC for its lifetime.
|
|
Environment=TZ=America/New_York
|
|
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
|
|
|
|
# Monthly Blancco CMMC evidence pack (ENT-056/158): on the 1st, save last
|
|
# month's ZIP to the reports share's evidence/ folder for the tech to copy
|
|
# into Box. Persistent catches a run missed while the server was off.
|
|
- name: "Create Blancco evidence pack service and timer"
|
|
copy:
|
|
dest: "/etc/systemd/system/{{ item.name }}"
|
|
content: "{{ item.content }}"
|
|
loop:
|
|
- name: pxe-blancco-evidence.service
|
|
content: |
|
|
[Unit]
|
|
Description=Save last month's Blancco CMMC evidence pack to the reports share
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
WorkingDirectory=/opt/pxe-webapp
|
|
Environment=BLANCCO_REPORTS=/srv/samba/blancco-reports
|
|
Environment=TZ=America/New_York
|
|
ExecStart=/usr/bin/python3 /opt/pxe-webapp/blancco_evidence_monthly.py
|
|
- name: pxe-blancco-evidence.timer
|
|
content: |
|
|
[Unit]
|
|
Description=Monthly Blancco CMMC evidence pack
|
|
|
|
[Timer]
|
|
OnCalendar=*-*-01 06:00:00
|
|
Persistent=true
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
|
|
- name: "Enable Blancco evidence pack timer"
|
|
systemd:
|
|
name: pxe-blancco-evidence.timer
|
|
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: |
|
|
# 9009 listens on every address: admins use the webapp from the GE
|
|
# corporate NIC. TODO: the webapp has NO authentication; anyone on
|
|
# the GE network who can reach this box can drive it. Add auth (or
|
|
# restrict 9009 on the corp NIC to an admin subnet in UFW).
|
|
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
|
|
|
|
# The network config (two NICs pinned by MAC) is applied near the top of
|
|
# the play, "NICs: ..." tasks, so dnsmasq/UFW/Samba see the final names.
|
|
# The old br-pxe bridge layout (onboard + USB NIC bonded) is gone: one NIC
|
|
# per network, see templates/netplan-pxe.yaml.j2.
|
|
|
|
handlers:
|
|
- name: "reload sysctl"
|
|
command: sysctl --system
|