--- # ============================================================ # Install required packages # ============================================================ - name: Install required packages ansible.builtin.apt: name: ethtool state: present update_cache: true # ============================================================ # Normalize and validate configuration # ============================================================ - name: Set WOL bridges list (handle string or list) ansible.builtin.set_fact: wol_bridges_list: "{{ wol_bridges if wol_bridges is iterable and wol_bridges is not string else [wol_bridges] }}" - name: Validate WOL bridges list is not empty ansible.builtin.assert: that: - wol_bridges_list | length > 0 fail_msg: "wol_bridges must contain at least one bridge interface" # ============================================================ # Detect physical NICs with WOL support using Ansible facts # ============================================================ - name: Gather network interface facts ansible.builtin.setup: gather_subset: - network when: ansible_facts.interfaces is not defined - name: Get all physical network interfaces with WOL support ansible.builtin.set_fact: wol_capable_interfaces: >- {{ ansible_facts.interfaces | map('extract', hostvars[inventory_hostname]['ansible_' ~ item] | default({})) | selectattr('type', 'defined') | selectattr('type', 'equalto', 'ether') | selectattr('device', 'defined') | rejectattr('device', 'search', '^(veth|tap|fw|lo|docker|br)') | map(attribute='device') | list }} - name: Validate WOL capability using ethtool for detected interfaces ansible.builtin.command: "ethtool {{ item }}" register: wol_capabilities_check changed_when: false failed_when: false loop: "{{ wol_capable_interfaces }}" loop_control: label: "{{ item }}" - name: Filter interfaces that actually support WOL ansible.builtin.set_fact: wol_supported_interfaces: >- {{ wol_capabilities_check.results | selectattr('stdout', 'search', 'Supports Wake-on:.*[gGdDpPuU]') | map(attribute='item') | list }} - name: Fail if no interfaces support WOL ansible.builtin.assert: that: - wol_supported_interfaces | length > 0 fail_msg: "No network interfaces found that support Wake-on-LAN. Check BIOS settings and NIC capabilities." # ============================================================ # Map bridges to physical NICs using Ansible facts # ============================================================ - name: Get bridge link information ansible.builtin.command: "bridge link show" register: bridge_links changed_when: false check_mode: false - name: Initialize detected interfaces dictionary ansible.builtin.set_fact: wol_detected_interfaces: {} - name: Detect physical NIC for each bridge ansible.builtin.set_fact: wol_detected_interfaces: >- {{ wol_detected_interfaces | combine({ item: ( bridge_links.stdout_lines | select('search', 'master ' ~ item) | map('regex_replace', '^\\d+: ([a-z0-9@.]+):.*$', '\\1') | reject('search', '^(veth|tap|fw)') | reject('search', '^\\d+:') | select('in', wol_supported_interfaces) | first | default('') ) }) }} loop: "{{ wol_bridges_list }}" loop_control: label: "{{ item }}" - name: Check for bond0 backing when: bond_info.rc == 0 block: - name: Detect if any bridge is backed by bond0 ansible.builtin.set_fact: wol_has_bond0: "{{ wol_detected_interfaces.values() | select('search', 'bond0') | length > 0 }}" - name: Extract bond0 slaves if present ansible.builtin.set_fact: wol_bond0_slaves: >- {{ (bond_info.stdout | regex_findall('Slave Interface: ([a-zA-Z0-9]+)')) | list }} when: wol_has_bond0 | default(false) # ============================================================ # Validate configuration and resolve to physical NICs # ============================================================ - name: Fail if any bridge backing NIC could not be detected ansible.builtin.fail: msg: > Unable to detect physical NIC backing bridge(s): {{ unresolved_bridges | join(', ') }}. Please verify bridges exist and are backed by WOL-capable interfaces. Available WOL-capable interfaces: {{ wol_supported_interfaces | join(', ') }} vars: unresolved_bridges: "{{ wol_detected_interfaces | dict2items | selectattr('value', 'equalto', '') | map(attribute='key') | list }}" when: unresolved_bridges | length > 0 - name: Build final WOL interfaces list (deduped physical NICs) ansible.builtin.set_fact: wol_final_interfaces: "{{ wol_detected_interfaces.values() | unique | list }}" # ============================================================ # Check current WOL status to ensure idempotency # ============================================================ - name: Get current WOL status ansible.builtin.command: "ethtool {{ item }}" register: wol_current_status changed_when: false failed_when: false loop: "{{ wol_final_interfaces }}" loop_control: label: "{{ item }}" - name: Build list of NICs needing WOL enabled ansible.builtin.set_fact: wol_needs_enable: >- {{ wol_current_status.results | selectattr('stdout', 'search', 'Wake-on: [^g]') | map(attribute='item') | list }} # ============================================================ # Enable WOL immediately (only if needed) # ============================================================ - name: Enable Wake-on-LAN immediately on NICs ansible.builtin.command: "ethtool -s {{ item }} wol {{ wol_mode }}" register: wol_enable_result changed_when: true loop: "{{ wol_needs_enable }}" loop_control: label: "{{ item }}" when: wol_needs_enable | length > 0 # ============================================================ # Persist WOL via udev rules (safe and idempotent) # ============================================================ - name: Create udev rule content ansible.builtin.set_fact: wol_udev_rules: >- {{ wol_final_interfaces | map('regex_replace', '^(.+)$', 'ACTION=="add", SUBSYSTEM=="net", KERNEL=="\1", RUN+="/sbin/ethtool -s \1 wol {{ wol_mode }}"') | list }} - name: Create/Update udev rules file ansible.builtin.copy: dest: /etc/udev/rules.d/90-wol.rules owner: root group: root mode: "0644" content: | # Wake-on-LAN udev rules - Auto-generated by Ansible # Applies to: {{ wol_final_interfaces | join(', ') }} {% for rule in wol_udev_rules %} {{ rule }} {% endfor %} notify: - Reload_udev_rules - Trigger_udev_net # ============================================================ # Verification & Reporting # ============================================================ - name: Verify Wake-on-LAN status ansible.builtin.command: "ethtool {{ item }}" register: wol_status changed_when: false loop: "{{ wol_final_interfaces }}" loop_control: label: "{{ item }}" when: wol_verify - name: Display WOL status per interface ansible.builtin.debug: msg: > Interface {{ item.item }} WOL Status: {{ item.stdout_lines | select('search', 'Wake-on:') | first | default('Status Unknown') }} loop: "{{ wol_status.results | default([]) }}" loop_control: label: "{{ item.item }}" when: wol_verify - name: Get MAC addresses for all interfaces ansible.builtin.set_fact: wol_mac_addresses: >- {{ wol_final_interfaces | map('extract', hostvars[inventory_hostname]['ansible_' ~ item] | default({}), 'macaddress') | list }} - name: Report WOL configuration ansible.builtin.debug: msg: | Wake-on-LAN Configuration Summary: =================================== Bridges Configured: {{ wol_bridges_list | join(', ') }} Physical Interfaces: {{ wol_final_interfaces | join(', ') }} WOL Mode: {{ wol_mode }} {% if wol_has_bond0 | default(false) %} Bond0 Detected: Yes Bond0 Slaves: {{ wol_bond0_slaves | join(', ') }} {% endif %} {% if wol_report_mac and wol_mac_addresses | length > 0 %} MAC Addresses: {% for iface, mac in (wol_final_interfaces | zip(wol_mac_addresses) | list) %} - {{ iface }}: {{ mac | default('Unable to detect') }} {% endfor %} {% endif %}