diff --git a/roles/libvirt_manager/README.md b/roles/libvirt_manager/README.md index 8e2010a12..1d51ed469 100644 --- a/roles/libvirt_manager/README.md +++ b/roles/libvirt_manager/README.md @@ -100,6 +100,7 @@ cifmw_libvirt_manager_configuration: networkconfig: (dict or list[dict], [network-config](https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html#network-config-v2) v2 config, needed if a static ip address should be defined at boot time in absence of a dhcp server in special scenarios. Optional) devices: (dict, optional, defaults to {}. The keys are the VMs of that type that needs devices to be attached, and the values are lists of strings, where each string must contain a valid libvirt XML element that will be passed to virsh attach-device) dhcp_options: (list, optional, defaults to []. List of DHCP options to apply to all VMs of this type. Format: ["option_number,value", ...]) + parent_group: (string, optional. Shared inventory group name that this VM type belongs to. All subtypes sharing a parent_group are aggregated under it while remaining independently targetable. See "Parent Groups" below.) networks: net_name: ``` @@ -204,6 +205,82 @@ cifmw_libvirt_manager_configuration: ``` +### Parent Groups + +VM types can declare a `parent_group` to be aggregated under a shared +inventory group while remaining independently targetable. + +When `parent_group` is set on a VM type, the role: + +1. Creates the subtype inventory group as usual (e.g. `compute94s`, `compute96s`). +2. Adds each VM to both the subtype group and the parent group. +3. Emits a parent group entry in `all-group.yml` with `children:` listing + all subtypes that share it. +4. Generates a single combined networking entry under the parent group name, + with an IP range length equal to the sum of all subtypes' amounts. + +This means plays targeting `computes` will reach all compute VMs regardless +of subtype, while plays targeting `compute94s` or `compute96s` will reach +only that subset. + +A parent group is abstract -- it exists only as a grouping mechanism and +must not correspond to an actual VM type in the layout. If you use +`parent_group: computes`, there must be no `compute:` entry in the +`vms` dictionary. The parent group is defined entirely by its children. + +All subtypes sharing a parent group must declare the same `parent_group` +value and the same set of `nets`. + +#### Example + +```YAML +cifmw_libvirt_manager_configuration: + vms: + compute94: + amount: 1 + disk_file_name: base-os-94.qcow2 + image_url: "https://example.com/rhel-9.4.qcow2" + parent_group: computes + nets: + - ocpbm + - osp_trunk + compute96: + amount: 2 + disk_file_name: base-os-96.qcow2 + image_url: "https://example.com/rhel-9.6.qcow2" + parent_group: computes + nets: + - ocpbm + - osp_trunk + controller: + disk_file_name: base-os.qcow2 + nets: + - ocpbm + - osp_trunk +``` + +This produces three inventory groups for computes: + +- `compute94s` -- contains `compute94-0` +- `compute96s` -- contains `compute96-0`, `compute96-1` +- `computes` -- parent group containing both `compute94s` and `compute96s` + as children + +The generated `all-group.yml` will include: + +```YAML +all: + children: + compute94s: + vars: ... + compute96s: + vars: ... + computes: + children: + compute94s: {} + compute96s: {} +``` + ### Parameters imported from the reproducer role The following parameters are usually set in the [reproducer](./reproducer.md) context. diff --git a/roles/libvirt_manager/tasks/add_vm_to_inventory.yml b/roles/libvirt_manager/tasks/add_vm_to_inventory.yml index b5642f903..2145ba029 100644 --- a/roles/libvirt_manager/tasks/add_vm_to_inventory.yml +++ b/roles/libvirt_manager/tasks/add_vm_to_inventory.yml @@ -2,7 +2,7 @@ - name: Add host to runtime inventory ansible.builtin.add_host: name: "{{ _full_host_name }}" - groups: "{{ _group }}s" + groups: "{{ _inventory_groups }}" ansible_ssh_user: "{{ _ssh_user }}" ansible_host: "{{ _add_ansible_host | ternary(_ansible_host, omit) }}" vm_type: "{{ _group }}" @@ -11,14 +11,20 @@ ansible.builtin.lineinfile: path: "{{ cifmw_libvirt_manager_tmp_inv_file }}" create: true - line: "[{{ _group }}s]" - regexp: "^\\[{{ _group }}s\\]$" + line: "[{{ _inv_group }}]" + regexp: "^\\[{{ _inv_group }}\\]$" state: present mode: "0644" + loop: "{{ _inventory_groups }}" + loop_control: + loop_var: _inv_group - name: Append host under proper group ansible.builtin.lineinfile: path: "{{ cifmw_libvirt_manager_tmp_inv_file }}" - insertafter: "^\\[{{ _group }}s\\]$" + insertafter: "^\\[{{ _inv_group }}\\]$" line: "{{ _ini_line }}" regexp: "^{{ _full_host_name | regex_escape() }} " + loop: "{{ _inventory_groups }}" + loop_control: + loop_var: _inv_group diff --git a/roles/libvirt_manager/tasks/generate_networking_data.yml b/roles/libvirt_manager/tasks/generate_networking_data.yml index ec393263a..058717eee 100644 --- a/roles/libvirt_manager/tasks/generate_networking_data.yml +++ b/roles/libvirt_manager/tasks/generate_networking_data.yml @@ -95,6 +95,13 @@ cifmw_baremetal_hosts[item.key] is defined) | ternary('baremetal', _std_group) }} + _subtype_group: "{{ _group }}s" + _parent_group: "{{ _cifmw_libvirt_manager_layout.vms[_vm_type].parent_group | default('') }}" + _inventory_groups: >- + {{ + [_subtype_group] + + ([_parent_group] if _parent_group | length > 0 else []) + }} _ocp_name: >- {{ item.key | replace('_', '-') | @@ -198,7 +205,6 @@ selectattr('name', 'match', _match) | first }} _dataset: | - {% set ns = namespace(ip_start=30) %} networks: {{ _lnet_data.name | replace('cifmw_', '') }}: {% if _lnet_data.ranges[0].start_v4 is defined and _lnet_data.ranges[0].start_v4 %} @@ -210,28 +216,43 @@ network-v6: '{{ net_6 }}' {% endif %} group-templates: + {% set ns = namespace(ip_start=30, rendered=[]) %} {% for group in _cifmw_libvirt_manager_layout.vms.keys() if group != 'controller' and ((_cifmw_libvirt_manager_layout.vms[group].amount is defined and (_cifmw_libvirt_manager_layout.vms[group].amount | int) > 0) or _cifmw_libvirt_manager_layout.vms[group].amount is undefined) %} - {% set _gr = (group == 'crc') | ternary('ocp', group) %} - {% if _lnet_data.name | replace('cifmw_', '') in _cifmw_libvirt_manager_layout.vms[group].nets %} - {{ _gr }}s: + {% set _gr = (group == 'crc') | ternary('ocp', group) %} + {% set _effective_group = _cifmw_libvirt_manager_layout.vms[group].parent_group | default(_gr ~ 's') %} + {% set _group_networks = _cifmw_libvirt_manager_layout.vms[group].nets | default([]) %} + {% if _effective_group not in ns.rendered and _lnet_data.name | replace('cifmw_', '') in _group_networks %} + {% set _shared_length = namespace(total=0) %} + {% for _candidate in _cifmw_libvirt_manager_layout.vms | dict2items + if _candidate.key != 'controller' and + ((_candidate.value.amount is defined and (_candidate.value.amount | int) > 0) or + _candidate.value.amount is undefined) %} + {% set _candidate_group = _candidate.value.parent_group | default((((_candidate.key == 'crc') | ternary('ocp', _candidate.key)) ~ 's')) %} + {% if _candidate_group == _effective_group and + (_lnet_data.name | replace('cifmw_', '') in (_candidate.value.nets | default([]))) %} + {% set _shared_length.total = _shared_length.total + (_candidate.value.amount | default(1) | int) %} + {% endif %} + {% endfor %} + {{ _effective_group }}: networks: {{ _lnet_data.name | replace('cifmw_', '') }}: - {% if cifmw_networking_definition['group-templates'][_gr ~ 's']['network-template'] is undefined %} + {% if cifmw_networking_definition['group-templates'][_effective_group]['network-template'] is undefined %} {% if net_4 is defined %} range-v4: start: '{{ net_4 | ansible.utils.nthhost(ns.ip_start | int ) }}' - length: {{ _cifmw_libvirt_manager_layout.vms[group].amount | default(1) }} + length: {{ _shared_length.total }} {% endif %} {% if net_6 is defined %} range-v6: start: '{{ net_6 | ansible.utils.nthhost(ns.ip_start | int) }}' - length: {{ _cifmw_libvirt_manager_layout.vms[group].amount | default(1) }} + length: {{ _shared_length.total }} {% endif %} - {% set ns.ip_start = ns.ip_start|int + (_cifmw_libvirt_manager_layout.vms[group].amount | default(1) | int ) + 1 %} + {% set ns.ip_start = ns.ip_start|int + (_shared_length.total | int ) + 1 %} {% endif %} + {% set _ = ns.rendered.append(_effective_group) %} {% endif %} {% endfor %} {% if cifmw_baremetal_hosts is defined and cifmw_baremetal_hosts | length > 0 %} diff --git a/roles/libvirt_manager/templates/all-inventory.yml.j2 b/roles/libvirt_manager/templates/all-inventory.yml.j2 index 04190a636..5055d0ac9 100644 --- a/roles/libvirt_manager/templates/all-inventory.yml.j2 +++ b/roles/libvirt_manager/templates/all-inventory.yml.j2 @@ -1,9 +1,18 @@ +{% set ns = namespace(parent_groups=[]) %} +{% for vm_name, vm_data in _cifmw_libvirt_manager_layout.vms.items() + if vm_data.manage | default(true) and + vm_data.amount | default(1) | int > 0 %} +{% if vm_data.parent_group is defined and vm_data.parent_group not in ns.parent_groups %} +{% set _ = ns.parent_groups.append(vm_data.parent_group) %} +{% endif %} +{% endfor %} all: children: {% for vm in _cifmw_libvirt_manager_layout.vms.keys() if (_cifmw_libvirt_manager_layout.vms[vm].manage | default(true) and _cifmw_libvirt_manager_layout.vms[vm].amount | default(1) | int > 0) %} - {{ (vm == 'crc') | ternary('ocp', vm) }}s: +{% set _group = ((vm == 'crc') | ternary('ocp', vm)) ~ 's' %} + {{ _group }}: vars: {% if _cifmw_libvirt_manager_layout.vms[vm].target is defined %} {% set _target = _cifmw_libvirt_manager_layout.vms[vm].target %} @@ -20,6 +29,16 @@ virtual machines. ansible_ssh_common_args: "-o StrictHostKeyChecking=no -J {{ ansible_user | default(ansible_user_id) }}@{{ _hostname }}" {% endif %} {% endfor %} +{% for parent in ns.parent_groups %} + {{ parent }}: + children: +{% for child_name, child_data in _cifmw_libvirt_manager_layout.vms.items() + if child_data.manage | default(true) and + child_data.amount | default(1) | int > 0 and + child_data.parent_group | default('') == parent %} + {{ ((child_name == 'crc') | ternary('ocp', child_name)) ~ 's' }}: {} +{% endfor %} +{% endfor %} hypervisors: hosts: {% set _hypervisors = (