feat: Nova aggregates for compute hosts

This patch introduces a new set of tasks allowing operators
to manage the aggregate membership of nova-compute hosts.

Operators can add compute-hosts to AZ-backed aggregates,
as well as any arbitrary aggregate, which are reconciled per run.

This allows for multi-az clusters out of the box, rather than being a
subsequent task.

Change-Id: I6aeea58f1d91d7a56238e44111d8351c076d9ffe
Signed-off-by: Bertrand Lanson <bertrand.lanson@infomaniak.com>
This commit is contained in:
Bertrand Lanson committed 2026-07-15 10:31:57 +02:00
1 parent 1e20a46c92
commit 41be3f368d
7 files changed
+280

No files matched your search

+13
View File
@@ -537,6 +537,19 @@ nova_libvirt_volume: "{{ 'libvirtd' if enable_nova_libvirt_container | bool else
haproxy_nova_serialconsole_proxy_tunnel_timeout: "10m"
haproxy_nova_spicehtml5_proxy_tunnel_timeout: "1h"
####################
# Availability Zones
####################
# Set per compute host (or group) to assign it to a Nova host aggregate backed
# AZ. Hosts with an empty value are left unmanaged.
nova_compute_availability_zone: ""
# List of arbitrary host aggregate names to assign this compute host to.
# Aggregates are created if they do not exist. No availability_zone metadata
# is set on these aggregates.
nova_compute_aggregates: []
####################
# OpenStack
####################
@@ -0,0 +1,95 @@
---
- name: Build nova AZ to compute host mapping
ansible.builtin.set_fact:
_nova_az_host_map: "{{ hostvars | nova_az_host_map(groups[nova_cell_compute_group]) }}"
run_once: true
- name: Get current nova AZ aggregates
become: true
kolla_toolbox:
container_engine: "{{ kolla_container_engine }}"
module_name: openstack.cloud.host_aggregate_info
module_args:
auth: "{{ openstack_nova_auth }}"
cloud: "{{ openstack_auth_cloud }}"
cacert: "{{ openstack_cacert }}"
endpoint_type: "{{ openstack_interface }}"
region_name: "{{ openstack_region_name }}"
register: _nova_aggregate_info
run_once: true
delegate_to: "{{ groups[nova_cell_conductor_group][0] }}"
when: _nova_az_host_map | default({}) | length > 0
- name: Remove stale hosts from AZ aggregates
become: true
kolla_toolbox:
container_engine: "{{ kolla_container_engine }}"
module_name: openstack.cloud.host_aggregate
module_args:
auth: "{{ openstack_nova_auth }}"
cloud: "{{ openstack_auth_cloud }}"
cacert: "{{ openstack_cacert }}"
endpoint_type: "{{ openstack_interface }}"
region_name: "{{ openstack_region_name }}"
name: "{{ item['name'] }}"
availability_zone: "{{ item['availability_zone'] }}"
hosts: >-
{{
item['hosts'] | default([])
| intersect((_nova_az_host_map | default({}))[item['name']] | default([]))
}}
purge_hosts: true
loop: >-
{{
_nova_aggregate_info.result.aggregates
| selectattr('availability_zone', 'defined')
| selectattr('availability_zone', 'ne', None)
| list
}}
run_once: true
delegate_to: "{{ groups[nova_cell_conductor_group][0] }}"
when: _nova_az_host_map | default({}) | length > 0
- name: Manage nova host aggregates for availability zones
become: true
kolla_toolbox:
container_engine: "{{ kolla_container_engine }}"
module_name: openstack.cloud.host_aggregate
module_args:
auth: "{{ openstack_nova_auth }}"
cloud: "{{ openstack_auth_cloud }}"
cacert: "{{ openstack_cacert }}"
endpoint_type: "{{ openstack_interface }}"
region_name: "{{ openstack_region_name }}"
name: "{{ item.key }}"
availability_zone: "{{ item.key }}"
hosts: "{{ item.value }}"
purge_hosts: false
loop: "{{ _nova_az_host_map | default({}) | dict2items }}"
run_once: true
delegate_to: "{{ groups[nova_cell_conductor_group][0] }}"
when: _nova_az_host_map | default({}) | length > 0
- name: Build arbitrary aggregate to compute host mapping
ansible.builtin.set_fact:
_nova_aggregate_host_map: "{{ hostvars | nova_aggregate_host_map(groups[nova_cell_compute_group]) }}"
run_once: true
- name: Manage nova host aggregates
become: true
kolla_toolbox:
container_engine: "{{ kolla_container_engine }}"
module_name: openstack.cloud.host_aggregate
module_args:
auth: "{{ openstack_nova_auth }}"
cloud: "{{ openstack_auth_cloud }}"
cacert: "{{ openstack_cacert }}"
endpoint_type: "{{ openstack_interface }}"
region_name: "{{ openstack_region_name }}"
name: "{{ item.key }}"
hosts: "{{ item.value }}"
purge_hosts: true
loop: "{{ _nova_aggregate_host_map | default({}) | dict2items }}"
run_once: true
delegate_to: "{{ groups[nova_cell_conductor_group][0] }}"
when: _nova_aggregate_host_map | default({}) | length > 0
+3
View File
@@ -48,3 +48,6 @@
# and there is a cell conductor in the inventory to delegate to.
- all_computes_in_batch | length > 0
- groups[nova_cell_conductor_group] | length > 0
- name: Import tasks from aggregate.yml
ansible.builtin.import_tasks: aggregate.yml
@@ -154,3 +154,52 @@ package. To enable this feature, set ``enable_nova_swtpm`` to ``true``.
Beware of `limitations`__ that come with this solution.
__ https://docs.openstack.org/nova/latest/admin/emulated-tpm.html#limitations
Host Aggregates and Availability Zones
=======================================
Kolla Ansible can manage Nova host aggregate membership and availability zones
directly from inventory variables, reconciling the state on every deploy or
reconfigure run.
Availability Zones
------------------
Set ``nova_compute_availability_zone`` on a compute host or group to assign it
to an AZ-backed host aggregate of the same name. The aggregate is created if it
does not exist. Hosts with an empty value are left unmanaged.
.. code-block:: yaml
# host_vars/compute01
nova_compute_availability_zone: az-1
# group_vars/gpu_computes
nova_compute_availability_zone: az-gpu
When a host moves from one AZ to another, Kolla Ansible removes it from the
old aggregate before adding it to the new one, avoiding Nova's restriction that
a host may only belong to one AZ aggregate at a time.
.. warning::
A compute host must not have any instances on it when its AZ is changed.
Nova will refuse the aggregate membership change while instances are
present on the host. Migrate or delete all instances from a host before
changing its ``nova_compute_availability_zone``.
Arbitrary Aggregates
--------------------
Set ``nova_compute_aggregates`` to a list of aggregate names to assign a host
to one or more aggregates without an availability zone. These aggregates are
created if they do not exist and their host membership is reconciled on each
run. A host may belong to both an AZ aggregate and any number of arbitrary
aggregates simultaneously.
.. code-block:: yaml
# host_vars/compute01
nova_compute_aggregates:
- gpu-v100
- ssd-storage
+35
View File
@@ -135,9 +135,44 @@ def _get_expected_compute_service_name(service_conf):
return f"{cg}-{sk}-ironic"
def nova_az_host_map(hostvars, compute_hosts):
"""Build an AZ-name to nodename list mapping from compute host hostvars.
:param hostvars: Ansible hostvars dict.
:param compute_hosts: list of compute host names in the cell.
:returns: dict mapping AZ name to list of nodenames assigned to that AZ.
"""
result = {}
for host in compute_hosts:
az = hostvars[host].get('nova_compute_availability_zone') or ''
if not az:
continue
nodename = hostvars[host]['ansible_facts']['nodename']
result.setdefault(az, []).append(nodename)
return result
def nova_aggregate_host_map(hostvars, compute_hosts):
"""Build an aggregate-name to nodename list mapping from compute hostvars.
:param hostvars: Ansible hostvars dict.
:param compute_hosts: list of compute host names in the cell.
:returns: dict mapping aggregate name to list of nodenames assigned to it.
"""
result = {}
for host in compute_hosts:
aggregates = hostvars[host].get('nova_compute_aggregates') or []
nodename = hostvars[host]['ansible_facts']['nodename']
for agg in aggregates:
result.setdefault(agg, []).append(nodename)
return result
def get_filters():
return {
"extract_cell": extract_cell,
"namespace_haproxy_for_cell": namespace_haproxy_for_cell,
"get_expected_ironic_compute_services": get_expected_ironic_compute_services, # noqa
"nova_az_host_map": nova_az_host_map,
"nova_aggregate_host_map": nova_aggregate_host_map,
}
@@ -159,6 +159,82 @@ class TestFilters(unittest.TestCase):
filters.get_expected_ironic_compute_services(
example_ironic_compute_conf, multi_conf, 'my-custom-group')
def _make_hostvars(self, hosts):
return {
h['name']: {
'ansible_facts': {'nodename': h['nodename']},
'nova_compute_availability_zone': h.get('az', ''),
'nova_compute_aggregates': h.get('aggregates', []),
}
for h in hosts
}
def test_nova_az_host_map_basic(self):
hostvars = self._make_hostvars([
{'name': 'compute01',
'nodename': 'compute01.example.com',
'az': 'az-1'},
{'name': 'compute02',
'nodename': 'compute02.example.com',
'az': 'az-1'},
{'name': 'compute03',
'nodename': 'compute03.example.com',
'az': 'az-2'},
])
result = filters.nova_az_host_map(hostvars, list(hostvars))
self.assertEqual(result, {
'az-1': ['compute01.example.com', 'compute02.example.com'],
'az-2': ['compute03.example.com'],
})
def test_nova_az_host_map_skips_unset(self):
hostvars = self._make_hostvars([
{'name': 'compute01',
'nodename': 'compute01.example.com',
'az': 'az-1'},
{'name': 'compute02', 'nodename': 'compute02.example.com'},
])
result = filters.nova_az_host_map(hostvars, list(hostvars))
self.assertEqual(result, {'az-1': ['compute01.example.com']})
def test_nova_az_host_map_empty(self):
hostvars = self._make_hostvars([
{'name': 'compute01', 'nodename': 'compute01.example.com'},
])
result = filters.nova_az_host_map(hostvars, list(hostvars))
self.assertEqual(result, {})
def test_nova_aggregate_host_map_basic(self):
hostvars = self._make_hostvars([
{'name': 'compute01', 'nodename': 'compute01.example.com',
'aggregates': ['gpu', 'ssd']},
{'name': 'compute02', 'nodename': 'compute02.example.com',
'aggregates': ['gpu']},
{'name': 'compute03', 'nodename': 'compute03.example.com',
'aggregates': ['ssd']},
])
result = filters.nova_aggregate_host_map(hostvars, list(hostvars))
self.assertEqual(result, {
'gpu': ['compute01.example.com', 'compute02.example.com'],
'ssd': ['compute01.example.com', 'compute03.example.com'],
})
def test_nova_aggregate_host_map_skips_unset(self):
hostvars = self._make_hostvars([
{'name': 'compute01', 'nodename': 'compute01.example.com',
'aggregates': ['gpu']},
{'name': 'compute02', 'nodename': 'compute02.example.com'},
])
result = filters.nova_aggregate_host_map(hostvars, list(hostvars))
self.assertEqual(result, {'gpu': ['compute01.example.com']})
def test_nova_aggregate_host_map_empty(self):
hostvars = self._make_hostvars([
{'name': 'compute01', 'nodename': 'compute01.example.com'},
])
result = filters.nova_aggregate_host_map(hostvars, list(hostvars))
self.assertEqual(result, {})
def test_namespace_haproxy_for_cell_with_empty_name(self):
example_services = {
'nova-novncproxy': {
@@ -0,0 +1,9 @@
---
features:
- |
Adds support for managing Nova host aggregates and availability zones via
Kolla Ansible inventory variables. Set ``nova_compute_availability_zone``
per compute host or group to assign it to an AZ-backed host aggregate.
Set ``nova_compute_aggregates`` (a list) to assign a host to one or more
arbitrary host aggregates. Aggregates are created if they do not exist, and
host memberships are reconciled on every deploy/reconfigure run.