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 = (