--- - 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 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: "Determine PXE interface" set_fact: pxe_iface: >- {{ (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: "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 - 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 ---- item clonezilla Clonezilla Live (Disk Imaging) item blancco Blancco Drive Eraser item memtest Memtest86+ (Memory Diagnostics) item --gap -- ---- item reboot Reboot item exit Exit to BIOS choose --default winpe --timeout 30000 target && goto ${target} :winpe 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 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 :memtest kernel http://${server}/memtest/memtest.efi boot :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: | DocumentRoot {{ web_root }} Options Indexes FollowSymLinks AllowOverride None Require all granted AddType text/plain .aspx - 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: "Configure Samba shares" blockinfile: path: /etc/samba/smb.conf backup: yes block: | [winpeapps] path = {{ samba_share }} browseable = yes read only = no guest ok = yes [clonezilla] path = /srv/samba/clonezilla browseable = yes read only = no guest ok = yes comment = Clonezilla backup images [blancco-reports] path = /srv/samba/blancco-reports browseable = yes read only = no guest ok = yes comment = Blancco Drive Eraser reports - 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: "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: "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 - 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" cron: name: "Restart dnsmasq after reboot" user: root special_time: "reboot" job: "/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=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 DocumentRoot {{ web_root }} Options Indexes FollowSymLinks AllowOverride None Require all granted ProxyPreserveHost On ProxyPass /manage http://127.0.0.1:9010/ ProxyPassReverse /manage http://127.0.0.1:9010/ Alias /static /opt/pxe-webapp/static Require all granted Options -Indexes ProxyPreserveHost On ProxyPass /static ! ProxyPass / http://127.0.0.1:9010/ ProxyPassReverse / http://127.0.0.1:9010/ - 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