From 6cd75b5b8dac7e8504b91932598972695546fba4 Mon Sep 17 00:00:00 2001 From: Jose Date: Tue, 23 Dec 2025 22:06:51 +0100 Subject: [PATCH] =?UTF-8?q?docs=20=F0=9F=93=9D:=20Refactor=20README.md=20w?= =?UTF-8?q?ith=20detailed=20features,=20role=20variables,=20and=20usage=20?= =?UTF-8?q?examples.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the README to provide comprehensive information on the Ansible Proxmox WOL role's features, variables, and usage examples. --- README.md | 208 ++++++++++++++++++++++++++++++++++++---- defaults/main.yml | 28 ++++-- tasks/main.yml | 237 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 397 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 8a36083..d4ac7c6 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,208 @@ # ansible_proxmox_WOL -This Ansible role enables persistent Wake-on-LAN (WOL) on a Proxmox VE server. +A robust, idempotent Ansible role for enabling persistent Wake-on-LAN (WOL) on Proxmox VE servers. This role automatically detects physical network interfaces backing bridge interfaces (including bonded interfaces) and persistently enables WOL via udev rules. -## Variables +## Features -| Variable | Default | Description | -|--------|---------|-------------| -| wol_interface | eno1 | Network interface | -| wol_mode | g | WOL mode (magic packet) | -| wol_verify | true | Verify WOL status | +✅ **Fully Idempotent**: Checks current WOL status and only applies changes when needed +✅ **Multiple Bridge Support**: Configure WOL on multiple bridges simultaneously +✅ **Bond0 Detection**: Automatically detects and configures bonded interfaces +✅ **Auto-Detection**: Intelligently detects physical NICs backing Proxmox bridges +✅ **Safe & Persistent**: Uses udev rules for persistence across reboots +✅ **Comprehensive Validation**: Verifies WOL capability before configuration +✅ **Detailed Reporting**: Shows configuration status and MAC addresses for WOL senders +## Role Variables -## Notes for Proxmox admins +| Variable | Default | Type | Description | +|----------|---------|------|-------------| +| `wol_bridges` | `vmbr0` | string/list | Bridge interface(s) to enable WOL on. Can be a single bridge as string or multiple bridges as a list. | +| `wol_mode` | `g` | string | WOL mode: `g` (magic packet - recommended), `d` (disable), `p` (physical activity), `u` (unicast), `m` (multicast), `b` (broadcast) | +| `wol_verify` | `true` | boolean | Verify WOL status after configuration and display results | +| `wol_report_mac` | `true` | boolean | Include MAC addresses in configuration report | -Auto-detection works best when: - The management IP uses the physical NIC - You are not routing management via vmbr0 only +## How It Works -If your default route is on vmbr0, manual override is recommended +1. **Package Installation**: Ensures `ethtool` is installed for WOL management +2. **Bridge Discovery**: Reads bridge configuration and maps to physical NICs +3. **Bond0 Detection**: Detects if interfaces are bonded and extracts slave information +4. **Validation**: Verifies all detected NICs support Wake-on-LAN +5. **Idempotency Check**: Reads current WOL status to avoid redundant changes +6. **Enable WOL**: Applies WOL settings only to interfaces that need it +7. **Persist Settings**: Creates/updates udev rules for persistence across reboots +8. **Reload Udev**: Reloads udev rules and triggers network interface refresh +9. **Verification & Reporting**: Displays WOL configuration status and MAC addresses -BIOS must still have: - Wake on LAN enabled - ErP disabled +## Usage Examples -## Example Playbook +### Basic Single Bridge (Auto-Detection) +```yaml +- hosts: proxmox + become: true + roles: + - ansible_proxmox_WOL +``` +Automatically configures WOL for the default `vmbr0` bridge. +### Multiple Bridges ```yaml - hosts: proxmox become: true roles: - role: ansible_proxmox_WOL vars: - wol_interface: vmbr0 # Optional, comment for autodetection + wol_bridges: + - vmbr0 + - vmbr1 + - vmbr2 +``` +Configures WOL for multiple bridge interfaces simultaneously. + +### Custom Bridge with Verification Disabled +```yaml +- hosts: proxmox + become: true + roles: + - role: ansible_proxmox_WOL + vars: + wol_bridges: vmbr1 + wol_verify: false +``` + +### Disable WOL +```yaml +- hosts: proxmox + become: true + roles: + - role: ansible_proxmox_WOL + vars: + wol_mode: d +``` + +## Bond0 Support + +The role automatically detects if configured bridges are backed by bonded interfaces (bond0). When bond0 is detected: + +- The underlying physical slave interfaces are identified +- All slaves are configured with the same WOL settings +- The configuration is displayed in the summary report + +Example output when bond0 is detected: +``` +Bond0 Detected: Yes +Bond0 Slaves: eth0, eth1 +Physical Interfaces: bond0 +``` + +## Common Proxmox Scenarios + +### Scenario 1: Standard vmbr0 Setup +``` +Physical NIC (eno1) → vmbr0 bridge +``` +The role automatically configures `eno1` with WOL settings. + +### Scenario 2: Bonded Interface +``` +Physical NICs (eno1, eno2) → bond0 → vmbr0 bridge +``` +The role detects bond0 and applies WOL to bonded slaves. + +### Scenario 3: Multiple Bridges +``` +eno1 → vmbr0 +eno2 → vmbr1 +bond0 (eno3, eno4) → vmbr2 +``` +Configure all bridges with one role application: +```yaml +wol_bridges: + - vmbr0 + - vmbr1 + - vmbr2 +``` + +## Prerequisites + +- **Proxmox VE** host with bridge interfaces configured +- **Ansible** 2.9+ +- **ethtool** package (installed automatically by role) +- **Root/sudo access** on target host (required for udev and ethtool) +- **BIOS Configuration**: + - Wake-on-LAN enabled in BIOS + - ErP (Energy-Related Products) disabled in BIOS + +## Idempotency + +This role is fully idempotent. Running it multiple times has the same effect as running it once: + +- ✅ Only enables WOL on interfaces that don't already have it enabled +- ✅ Skips udev rule reload if rules haven't changed +- ✅ Uses `changed_when` conditions to accurately report actual changes +- ✅ Safe to include in recurring Ansible playbooks and AWX workflows + +## Safety + +- **Non-Destructive**: Never disables interfaces or changes bridge configuration +- **Validation**: Verifies NIC WOL capability before making changes +- **Error Handling**: Fails gracefully with clear error messages if: + - Bridges cannot be detected + - Physical NICs cannot be found + - NICs don't support Wake-on-LAN +- **Check Mode Support**: Fully compatible with `--check` mode for safe preview + +## Implementation Details + +### Persistence Method +WOL settings are persisted using udev rules at `/etc/udev/rules.d/90-wol.rules`. This is the most reliable method for Debian/Proxmox systems and survives: +- System reboots +- Network service restarts +- Interface state changes + +Example generated udev rule: +``` +ACTION=="add", SUBSYSTEM=="net", KERNEL=="eno1", RUN+="/sbin/ethtool -s eno1 wol g" +``` + +### Detection Logic +1. Reads `bridge link show` output +2. Filters out virtual interfaces (veth, tap, fw*) +3. Selects first physical NIC per bridge +4. For bond0: extracts slave interfaces from `/proc/net/bonding/bond0` + +## Troubleshooting + +### "Unable to detect physical NIC backing bridge(s)" +- Verify bridges exist: `bridge link show` +- Check bridge configuration: `brctl show` +- Ensure physical NIC is member of bridge + +### "Does not support Wake-on-LAN" +- Check NIC capabilities: `ethtool ` +- Verify BIOS has WOL enabled +- Some NICs have disabled WOL by default (check driver documentation) + +### WOL not persisting after reboot +- Check udev rules: `cat /etc/udev/rules.d/90-wol.rules` +- Verify ethtool installed: `which ethtool` +- Check system logs: `journalctl -u systemd-udevd -b` + +### Bond0 not detected +- Check bond status: `cat /proc/net/bonding/bond0` +- Verify bond is backing the configured bridge +- Check bond slave interfaces + +## Notes for Proxmox Admins + +- **Default Bridge**: Proxmox typically uses `vmbr0` as the default management bridge +- **No DHCP Changes**: This role only configures WOL; it doesn't modify IP configuration +- **Performance Impact**: WOL has negligible performance impact +- **Network Redundancy**: If using bonds or multiple bridges, all configured interfaces will be enabled for WOL + +## License + +MIT + +## Author + +Ansible Proxmox WOL Contributors diff --git a/defaults/main.yml b/defaults/main.yml index 669d4f4..d4ae3a4 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,13 +1,27 @@ -# defaults/main.yml -# Bridge interface to enable Wake-on-LAN on -wol_bridge: vmbr0 +--- +# ============================================================ +# Bridge interfaces to enable Wake-on-LAN on +# Can be a string (single bridge) or list (multiple bridges) +# Supports detection of physical NICs backing bridges +# ============================================================ +wol_bridges: vmbr0 -# WOL mode: -# g = magic packet (most common) +# ============================================================ +# WOL mode options: +# g = magic packet (most common, highly recommended) +# d = disable Wake-on-LAN +# p = physical activity +# u = unicast frames +# m = multicast frames +# b = broadcast frames +# ============================================================ wol_mode: "g" -# Enable verification task +# ============================================================ +# Verification settings +# ============================================================ +# Enable WOL status verification after configuration wol_verify: true -# Report MAC address for WOL senders +# Report MAC addresses for WOL packet senders wol_report_mac: true diff --git a/tasks/main.yml b/tasks/main.yml index a80cd5d..175d57b 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,104 +1,237 @@ --- +# ============================================================ +# Install required packages +# ============================================================ - name: Install required packages ansible.builtin.apt: name: ethtool state: present update_cache: yes -# ------------------------------------------------------------ -# Map vmbr0 → backing physical NIC -# ------------------------------------------------------------ +# ============================================================ +# 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 for all bridges (including bond0) +# ============================================================ - name: Get bridge link information ansible.builtin.command: "bridge link show" register: bridge_links changed_when: false + check_mode: false + +- name: Get bond info + ansible.builtin.command: "cat /proc/net/bonding/bond0" + register: bond_info + changed_when: false failed_when: false + check_mode: false -- name: Show bridge_links.stdout_lines - ansible.builtin.debug: - msg: "{{ bridge_links.stdout_lines }}" - -- name: "Detect physical NIC backing {{ wol_bridge }}" +- name: Initialize detected interfaces dictionary ansible.builtin.set_fact: - wol_detected_phy_candidates: >- + wol_detected_interfaces: {} + +- name: Detect physical NIC for each bridge + ansible.builtin.set_fact: + wol_detected_interfaces: >- {{ - bridge_links.stdout_lines - | select('search', 'master ' ~ wol_bridge) - | map('regex_replace', '^\\d+: ([^:@]+):.*$', '\\1') - | reject('search', '^(veth|tap|fw)') - | list - }} + wol_detected_interfaces | combine({ + item: ( + bridge_links.stdout_lines + | select('search', 'master ' ~ item) + | map('regex_replace', '^\\d+: ([^:@]+):.*$', '\\1') + | reject('search', '^(veth|tap|fw)') + | list + ) | first | default('') + }) + }} + loop: "{{ wol_bridges_list }}" + loop_control: + label: "{{ item }}" -- name: Select first physical NIC candidate - ansible.builtin.set_fact: - wol_detected_phy: "{{ wol_detected_phy_candidates[0] | default('') }}" +- name: Check for bond0 backing + 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: Fail if vmbr0 backing NIC could not be detected + - 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) + when: bond_info.rc == 0 + +# ============================================================ +# 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 {{ wol_bridge }}. - Please set wol_interface explicitly. + Unable to detect physical NIC backing bridge(s): {{ unresolved_bridges | join(', ') }}. + Please verify bridges exist or set wol_interfaces explicitly. Bridge output: - {{ bridge_links.stdout_lines }} - when: wol_detected_phy | default('') | length == 0 + {{ bridge_links.stdout_lines | join('\n') }} + 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 }}" + +# ============================================================ # Validate WOL capability -# ------------------------------------------------------------ -- name: Check WOL capability on detected NIC - ansible.builtin.command: "ethtool {{ wol_detected_phy }}" - register: wol_capabilities +# ============================================================ +- name: Check WOL capability on all detected NICs + ansible.builtin.command: "ethtool {{ item }}" + register: wol_capabilities_check changed_when: false + failed_when: false + loop: "{{ wol_final_interfaces }}" + loop_control: + label: "{{ item }}" -- name: Fail if NIC does not support Wake-on-LAN - ansible.builtin.fail: - msg: "Interface {{ wol_detected_phy }} does not support Wake-on-LAN." - when: "'Supports Wake-on: g' not in wol_capabilities.stdout" +- name: Validate all NICs support WOL + ansible.builtin.assert: + that: + - item.stdout is search('Supports Wake-on:.*[gGdDpPuU]') + fail_msg: "Interface {{ item.item }} does not support Wake-on-LAN" + loop: "{{ wol_capabilities_check.results }}" + loop_control: + label: "{{ item.item }}" -# ------------------------------------------------------------ -# Enable WOL immediately -# ------------------------------------------------------------ -- name: Enable Wake-on-LAN immediately - ansible.builtin.command: > - ethtool -s {{ wol_detected_phy }} wol {{ wol_mode }} +# ============================================================ +# 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 }}" -# ------------------------------------------------------------ -# Persist WOL via udev (correct + recommended) -# ------------------------------------------------------------ -- name: Create udev rule to persist Wake-on-LAN +- 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: | - ACTION=="add", SUBSYSTEM=="net", KERNEL=="{{ wol_detected_phy }}", RUN+="/sbin/ethtool -s {{ wol_detected_phy }} wol {{ wol_mode }}" + # Wake-on-LAN udev rules - Auto-generated by Ansible + # Applies to: {{ wol_final_interfaces | join(', ') }} + {% for rule in wol_udev_rules %} + {{ rule }} + {% endfor %} + register: udev_rules_changed - name: Reload udev rules ansible.builtin.command: udevadm control --reload changed_when: false + when: udev_rules_changed is changed - name: Trigger udev for network interfaces ansible.builtin.command: udevadm trigger --subsystem-match=net changed_when: false + when: udev_rules_changed is changed -# ------------------------------------------------------------ +# ============================================================ # Verification & Reporting -# ------------------------------------------------------------ +# ============================================================ - name: Verify Wake-on-LAN status - ansible.builtin.command: "ethtool {{ wol_detected_phy }}" + ansible.builtin.command: "ethtool {{ item }}" register: wol_status changed_when: false + loop: "{{ wol_final_interfaces }}" + loop_control: + label: "{{ item }}" when: wol_verify -- name: Get MAC address - ansible.builtin.set_fact: - wol_mac_address: "{{ ansible_facts.interfaces[wol_detected_phy].macaddress | default('') }}" +- 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: Fail if MAC address could not be detected - ansible.builtin.fail: - msg: "Unable to determine MAC address for interface {{ wol_detected_phy }}" +- 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 %} when: - wol_report_mac - wol_mac_address | length == 0