feat(configuration): multi-backend networking, bind by match not MAC
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
---
|
||||
# Network configuration dispatch - maps OS name to the task file
|
||||
# that writes network config. Default (NetworkManager) applies to
|
||||
# all OSes not explicitly listed.
|
||||
configuration_network_task_map: {}
|
||||
# Network backend is detected per host from the target rootfs in network.yml;
|
||||
# no static map needed.
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
---
|
||||
- name: Read network interfaces
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ip
|
||||
- -o
|
||||
- link
|
||||
- show
|
||||
register: configuration_ip_link
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Detect available network interface names
|
||||
vars:
|
||||
configuration_detected_interfaces: >-
|
||||
{{
|
||||
configuration_ip_link.stdout
|
||||
| default('')
|
||||
| regex_findall('^[0-9]+: ([^:]+):', multiline=True)
|
||||
| reject('equalto', 'lo')
|
||||
| list
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
configuration_detected_interfaces: "{{ configuration_detected_interfaces }}"
|
||||
|
||||
- name: Validate at least one network interface detected
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- configuration_detected_interfaces | length > 0
|
||||
fail_msg: Failed to detect any network interfaces.
|
||||
|
||||
- name: Set DNS configuration facts
|
||||
ansible.builtin.set_fact:
|
||||
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
|
||||
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
|
||||
|
||||
- name: Configure networking
|
||||
ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"
|
||||
# 2+ unnamed interfaces would all match the first-ethernet glob, leaving the rest unconfigured.
|
||||
- name: Require an explicit name on every interface for multi-NIC
|
||||
vars:
|
||||
_unnamed: "{{ system_cfg.network.interfaces | map(attribute='name', default='') | map('string') | select('equalto', '') | list | length }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.network.interfaces | length <= 1 or _unnamed == 0
|
||||
fail_msg: >-
|
||||
Multi-NIC (system.network.interfaces with 2+ entries) requires a name on
|
||||
every interface; the first-adapter glob only binds a single NIC.
|
||||
|
||||
# Probe /mnt to detect the stack the installed rootfs will run (nothing runs in
|
||||
# the chroot). NM is checked first and wins, since bootstrap installs it on every
|
||||
# family; the rest are the fallback for a non-NM base image.
|
||||
- name: Probe the installed network stack on the target rootfs
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
register: configuration_net_probe
|
||||
loop:
|
||||
- /mnt/usr/bin/nmcli
|
||||
- /mnt/usr/lib/systemd/system/NetworkManager.service
|
||||
- /mnt/usr/sbin/netplan
|
||||
- /mnt/etc/netplan
|
||||
- /mnt/sbin/ifup
|
||||
- /mnt/usr/sbin/ifup
|
||||
- /mnt/etc/systemd/system/multi-user.target.wants/systemd-networkd.service
|
||||
- /mnt/etc/systemd/system/dbus-org.freedesktop.network1.service
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
|
||||
- name: Resolve the network backend from the probe
|
||||
vars:
|
||||
_found: "{{ configuration_net_probe.results | selectattr('stat.exists') | map(attribute='item') | list }}"
|
||||
ansible.builtin.set_fact:
|
||||
configuration_network_backend: >-
|
||||
{{
|
||||
'nm' if (['/mnt/usr/bin/nmcli', '/mnt/usr/lib/systemd/system/NetworkManager.service'] | intersect(_found))
|
||||
else 'netplan' if (['/mnt/usr/sbin/netplan', '/mnt/etc/netplan'] | intersect(_found))
|
||||
else 'eni' if (['/mnt/sbin/ifup', '/mnt/usr/sbin/ifup'] | intersect(_found))
|
||||
else 'networkd' if (['/mnt/etc/systemd/system/multi-user.target.wants/systemd-networkd.service', '/mnt/etc/systemd/system/dbus-org.freedesktop.network1.service'] | intersect(_found))
|
||||
else 'nm'
|
||||
}}
|
||||
|
||||
- name: Configure networking for the detected backend {{ configuration_network_backend }}
|
||||
ansible.builtin.include_tasks: "network_{{ configuration_network_backend }}.yml"
|
||||
|
||||
35
roles/configuration/tasks/network_eni.yml
Normal file
35
roles/configuration/tasks/network_eni.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
# ifupdown can't glob iface stanzas (no mapping on ifupdown2/Proxmox), so ENI binds
|
||||
# a literal name detected here. The chroot only sees live-ISO names: on a real
|
||||
# ifupdown base, set system.network.interfaces[].name to the installed name. Bootstrap
|
||||
# installs NetworkManager, so this fires only on a non-NM base image.
|
||||
- name: Detect ethernet interface names
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- ip
|
||||
- -o
|
||||
- link
|
||||
- show
|
||||
register: configuration_eni_link
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Resolve detected ethernet interface names
|
||||
ansible.builtin.set_fact:
|
||||
configuration_eni_detected: >-
|
||||
{{
|
||||
configuration_eni_link.stdout | default('')
|
||||
| regex_findall('^[0-9]+: ([^:@]+)[@:].*?link/ether', multiline=True)
|
||||
}}
|
||||
|
||||
- name: Ensure the network configuration directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/network
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write the ifupdown interfaces file
|
||||
ansible.builtin.template:
|
||||
src: network_eni.j2
|
||||
dest: /mnt/etc/network/interfaces
|
||||
mode: "0644"
|
||||
12
roles/configuration/tasks/network_netplan.yml
Normal file
12
roles/configuration/tasks/network_netplan.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
- name: Ensure the netplan directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/netplan
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write the netplan configuration
|
||||
ansible.builtin.template:
|
||||
src: network_netplan.j2
|
||||
dest: /mnt/etc/netplan/10-sg.yaml
|
||||
mode: "0600"
|
||||
18
roles/configuration/tasks/network_networkd.yml
Normal file
18
roles/configuration/tasks/network_networkd.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
- name: Ensure the systemd-networkd directory exists
|
||||
ansible.builtin.file:
|
||||
path: /mnt/etc/systemd/network
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Write systemd-networkd configuration per interface
|
||||
vars:
|
||||
configuration_iface: "{{ item }}"
|
||||
ansible.builtin.template:
|
||||
src: network_networkd.j2
|
||||
dest: "/mnt/etc/systemd/network/10-static-{{ idx }}.network"
|
||||
mode: "0644"
|
||||
loop: "{{ system_cfg.network.interfaces }}"
|
||||
loop_control:
|
||||
index_var: idx
|
||||
label: "10-static-{{ idx }}"
|
||||
@@ -2,7 +2,6 @@
|
||||
- name: Copy NetworkManager keyfile per interface
|
||||
vars:
|
||||
configuration_iface: "{{ item }}"
|
||||
configuration_iface_name: "{{ item.name | default(configuration_detected_interfaces[idx] | default('')) }}"
|
||||
configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}"
|
||||
ansible.builtin.template:
|
||||
src: network.j2
|
||||
|
||||
@@ -3,12 +3,18 @@ id=LAN-{{ idx }}
|
||||
uuid={{ configuration_net_uuid }}
|
||||
type=ethernet
|
||||
autoconnect-priority=10
|
||||
{% if configuration_iface_name | length > 0 %}
|
||||
interface-name={{ configuration_iface_name }}
|
||||
{% endif %}
|
||||
|
||||
[ipv4]
|
||||
{% set iface = configuration_iface %}
|
||||
{% if iface.name | default('') | string | length %}
|
||||
interface-name={{ iface.name }}
|
||||
|
||||
{% else %}
|
||||
{# Bind the first available ethernet by name glob, never a MAC: a clone with a new adapter/MAC stays networked (#12). #}
|
||||
|
||||
[match]
|
||||
interface-name=en*;eth*;
|
||||
|
||||
{% endif %}
|
||||
[ipv4]
|
||||
{% set dns_list = configuration_dns_list %}
|
||||
{% set search_list = configuration_dns_search %}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
|
||||
23
roles/configuration/templates/network_eni.j2
Normal file
23
roles/configuration/templates/network_eni.j2
Normal file
@@ -0,0 +1,23 @@
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
{% set ifname = iface.name if (iface.name | default('') | string | length) else (configuration_eni_detected[loop.index0] | default('eth' ~ loop.index0)) %}
|
||||
auto {{ ifname }}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
iface {{ ifname }} inet static
|
||||
address {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
gateway {{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_list %}
|
||||
dns-nameservers {{ configuration_dns_list | join(' ') }}
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and configuration_dns_search %}
|
||||
dns-search {{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
iface {{ ifname }} inet dhcp
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
29
roles/configuration/templates/network_netplan.j2
Normal file
29
roles/configuration/templates/network_netplan.j2
Normal file
@@ -0,0 +1,29 @@
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
{% for iface in system_cfg.network.interfaces %}
|
||||
lan{{ loop.index0 }}:
|
||||
{# Unnamed binds the first ethernet by name glob (e* = en*/eth*, netplan match.name takes one glob), never a MAC (#12). #}
|
||||
match:
|
||||
name: "{{ iface.name if (iface.name | default('') | string | length) else 'e*' }}"
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
addresses:
|
||||
- {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
routes:
|
||||
- to: default
|
||||
via: {{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
dhcp4: true
|
||||
{% endif %}
|
||||
{% if loop.index0 == 0 and (configuration_dns_list or configuration_dns_search) %}
|
||||
nameservers:
|
||||
{% if configuration_dns_list %}
|
||||
addresses: [{{ configuration_dns_list | join(', ') }}]
|
||||
{% endif %}
|
||||
{% if configuration_dns_search %}
|
||||
search: [{{ configuration_dns_search | join(', ') }}]
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
27
roles/configuration/templates/network_networkd.j2
Normal file
27
roles/configuration/templates/network_networkd.j2
Normal file
@@ -0,0 +1,27 @@
|
||||
[Match]
|
||||
{% set iface = configuration_iface %}
|
||||
{% if iface.name | default('') | string | length %}
|
||||
Name={{ iface.name }}
|
||||
{% else %}
|
||||
{# First available ethernet by name glob + device type, never a MAC (#12). #}
|
||||
Name=en* eth*
|
||||
Type=ether
|
||||
{% endif %}
|
||||
|
||||
[Network]
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
Address={{ iface.ip }}/{{ iface.prefix }}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
Gateway={{ iface.gateway }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
DHCP=yes
|
||||
{% endif %}
|
||||
{% if idx | int == 0 and configuration_dns_list %}
|
||||
{% for dns in configuration_dns_list %}
|
||||
DNS={{ dns }}
|
||||
{% endfor %}
|
||||
{% if configuration_dns_search %}
|
||||
Domains={{ configuration_dns_search | join(' ') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user