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:
7 files changed
+280
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user