From 94bb10a5eedbc2ff248984e1b6a6ae2a9497fd76 Mon Sep 17 00:00:00 2001 From: Bertrand Lanson Date: Tue, 19 May 2026 20:50:29 +0200 Subject: [PATCH] Add host_aggregate_info module This module allows operators to get informations on compute aggregates in a cluster, including availability zone informations and hosts membership. Change-Id: I14fa8d6914072bee314efa3753633933b21e9439 Signed-off-by: Bertrand Lanson --- meta/runtime.yml | 1 + plugins/modules/host_aggregate_info.py | 107 ++++++++++++++++++ .../openstack/test_host_aggregate_info.py | 97 ++++++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 plugins/modules/host_aggregate_info.py create mode 100644 tests/unit/modules/cloud/openstack/test_host_aggregate_info.py diff --git a/meta/runtime.yml b/meta/runtime.yml index ff76e95e..9938f7da 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -32,6 +32,7 @@ action_groups: - floating_ip_info - group_assignment - host_aggregate + - host_aggregate_info - identity_domain - identity_domain_info - identity_group diff --git a/plugins/modules/host_aggregate_info.py b/plugins/modules/host_aggregate_info.py new file mode 100644 index 00000000..bb68e896 --- /dev/null +++ b/plugins/modules/host_aggregate_info.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2025 OpenStack Ansible SIG +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: host_aggregate_info +short_description: Fetch OpenStack host aggregates +author: OpenStack Ansible SIG +description: + - Fetch OpenStack host aggregates. +options: + name: + description: + - Name or ID of the aggregate. + - Returns only the matching aggregate when set. + type: str +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = r''' +- name: Fetch all host aggregates + openstack.cloud.host_aggregate_info: + cloud: mycloud + +- name: Fetch a specific host aggregate by name + openstack.cloud.host_aggregate_info: + cloud: mycloud + name: db_aggregate +''' + +RETURN = r''' +aggregates: + description: List of dictionaries describing host aggregates. + returned: always + type: list + elements: dict + contains: + availability_zone: + description: Availability zone of the aggregate. + type: str + created_at: + description: The date and time when the resource was created. + type: str + deleted_at: + description: The date and time when the resource was deleted. + type: str + hosts: + description: List of hosts belonging to the aggregate. + type: list + elements: str + id: + description: The UUID of the aggregate. + type: str + is_deleted: + description: Whether or not the resource is deleted. + type: bool + metadata: + description: Metadata attached to the aggregate. + type: dict + name: + description: Name of the aggregate. + type: str + updated_at: + description: The date and time when the resource was updated. + type: str + uuid: + description: UUID of the aggregate. + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule + + +class ComputeHostAggregateInfoModule(OpenStackModule): + argument_spec = dict( + name=dict(), + ) + + module_kwargs = dict( + supports_check_mode=True + ) + + def run(self): + name = self.params['name'] + + if name: + aggregate = self.conn.compute.find_aggregate(name) + aggregates = [aggregate] if aggregate else [] + else: + aggregates = list(self.conn.compute.aggregates()) + + self.exit_json(changed=False, + aggregates=[a.to_dict(computed=False) + for a in aggregates]) + + +def main(): + module = ComputeHostAggregateInfoModule() + module() + + +if __name__ == '__main__': + main() diff --git a/tests/unit/modules/cloud/openstack/test_host_aggregate_info.py b/tests/unit/modules/cloud/openstack/test_host_aggregate_info.py new file mode 100644 index 00000000..d6c89220 --- /dev/null +++ b/tests/unit/modules/cloud/openstack/test_host_aggregate_info.py @@ -0,0 +1,97 @@ +import unittest +from unittest import mock + +from ansible_collections.openstack.cloud.plugins.modules import host_aggregate_info +from ansible_collections.openstack.cloud.tests.unit.modules.utils import ( + AnsibleExitJson, + ModuleTestCase, + set_module_args, +) + + +class FakeAggregate(dict): + + def to_dict(self, computed=False): + return dict(self) + + +FAKE_AGGREGATE = FakeAggregate( + id=7, + uuid='583bf5e7-b228-4850-ad3e-9a0b7aa6c8da', + name='dc1-a1', + availability_zone='dc1-a1', + hosts=['o3080-r01u07-1'], + metadata={'availability_zone': 'dc1-a1'}, + created_at='2026-05-16T23:46:45.000000', + updated_at=None, + deleted_at=None, + is_deleted=False, +) + + +class FakeSDK(object): + class exceptions: + class OpenStackCloudException(Exception): + pass + + +class TestHostAggregateInfo(ModuleTestCase): + + def _run_module(self, module_args, compute): + set_module_args(module_args) + conn = mock.Mock() + conn.compute = compute + with mock.patch.object( + host_aggregate_info.ComputeHostAggregateInfoModule, + 'openstack_cloud_from_module', + return_value=(FakeSDK(), conn), + ): + host_aggregate_info.main() + + def _new_compute(self): + compute = mock.Mock() + compute.aggregates.return_value = [FAKE_AGGREGATE] + compute.find_aggregate.return_value = FAKE_AGGREGATE + return compute + + def test_list_all_aggregates(self): + compute = self._new_compute() + + with self.assertRaises(AnsibleExitJson) as ex: + self._run_module({'name': None}, compute) + + result = ex.exception.args[0] + self.assertFalse(result['changed']) + self.assertEqual(1, len(result['aggregates'])) + self.assertEqual('dc1-a1', result['aggregates'][0]['name']) + compute.aggregates.assert_called_once_with() + compute.find_aggregate.assert_not_called() + + def test_find_aggregate_by_name(self): + compute = self._new_compute() + + with self.assertRaises(AnsibleExitJson) as ex: + self._run_module({'name': 'dc1-a1'}, compute) + + result = ex.exception.args[0] + self.assertFalse(result['changed']) + self.assertEqual(1, len(result['aggregates'])) + self.assertEqual('dc1-a1', result['aggregates'][0]['name']) + compute.find_aggregate.assert_called_once_with('dc1-a1') + compute.aggregates.assert_not_called() + + def test_find_aggregate_by_name_not_found(self): + compute = self._new_compute() + compute.find_aggregate.return_value = None + + with self.assertRaises(AnsibleExitJson) as ex: + self._run_module({'name': 'nonexistent'}, compute) + + result = ex.exception.args[0] + self.assertFalse(result['changed']) + self.assertEqual([], result['aggregates']) + compute.find_aggregate.assert_called_once_with('nonexistent') + + +if __name__ == '__main__': + unittest.main()