--- - 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" # where your USB is mounted 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) | list ) | first | default(ansible_default_ipv4.interface) }} - 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" copy: src: "{{ usb_mount }}/{{ 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" } - name: "Copy iPXE binaries from USB" copy: src: "{{ usb_mount }}/{{ item }}" dest: "{{ tftp_dir }}/{{ item }}" mode: '0755' loop: - ipxe.efi - name: "Copy boot tool files from USB (Clonezilla, Blancco, Memtest)" shell: > cp -r "{{ usb_mount }}/../boot-tools/{{ item }}/"* "{{ web_root }}/{{ item }}/" 2>/dev/null || cp -r "{{ usb_mount }}/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_mount }}/images" register: usb_images_dir - name: "Import WinPE deployment content from USB (if present)" shell: > cp -rn "{{ usb_mount }}/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: - 67 - 69 - 80 - 4433 - 445 - name: "Enable UFW firewall" ufw: state: enabled policy: allow - 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: "Install pip for Python package management" command: apt-get install -y python3-pip python3-venv args: creates: /usr/bin/pip3 - name: "Create webapp directory" file: path: /opt/pxe-webapp state: directory mode: '0755' - name: "Copy webapp from USB" shell: > cp -r "{{ usb_mount }}/../webapp/"* /opt/pxe-webapp/ 2>/dev/null || cp -r "{{ usb_mount }}/webapp/"* /opt/pxe-webapp/ 2>/dev/null || true args: creates: /opt/pxe-webapp/app.py - name: "Create Python virtual environment for webapp" command: python3 -m venv /opt/pxe-webapp/venv args: creates: /opt/pxe-webapp/venv/bin/python - name: "Install webapp Python dependencies" pip: requirements: /opt/pxe-webapp/requirements.txt virtualenv: /opt/pxe-webapp/venv - 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 ExecStart=/opt/pxe-webapp/venv/bin/python 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 Apache reverse proxy for webapp" copy: dest: /etc/apache2/sites-available/pxe-webapp.conf content: | ProxyPreserveHost On ProxyPass /manage http://127.0.0.1:5000/ ProxyPassReverse /manage http://127.0.0.1:5000/ - name: "Enable Apache proxy modules" command: a2enmod proxy proxy_http args: creates: /etc/apache2/mods-enabled/proxy.load - name: "Enable webapp Apache site" command: a2ensite pxe-webapp.conf args: creates: /etc/apache2/sites-enabled/pxe-webapp.conf - 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