feat(bootstrap): accept extra_addresses on network interfaces

This commit is contained in:
2026-09-29 09:20:39 +02:00
parent 5a43c9a29e
commit 36ba36fd21
7 changed files with 33 additions and 1 deletions
+1 -1
View File
@@ -213,7 +213,7 @@ Uniform package content source, family-resolved. `source: ''` defaults to `dvd`
| `dns.search` | list | `[]` | Search domains (must be a YAML list) |
| `interfaces` | list | `[]` | Multi-NIC config (overrides flat fields above) |
When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry list. When `interfaces` is set, it takes precedence. Each entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`.
When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry list. When `interfaces` is set, it takes precedence. Each entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`, and `extra_addresses`: further IPv4 CIDRs (`192.0.2.10/24`) on the same NIC beside `ip`, written by every network backend. Proxmox `ipconfigN` carries one address per NIC, so on Proxmox the extras arrive with the installed network config, not cloud-init.
#### `system.users`
+3
View File
@@ -19,6 +19,9 @@ interface-name=en*;eth*;
{% set search_list = configuration_dns_search %}
{% if iface.ip | default('') | string | length %}
address1={{ iface.ip }}/{{ iface.prefix }}{{ (',' ~ iface.gateway) if (iface.gateway | default('') | string | length) else '' }}
{% for address in iface.extra_addresses | default([]) %}
address{{ loop.index + 1 }}={{ address }}
{% endfor %}
method=manual
{% else %}
method=auto
@@ -16,6 +16,11 @@ iface {{ ifname }} inet static
{% if loop.index0 == 0 and configuration_dns_search %}
dns-search {{ configuration_dns_search | join(' ') }}
{% endif %}
{# One stanza per extra address: classic ifupdown takes one address per stanza, ifupdown2 merges them. #}
{% for address in iface.extra_addresses | default([]) %}
iface {{ ifname }} inet static
address {{ address }}
{% endfor %}
{% else %}
iface {{ ifname }} inet dhcp
{% endif %}
@@ -9,6 +9,9 @@ network:
{% if iface.ip | default('') | string | length %}
addresses:
- {{ iface.ip }}/{{ iface.prefix }}
{% for address in iface.extra_addresses | default([]) %}
- {{ address }}
{% endfor %}
{% if iface.gateway | default('') | string | length %}
routes:
- to: default
@@ -11,6 +11,9 @@ Type=ether
[Network]
{% if iface.ip | default('') | string | length %}
Address={{ iface.ip }}/{{ iface.prefix }}
{% for address in iface.extra_addresses | default([]) %}
Address={{ address }}
{% endfor %}
{% if iface.gateway | default('') | string | length %}
Gateway={{ iface.gateway }}
{% endif %}
@@ -467,6 +467,21 @@
loop_control:
label: "{{ item | to_json }}"
# A string here would iterate into one address per character in every network renderer.
- name: Validate network interfaces extra_addresses
when: item.extra_addresses is defined
ansible.builtin.assert:
that:
- item.extra_addresses is not string
- item.extra_addresses is iterable
- item.ip | default('') | string | length > 0
- item.extra_addresses | reject('match', '^(\\d{1,3}\\.){3}\\d{1,3}/\\d{1,2}$') | list | length == 0
fail_msg: "system.network.interfaces[].extra_addresses must be a list of IPv4 CIDRs (192.0.2.10/24) on an entry that also sets 'ip'."
quiet: true
loop: "{{ system_cfg.network.interfaces | default([]) }}"
loop_control:
label: "{{ item | to_json }}"
- name: Validate hostname format
ansible.builtin.assert:
that:
@@ -12,6 +12,9 @@ network:
{% if has_static %}
addresses:
- "{{ iface.ip }}/{{ iface.prefix }}"
{% for address in iface.extra_addresses | default([]) %}
- "{{ address }}"
{% endfor %}
{% if iface.gateway | default('') | string | length %}
routes:
- to: default