---
- 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
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:
- geastandardpbr
- geaengineerpbr
- geashopfloorpbr
- gestandardlegacy
- geengineerlegacy
- geshopfloorlegacy
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 GetPxeScript.aspx"
copy:
dest: "{{ web_root }}/Altiris/iPXE/GetPxeScript.aspx"
backup: yes
content: |
#!ipxe
set server 10.9.100.1
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
- 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: "Configure Samba share"
blockinfile:
path: /etc/samba/smb.conf
backup: yes
block: |
[winpeapps]
path = {{ samba_share }}
browseable = yes
read only = no
guest ok = yes
- 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: "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 }}
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