From 8071a7c56cbb49f6477f1d0005c5b86531a474b4 Mon Sep 17 00:00:00 2001 From: Sandwich Date: Thu, 12 Feb 2026 22:17:02 +0100 Subject: [PATCH] feat(network): make interfaces[] canonical, normalize flat fields as AWX compat Flat network fields (bridge, ip, prefix, gateway, vlan) are now converted into a single-entry interfaces[] list during normalization. All virtualization tasks (proxmox, vmware, libvirt, xen) and configuration (NM, Alpine, Void) now consume system_cfg.network.interfaces exclusively for multi-NIC support. Also fixes: user.key -> user.keys in system_cfg output, strict list-only DNS in example inventories, removes legacy single-MAC virtualization_mac_address default. --- inventory_example.yml | 10 +- inventory_libvirt_example.yml | 13 +- main.yml | 6 +- roles/configuration/tasks/network.yml | 137 ++++++------------ roles/configuration/tasks/users.yml | 6 +- roles/configuration/templates/network.j2 | 14 +- roles/global_defaults/defaults/main.yml | 3 +- roles/global_defaults/tasks/system.yml | 52 ++++++- roles/global_defaults/tasks/validation.yml | 34 ++++- roles/virtualization/defaults/main.yml | 2 - roles/virtualization/tasks/proxmox.yml | 38 +++-- roles/virtualization/tasks/vmware.yml | 16 +- .../templates/cloud-network-config.yml.j2 | 21 +-- roles/virtualization/templates/vm.xml.j2 | 6 +- templates/xen.cfg.j2 | 6 +- vars_baremetal_example.yml | 3 +- vars_example.yml | 14 +- 17 files changed, 222 insertions(+), 159 deletions(-) diff --git a/inventory_example.yml b/inventory_example.yml index 02aadbf..533a966 100644 --- a/inventory_example.yml +++ b/inventory_example.yml @@ -45,7 +45,8 @@ all: user: name: "ops" password: "CHANGE_ME" - key: "ssh-ed25519 AAAA..." + keys: + - "ssh-ed25519 AAAA..." root: password: "CHANGE_ME" packages: @@ -89,7 +90,9 @@ all: prefix: 24 gateway: 10.0.0.1 dns: - servers: "1.1.1.1,1.0.0.1" + servers: + - "1.1.1.1" + - "1.0.0.1" disks: - size: 80 - size: 200 @@ -99,7 +102,8 @@ all: user: name: "dbadmin" password: "CHANGE_ME" - key: "ssh-ed25519 AAAA..." + keys: + - "ssh-ed25519 AAAA..." root: password: "CHANGE_ME" luks: diff --git a/inventory_libvirt_example.yml b/inventory_libvirt_example.yml index ee4764e..08c03c7 100644 --- a/inventory_libvirt_example.yml +++ b/inventory_libvirt_example.yml @@ -42,7 +42,8 @@ all: user: name: "web" password: "CHANGE_ME" - key: "ssh-ed25519 AAAA..." + keys: + - "ssh-ed25519 AAAA..." root: password: "CHANGE_ME" packages: @@ -83,7 +84,8 @@ all: user: name: "db" password: "CHANGE_ME" - key: "ssh-ed25519 AAAA..." + keys: + - "ssh-ed25519 AAAA..." root: password: "CHANGE_ME" luks: @@ -111,7 +113,9 @@ all: prefix: 24 gateway: 192.168.122.1 dns: - servers: "1.1.1.1,1.0.0.1" + servers: + - "1.1.1.1" + - "1.0.0.1" disks: - size: 80 - size: 200 @@ -121,7 +125,8 @@ all: user: name: "compute" password: "CHANGE_ME" - key: "ssh-ed25519 AAAA..." + keys: + - "ssh-ed25519 AAAA..." root: password: "CHANGE_ME" features: diff --git a/main.yml b/main.yml index d5f1e4c..f0fe352 100644 --- a/main.yml +++ b/main.yml @@ -44,9 +44,9 @@ 'name': ( (system_user_input.name | default('') | string | length) > 0 ) | ternary(system_user_input.name | string, prompt_user_name), - 'key': ( - system_user_input.key - if (system_user_input.key is iterable and system_user_input.key is not string and system_user_input.key | length > 0) + 'keys': ( + system_user_input.keys + if (system_user_input.keys is iterable and system_user_input.keys is not string and system_user_input.keys | length > 0) else ( [prompt_user_key] if (prompt_user_key | length > 0) diff --git a/roles/configuration/tasks/network.yml b/roles/configuration/tasks/network.yml index 983e0c9..bb641e8 100644 --- a/roles/configuration/tasks/network.yml +++ b/roles/configuration/tasks/network.yml @@ -1,9 +1,4 @@ --- -- name: Generate UUID for Network Profile - ansible.builtin.set_fact: - configuration_net_uuid: "{{ ('LAN-' ~ hostname) | ansible.builtin.to_uuid }}" - changed_when: false - - name: Read network interfaces ansible.builtin.command: argv: @@ -15,81 +10,41 @@ changed_when: false failed_when: false -- name: Resolve network interface and MAC address +- name: Detect available network interface names vars: - configuration_net_inf_from_facts: "{{ (ansible_default_ipv4 | default({})).get('interface', '') }}" - configuration_net_inf_from_ip: >- + configuration_detected_interfaces: >- {{ - ( - configuration_ip_link.stdout - | default('') - | regex_findall('^[0-9]+: ([^:]+):', multiline=True) - | reject('equalto', 'lo') - | list - | first - ) - | default('') - }} - configuration_net_inf_detected: >- - {{ configuration_net_inf_from_facts | default(configuration_net_inf_from_ip, true) }} - configuration_net_inf_regex: "{{ configuration_net_inf_detected | ansible.builtin.regex_escape }}" - configuration_net_mac_from_virtualization: "{{ virtualization_mac_address | default('') }}" - configuration_net_mac_from_facts: >- - {{ - ( - (ansible_facts | default({})).get(configuration_net_inf_detected, {}).get('macaddress', '') - ) - | default( - (ansible_facts | default({})).get('ansible_' + configuration_net_inf_detected, {}).get('macaddress', ''), - true - ) - }} - configuration_net_mac_from_ip: >- - {{ - ( - configuration_ip_link.stdout - | default('') - | regex_findall( - '^\\d+: ' ~ configuration_net_inf_regex ~ ':.*?link/ether\\s+([0-9A-Fa-f:]{17})', - multiline=True - ) - | first - ) + configuration_ip_link.stdout | default('') + | regex_findall('^[0-9]+: ([^:]+):', multiline=True) + | reject('equalto', 'lo') + | list }} ansible.builtin.set_fact: - configuration_net_inf: "{{ configuration_net_inf_detected }}" - configuration_net_mac: >- - {{ - ( - configuration_net_mac_from_virtualization - | default(configuration_net_mac_from_facts, true) - | default(configuration_net_mac_from_ip, true) - ) - | upper - }} - changed_when: false + configuration_detected_interfaces: "{{ configuration_detected_interfaces }}" -- name: Validate Network Interface Name +- name: Validate at least one network interface detected ansible.builtin.assert: that: - - configuration_net_inf | length > 0 - fail_msg: Failed to detect an active network interface. + - configuration_detected_interfaces | length > 0 + fail_msg: Failed to detect any network interfaces. -- name: Validate Network Interface MAC Address - ansible.builtin.assert: - that: - - configuration_net_mac | length > 0 - fail_msg: Failed to detect the MAC address for network interface {{ configuration_net_inf }}. - -- name: Configure NetworkManager profile +- name: Configure NetworkManager profiles when: os | lower not in ["alpine", "void"] block: - - name: Copy NetworkManager keyfile + - name: Copy NetworkManager keyfile per interface + vars: + configuration_iface: "{{ item }}" + configuration_iface_name: "{{ configuration_detected_interfaces[idx] | default('eth' ~ idx) }}" + configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}" ansible.builtin.template: src: network.j2 - dest: /mnt/etc/NetworkManager/system-connections/LAN.nmconnection + dest: "/mnt/etc/NetworkManager/system-connections/LAN-{{ idx }}.nmconnection" mode: "0600" + loop: "{{ system_cfg.network.interfaces }}" + loop_control: + index_var: idx + label: "LAN-{{ idx }}" - name: Fix Ubuntu unmanaged devices when: os | lower in ["ubuntu", "ubuntu-lts"] @@ -102,13 +57,6 @@ when: os | lower == "alpine" vars: configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}" - configuration_alpine_static: >- - {{ - system_cfg.network.ip is defined - and system_cfg.network.ip | string | length > 0 - and system_cfg.network.prefix is defined - and (system_cfg.network.prefix | string | length) > 0 - }} block: - name: Write Alpine network interfaces ansible.builtin.copy: @@ -117,15 +65,19 @@ content: | auto lo iface lo inet loopback + {% for iface in system_cfg.network.interfaces %} + {% set iface_name = configuration_detected_interfaces[loop.index0] | default(iface.name | default('eth' ~ loop.index0)) %} + {% set has_static = (iface.ip | default('') | string | length) > 0 %} - auto {{ configuration_net_inf }} - iface {{ configuration_net_inf }} inet {{ 'static' if configuration_alpine_static | bool else 'dhcp' }} - {% if configuration_alpine_static | bool %} - address {{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }} - {% if system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length %} - gateway {{ system_cfg.network.gateway }} + auto {{ iface_name }} + iface {{ iface_name }} inet {{ 'static' if has_static else 'dhcp' }} + {% if has_static %} + address {{ iface.ip }}/{{ iface.prefix }} + {% if iface.gateway | default('') | string | length %} + gateway {{ iface.gateway }} {% endif %} {% endif %} + {% endfor %} - name: Set Alpine DNS resolvers when: configuration_dns_list | length > 0 @@ -141,25 +93,24 @@ when: os | lower == "void" vars: configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}" - configuration_void_static: >- - {{ - system_cfg.network.ip is defined - and system_cfg.network.ip | string | length > 0 - and system_cfg.network.prefix is defined - and (system_cfg.network.prefix | string | length) > 0 - }} block: - - name: Write dhcpcd configuration for static networking - when: configuration_void_static | bool + - name: Write dhcpcd configuration ansible.builtin.copy: dest: /mnt/etc/dhcpcd.conf mode: "0644" content: | - interface {{ configuration_net_inf }} - static ip_address={{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }} - {% if system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length %} - static routers={{ system_cfg.network.gateway }} + {% for iface in system_cfg.network.interfaces %} + {% set iface_name = configuration_detected_interfaces[loop.index0] | default(iface.name | default('eth' ~ loop.index0)) %} + {% set has_static = (iface.ip | default('') | string | length) > 0 %} + {% if has_static %} + interface {{ iface_name }} + static ip_address={{ iface.ip }}/{{ iface.prefix }} + {% if iface.gateway | default('') | string | length %} + static routers={{ iface.gateway }} {% endif %} - {% if configuration_dns_list | length > 0 %} + {% if loop.index0 == 0 and configuration_dns_list | length > 0 %} static domain_name_servers={{ configuration_dns_list | join(' ') }} {% endif %} + + {% endif %} + {% endfor %} diff --git a/roles/configuration/tasks/users.yml b/roles/configuration/tasks/users.yml index 0c0a47a..e683743 100644 --- a/roles/configuration/tasks/users.yml +++ b/roles/configuration/tasks/users.yml @@ -18,7 +18,7 @@ changed_when: configuration_user_result.rc == 0 - name: Ensure .ssh directory exists - when: system_cfg.user.key | length > 0 + when: system_cfg.user.keys | length > 0 ansible.builtin.file: path: /mnt/home/{{ system_cfg.user.name }}/.ssh state: directory @@ -27,7 +27,7 @@ mode: "0700" - name: Add SSH public keys to authorized_keys - when: system_cfg.user.key | length > 0 + when: system_cfg.user.keys | length > 0 ansible.builtin.lineinfile: path: /mnt/home/{{ system_cfg.user.name }}/.ssh/authorized_keys line: "{{ item }}" @@ -35,4 +35,4 @@ group: 1000 mode: "0600" create: true - loop: "{{ system_cfg.user.key }}" + loop: "{{ system_cfg.user.keys }}" diff --git a/roles/configuration/templates/network.j2 b/roles/configuration/templates/network.j2 index 92215ae..fd8ebd9 100644 --- a/roles/configuration/templates/network.j2 +++ b/roles/configuration/templates/network.j2 @@ -1,24 +1,26 @@ [connection] -id=LAN +id=LAN-{{ idx }} uuid={{ configuration_net_uuid }} type=ethernet +interface-name={{ configuration_iface_name }} [ipv4] +{% set iface = configuration_iface %} {% set dns_list = system_cfg.network.dns.servers | default([]) %} {% set search_list = system_cfg.network.dns.search | default([]) %} -{% if system_cfg.network.ip is defined and system_cfg.network.ip | string | length %} -address1={{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }}{{ (',' ~ system_cfg.network.gateway) if (system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length) else '' }} +{% if iface.ip | default('') | string | length %} +address1={{ iface.ip }}/{{ iface.prefix }}{{ (',' ~ iface.gateway) if (iface.gateway | default('') | string | length) else '' }} method=manual {% else %} method=auto {% endif %} -{% if dns_list %} +{% if idx | int == 0 and dns_list %} dns={{ dns_list | join(';') }} {% endif %} -{% if dns_list %} +{% if idx | int == 0 and dns_list %} ignore-auto-dns=true {% endif %} -{% if search_list %} +{% if idx | int == 0 and search_list %} dns-search={{ search_list | join(';') }} {% endif %} diff --git a/roles/global_defaults/defaults/main.yml b/roles/global_defaults/defaults/main.yml index e9a962b..cca462e 100644 --- a/roles/global_defaults/defaults/main.yml +++ b/roles/global_defaults/defaults/main.yml @@ -36,13 +36,14 @@ system_defaults: dns: servers: [] search: [] + interfaces: [] path: "" packages: [] disks: [] user: name: "" password: "" - key: [] + keys: [] root: password: "" luks: diff --git a/roles/global_defaults/tasks/system.yml b/roles/global_defaults/tasks/system.yml index 8ca21a5..d20d8c0 100644 --- a/roles/global_defaults/tasks/system.yml +++ b/roles/global_defaults/tasks/system.yml @@ -16,7 +16,7 @@ fail_msg: "system and its nested keys (network, user, root, luks, features) must be dictionaries." quiet: true -- name: Validate DNS and user.key are lists (not strings) +- name: Validate DNS and user.keys are lists (not strings) when: system.network is defined and system.network.dns is defined ansible.builtin.assert: that: @@ -25,12 +25,12 @@ fail_msg: "system.network.dns.servers and system.network.dns.search must be lists, not strings." quiet: true -- name: Validate user.key is a list - when: system.user is defined and system.user.key is defined +- name: Validate user.keys is a list + when: system.user is defined and system.user.keys is defined ansible.builtin.assert: that: - - system.user.key is iterable and system.user.key is not string - fail_msg: "system.user.key must be a list of SSH public key strings." + - system.user.keys is iterable and system.user.keys is not string + fail_msg: "system.user.keys must be a list of SSH public key strings." quiet: true - name: Validate system features input types @@ -88,6 +88,27 @@ dns: servers: "{{ system_raw.network.dns.servers | default([]) }}" search: "{{ system_raw.network.dns.search | default([]) }}" + interfaces: >- + {{ + system_raw.network.interfaces + if (system_raw.network.interfaces | default([]) | length > 0) + else ( + [{ + 'name': 'eth0', + 'bridge': system_raw.network.bridge | default('') | string, + 'vlan': system_raw.network.vlan | default('') | string, + 'ip': system_raw.network.ip | default('') | string, + 'prefix': ( + (system_raw.network.prefix | int | string) + if (system_raw.network.prefix | default('') | string | length) > 0 + else '' + ), + 'gateway': system_raw.network.gateway | default('') | string + }] + if (system_raw.network.bridge | default('') | string | length > 0) + else [] + ) + }} path: "{{ system_raw.path | default('') | string }}" packages: >- {{ @@ -104,7 +125,7 @@ user: name: "{{ system_raw.user.name | string }}" password: "{{ system_raw.user.password | string }}" - key: "{{ system_raw.user.key | default([]) }}" + keys: "{{ system_raw.user.keys | default([]) }}" root: password: "{{ system_raw.root.password | string }}" luks: @@ -150,6 +171,25 @@ os: "{{ system_os_input if system_os_input | length > 0 else ('archlinux' if system_type == 'physical' else '') }}" os_version: "{{ system_raw.version | default('') | string }}" +- name: Populate primary network fields from first interface + when: + - system_cfg.network.interfaces | length > 0 + - system_cfg.network.bridge | default('') | string | length == 0 + vars: + _primary: "{{ system_cfg.network.interfaces[0] }}" + ansible.builtin.set_fact: + system_cfg: >- + {{ + system_cfg | combine({ + 'network': system_cfg.network | combine({ + 'bridge': _primary.bridge | default(''), + 'vlan': _primary.vlan | default(''), + 'ip': _primary.ip | default(''), + 'prefix': _primary.prefix | default(''), + 'gateway': _primary.gateway | default('') + }) + }, recursive=True) + }} - name: Normalize system disks input vars: diff --git a/roles/global_defaults/tasks/validation.yml b/roles/global_defaults/tasks/validation.yml index 13c616b..7f9f554 100644 --- a/roles/global_defaults/tasks/validation.yml +++ b/roles/global_defaults/tasks/validation.yml @@ -171,8 +171,12 @@ - hypervisor_cfg.host | string | length > 0 - hypervisor_cfg.storage | string | length > 0 - system_cfg.id | string | length > 0 - - system_cfg.network.bridge | string | length > 0 - fail_msg: "Missing required Proxmox inputs. Define hypervisor.(url,username,password,host,storage), system.id, and system.network.bridge." + - >- + (system_cfg.network.bridge | default('') | string | length > 0) + or (system_cfg.network.interfaces | default([]) | length > 0) + fail_msg: >- + Missing required Proxmox inputs. Define hypervisor.(url,username,password,host,storage), + system.id, and system.network.bridge (or system.network.interfaces[]). quiet: true - name: Validate VMware hypervisor inputs @@ -187,8 +191,12 @@ - hypervisor_cfg.datacenter | string | length > 0 - hypervisor_cfg.cluster | string | length > 0 - hypervisor_cfg.storage | string | length > 0 - - system_cfg.network.bridge | string | length > 0 - fail_msg: "Missing required VMware inputs. Define hypervisor.(url,username,password,datacenter,cluster,storage) and system.network.bridge." + - >- + (system_cfg.network.bridge | default('') | string | length > 0) + or (system_cfg.network.interfaces | default([]) | length > 0) + fail_msg: >- + Missing required VMware inputs. Define hypervisor.(url,username,password,datacenter,cluster,storage) + and system.network.bridge (or system.network.interfaces[]). quiet: true - name: Validate Xen hypervisor inputs @@ -197,8 +205,10 @@ - hypervisor_type == "xen" ansible.builtin.assert: that: - - system_cfg.network.bridge | string | length > 0 - fail_msg: "Missing required Xen inputs. Define system.network.bridge." + - >- + (system_cfg.network.bridge | default('') | string | length > 0) + or (system_cfg.network.interfaces | default([]) | length > 0) + fail_msg: "Missing required Xen inputs. Define system.network.bridge (or system.network.interfaces[])." quiet: true - name: Validate virtual installer ISO requirement @@ -329,3 +339,15 @@ - (system_cfg.network.prefix | int) > 0 fail_msg: "system.network.prefix is required when system.network.ip is set." quiet: true + +- name: Validate network interfaces entries + when: system_cfg.network.interfaces | default([]) | length > 0 + ansible.builtin.assert: + that: + - item is mapping + - item.bridge is defined and (item.bridge | string | length) > 0 + fail_msg: "Each system.network.interfaces[] entry must be a dict with at least a 'bridge' key." + quiet: true + loop: "{{ system_cfg.network.interfaces }}" + loop_control: + label: "{{ item | to_json }}" diff --git a/roles/virtualization/defaults/main.yml b/roles/virtualization/defaults/main.yml index 65b2b81..eb7a7be 100644 --- a/roles/virtualization/defaults/main.yml +++ b/roles/virtualization/defaults/main.yml @@ -9,8 +9,6 @@ virtualization_libvirt_disk_path: >- {{ [virtualization_libvirt_image_dir, hostname ~ '.qcow2'] | ansible.builtin.path_join }} virtualization_libvirt_cloudinit_path: >- {{ [virtualization_libvirt_image_dir, hostname ~ '-cloudinit.iso'] | ansible.builtin.path_join }} -virtualization_mac_address: >- - {{ '52:54:00' | community.general.random_mac(seed=hostname) }} virtualization_xen_disk_path: /var/lib/xen/images virtualization_tpm2_enabled: >- diff --git a/roles/virtualization/tasks/proxmox.yml b/roles/virtualization/tasks/proxmox.yml index bba405f..75834b6 100644 --- a/roles/virtualization/tasks/proxmox.yml +++ b/roles/virtualization/tasks/proxmox.yml @@ -8,6 +8,28 @@ {%- set _ = out.update({ 'scsi' ~ loop.index0: hypervisor_cfg.storage ~ ':' ~ (disk.size | int) }) -%} {%- endfor -%} {{ out }} + virtualization_proxmox_net: >- + {%- set out = {} -%} + {%- for iface in system_cfg.network.interfaces -%} + {%- set val = 'virtio,bridge=' ~ iface.bridge -%} + {%- if iface.vlan | default('') | string | length > 0 -%} + {%- set val = val ~ ',tag=' ~ iface.vlan -%} + {%- endif -%} + {%- set _ = out.update({ 'net' ~ loop.index0: val }) -%} + {%- endfor -%} + {{ out }} + virtualization_proxmox_ipconfig: >- + {%- set out = {} -%} + {%- for iface in system_cfg.network.interfaces -%} + {%- if iface.ip | default('') | string | length > 0 -%} + {%- set val = 'ip=' ~ iface.ip ~ '/' ~ iface.prefix + ~ ((',gw=' ~ iface.gateway) if (iface.gateway | default('') | length > 0) else '') -%} + {%- else -%} + {%- set val = 'ip=dhcp' -%} + {%- endif -%} + {%- set _ = out.update({ 'ipconfig' ~ loop.index0: val }) -%} + {%- endfor -%} + {{ out }} community.proxmox.proxmox_kvm: api_host: "{{ hypervisor_cfg.url }}" api_user: "{{ hypervisor_cfg.username }}" @@ -46,20 +68,8 @@ ide0: "{{ boot_iso }},media=cdrom" ide1: "{{ rhel_iso + ',media=cdrom' if rhel_iso is defined and rhel_iso | length > 0 else omit }}" ide2: "{{ hypervisor_cfg.storage }}:cloudinit" - net: - net0: >- - virtio,bridge={{ system_cfg.network.bridge - }}{%- if system_cfg.network.vlan is defined - and system_cfg.network.vlan | string | length > 0 - %},tag={{ system_cfg.network.vlan }}{% endif %} - ipconfig: - ipconfig0: >- - {{ - 'ip=' ~ system_cfg.network.ip ~ '/' ~ system_cfg.network.prefix - ~ (',gw=' ~ system_cfg.network.gateway if system_cfg.network.gateway is defined and system_cfg.network.gateway | length else '') - if system_cfg.network.ip is defined and system_cfg.network.ip | string | length - else 'ip=dhcp' - }} + net: "{{ virtualization_proxmox_net }}" + ipconfig: "{{ virtualization_proxmox_ipconfig }}" nameservers: "{{ system_cfg.network.dns.servers if system_cfg.network.dns.servers | length else omit }}" searchdomains: "{{ system_cfg.network.dns.search if system_cfg.network.dns.search | length else omit }}" onboot: true diff --git a/roles/virtualization/tasks/vmware.yml b/roles/virtualization/tasks/vmware.yml index dac607f..eb783c7 100644 --- a/roles/virtualization/tasks/vmware.yml +++ b/roles/virtualization/tasks/vmware.yml @@ -14,6 +14,17 @@ - name: Create VM in vCenter delegate_to: localhost + vars: + virtualization_vmware_networks: >- + {%- set ns = namespace(out=[]) -%} + {%- for iface in system_cfg.network.interfaces -%} + {%- set entry = {'name': iface.bridge, 'type': 'dhcp'} -%} + {%- if (iface.vlan | default('') | string | length) > 0 -%} + {%- set entry = entry | combine({'vlan': iface.vlan | int}) -%} + {%- endif -%} + {%- set ns.out = ns.out + [entry] -%} + {%- endfor -%} + {{ ns.out }} community.vmware.vmware_guest: hostname: "{{ hypervisor_cfg.url }}" username: "{{ hypervisor_cfg.username }}" @@ -53,10 +64,7 @@ "iso_path": rhel_iso } ] if rhel_iso is defined and rhel_iso | length > 0 else [] ) }} - networks: - - name: "{{ system_cfg.network.bridge }}" - type: dhcp - vlan: "{{ system_cfg.network.vlan if system_cfg.network.vlan is defined and system_cfg.network.vlan | string | length > 0 else omit }}" + networks: "{{ virtualization_vmware_networks }}" register: virtualization_vmware_create_result - name: Set VM created fact when VM was powered on during creation diff --git a/roles/virtualization/templates/cloud-network-config.yml.j2 b/roles/virtualization/templates/cloud-network-config.yml.j2 index d439a1e..f98b601 100644 --- a/roles/virtualization/templates/cloud-network-config.yml.j2 +++ b/roles/virtualization/templates/cloud-network-config.yml.j2 @@ -1,21 +1,23 @@ network: version: 2 ethernets: - id0: - match: - macaddress: "{{ virtualization_mac_address }}" -{% set has_static = system_cfg.network.ip is defined and system_cfg.network.ip | string | length %} {% set dns_list = system_cfg.network.dns.servers | default([]) %} {% set search_list = system_cfg.network.dns.search | default([]) %} +{% for iface in system_cfg.network.interfaces %} +{% set iface_mac = '52:54:00' | community.general.random_mac(seed=hostname if loop.index0 == 0 else hostname ~ '-nic' ~ loop.index0) %} +{% set has_static = (iface.ip | default('') | string | length) > 0 %} + id{{ loop.index0 }}: + match: + macaddress: "{{ iface_mac }}" {% if has_static %} addresses: - - "{{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }}" -{% if system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length %} - gateway4: "{{ system_cfg.network.gateway }}" + - "{{ iface.ip }}/{{ iface.prefix }}" +{% if iface.gateway | default('') | string | length %} + gateway4: "{{ iface.gateway }}" {% endif %} {% else %} dhcp4: true -{% if dns_list | length or search_list | length %} +{% if loop.index0 == 0 and (dns_list | length or search_list | length) %} dhcp4-overrides: {% if dns_list | length %} use-dns: false @@ -25,7 +27,7 @@ network: {% endif %} {% endif %} {% endif %} -{% if dns_list or search_list %} +{% if loop.index0 == 0 and (dns_list or search_list) %} nameservers: {% if dns_list %} addresses: @@ -40,3 +42,4 @@ network: {% endfor %} {% endif %} {% endif %} +{% endfor %} diff --git a/roles/virtualization/templates/vm.xml.j2 b/roles/virtualization/templates/vm.xml.j2 index 938d1ad..73d662f 100644 --- a/roles/virtualization/templates/vm.xml.j2 +++ b/roles/virtualization/templates/vm.xml.j2 @@ -46,11 +46,13 @@ {% endif %} + {% for iface in system_cfg.network.interfaces %} - - 0 else "default" }}'/> + + + {% endfor %} {% if virtualization_tpm2_enabled %} diff --git a/templates/xen.cfg.j2 b/templates/xen.cfg.j2 index d13400b..7801d14 100644 --- a/templates/xen.cfg.j2 +++ b/templates/xen.cfg.j2 @@ -10,7 +10,11 @@ disk = [ '{{ boot_iso }},,hdc,cdrom'{% if rhel_iso is defined and rhel_iso | length > 0 %}, '{{ rhel_iso }},,hdd,cdrom'{% endif %} {%- endif -%} ] -vif = [ 'bridge={{ system_cfg.network.bridge }},model=e1000' ] +vif = [ +{%- for iface in system_cfg.network.interfaces -%} + 'bridge={{ iface.bridge }},model=e1000'{% if not loop.last %}, {% endif %} +{%- endfor -%} +] boot = "{{ 'dc' if xen_installer_media_enabled | bool else 'c' }}" on_crash = "preserve" on_poweroff = "destroy" diff --git a/vars_baremetal_example.yml b/vars_baremetal_example.yml index 7e48e22..cf4c78d 100644 --- a/vars_baremetal_example.yml +++ b/vars_baremetal_example.yml @@ -28,7 +28,8 @@ system: user: name: "admin" password: "CHANGE_ME" - key: "ssh-ed25519 AAAA..." + keys: + - "ssh-ed25519 AAAA..." root: password: "CHANGE_ME" luks: diff --git a/vars_example.yml b/vars_example.yml index 9cc169a..3574771 100644 --- a/vars_example.yml +++ b/vars_example.yml @@ -25,6 +25,7 @@ system: memory: 8192 balloon: 0 network: + # Flat fields (AWX survey compatibility, builds single-entry interfaces[]) bridge: "vmbr0" ip: "{{ inventory_hostname }}" prefix: 24 @@ -35,6 +36,16 @@ system: - "1.0.0.1" search: - "example.com" + # Multi-NIC: use interfaces[] instead of flat fields above + # interfaces: + # - name: "eth0" + # bridge: "vmbr0" + # ip: "10.0.0.10" + # prefix: 24 + # gateway: "10.0.0.1" + # - name: "eth1" + # bridge: "vmbr1" + # vlan: "100" path: "/Lab/Example" disks: - size: 80 @@ -47,7 +58,8 @@ system: user: name: "ops" password: "CHANGE_ME" - key: "ssh-ed25519 AAAA..." + keys: + - "ssh-ed25519 AAAA..." root: password: "CHANGE_ME" luks: