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
|
# Network backend is detected per host from the target rootfs in network.yml;
|
||||||
# that writes network config. Default (NetworkManager) applies to
|
# no static map needed.
|
||||||
# all OSes not explicitly listed.
|
|
||||||
configuration_network_task_map: {}
|
|
||||||
|
|||||||
@@ -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
|
- name: Set DNS configuration facts
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
|
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
|
||||||
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
|
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
|
||||||
|
|
||||||
- name: Configure networking
|
# 2+ unnamed interfaces would all match the first-ethernet glob, leaving the rest unconfigured.
|
||||||
ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"
|
- 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
|
- name: Copy NetworkManager keyfile per interface
|
||||||
vars:
|
vars:
|
||||||
configuration_iface: "{{ item }}"
|
configuration_iface: "{{ item }}"
|
||||||
configuration_iface_name: "{{ item.name | default(configuration_detected_interfaces[idx] | default('')) }}"
|
|
||||||
configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}"
|
configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}"
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: network.j2
|
src: network.j2
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ id=LAN-{{ idx }}
|
|||||||
uuid={{ configuration_net_uuid }}
|
uuid={{ configuration_net_uuid }}
|
||||||
type=ethernet
|
type=ethernet
|
||||||
autoconnect-priority=10
|
autoconnect-priority=10
|
||||||
{% if configuration_iface_name | length > 0 %}
|
|
||||||
interface-name={{ configuration_iface_name }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
[ipv4]
|
|
||||||
{% set iface = configuration_iface %}
|
{% 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 dns_list = configuration_dns_list %}
|
||||||
{% set search_list = configuration_dns_search %}
|
{% set search_list = configuration_dns_search %}
|
||||||
{% if iface.ip | default('') | string | length %}
|
{% 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