diff --git a/changelogs/fragments/load-balancer-status-filter.yml b/changelogs/fragments/load-balancer-status-filter.yml new file mode 100644 index 0000000..670daad --- /dev/null +++ b/changelogs/fragments/load-balancer-status-filter.yml @@ -0,0 +1,2 @@ +minor_changes: + - load_balancer_status - Add new filter to compute the status of a Load Balancer based on its targets. diff --git a/plugins/filter/all.py b/plugins/filter/all.py new file mode 100644 index 0000000..b0afd1d --- /dev/null +++ b/plugins/filter/all.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import Literal + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.common.text.converters import to_native + + +# pylint: disable=unused-argument +def load_balancer_status(load_balancer: dict, *args, **kwargs) -> Literal["unknown", "unhealthy", "healthy"]: + """ + Return the status of a Load Balancer based on its targets. + """ + try: + result = "healthy" + for target in load_balancer["targets"]: + target_health_status = target.get("health_status") + + # Report missing health status as unknown + if not target_health_status: + result = "unknown" + continue + + for health_status in target_health_status: + status = health_status.get("status") + if status == "healthy": + continue + + if status in (None, "unknown"): + result = "unknown" + continue + + if status == "unhealthy": + return "unhealthy" + + return result + except Exception as exc: + raise AnsibleFilterError(f"load_balancer_status - {to_native(exc)}", orig_exc=exc) from exc + + +class FilterModule: + """ + Hetzner Cloud filters. + """ + + def filters(self): + return { + "load_balancer_status": load_balancer_status, + } diff --git a/plugins/filter/load_balancer_status.yml b/plugins/filter/load_balancer_status.yml new file mode 100644 index 0000000..ec2b509 --- /dev/null +++ b/plugins/filter/load_balancer_status.yml @@ -0,0 +1,19 @@ +DOCUMENTATION: + name: load_balancer_status + version_added: 4.2.0 + short_description: Compute the status of a Load Balancer + description: + - Compute the status of a Load Balancer based on its targets. + options: + _input: + description: Load Balancer data. + type: dict + required: true +EXAMPLES: | + # Ensure a load balancer is healthy + {{ result.hcloud_load_balancer_info[0] | hetzner.hcloud.load_balancer_status == "healthy" }} + +RETURN: + _value: + description: The status of the Load Balancer targets, can be one of C(unknown), C(unhealthy) or C(healthy). + type: string diff --git a/plugins/modules/load_balancer.py b/plugins/modules/load_balancer.py index bc34aec..bce337c 100644 --- a/plugins/modules/load_balancer.py +++ b/plugins/modules/load_balancer.py @@ -104,11 +104,6 @@ hcloud_load_balancer: returned: always type: str sample: my-Load-Balancer - status: - description: Status of the Load Balancer - returned: always - type: str - sample: running load_balancer_type: description: Name of the Load Balancer type of the Load Balancer returned: always diff --git a/plugins/modules/load_balancer_info.py b/plugins/modules/load_balancer_info.py index a0e2cb7..27a3579 100644 --- a/plugins/modules/load_balancer_info.py +++ b/plugins/modules/load_balancer_info.py @@ -64,11 +64,6 @@ hcloud_load_balancer_info: returned: always type: str sample: my-Load-Balancer - status: - description: Status of the Load Balancer - returned: always - type: str - sample: running load_balancer_type: description: Name of the Load Balancer type of the Load Balancer returned: always diff --git a/tests/integration/targets/filter_all/aliases b/tests/integration/targets/filter_all/aliases new file mode 100644 index 0000000..18b1111 --- /dev/null +++ b/tests/integration/targets/filter_all/aliases @@ -0,0 +1,3 @@ +cloud/hcloud +gather_facts/no +azp/group2 diff --git a/tests/integration/targets/filter_all/defaults/main/common.yml b/tests/integration/targets/filter_all/defaults/main/common.yml new file mode 100644 index 0000000..0b15142 --- /dev/null +++ b/tests/integration/targets/filter_all/defaults/main/common.yml @@ -0,0 +1,29 @@ +# +# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead. +# +--- +# Azure Pipelines will configure this value to something similar to +# "azp-84824-1-hetzner-2-13-test-2-13-hcloud-3-9-1-default-i" +hcloud_prefix: "tests" + +# Used to namespace resources created by concurrent test pipelines/targets +hcloud_run_ns: "{{ hcloud_prefix | md5 }}" +hcloud_role_ns: "{{ role_name | split('_') | map('batch', 2) | map('first') | flatten() | join() }}" +hcloud_ns: "ansible-{{ hcloud_run_ns }}-{{ hcloud_role_ns }}" + +# Used to easily update the server types and images across all our tests. +hcloud_server_type_name: cax11 +hcloud_server_type_id: 45 + +hcloud_server_type_upgrade_name: cax21 +hcloud_server_type_upgrade_id: 93 + +hcloud_image_name: debian-12 +hcloud_image_id: 114690389 # architecture=arm + +hcloud_location_name: hel1 +hcloud_location_id: 3 +hcloud_datacenter_name: hel1-dc2 +hcloud_datacenter_id: 3 + +hcloud_network_zone_name: eu-central diff --git a/tests/integration/targets/filter_all/tasks/main.yml b/tests/integration/targets/filter_all/tasks/main.yml new file mode 100644 index 0000000..767fc46 --- /dev/null +++ b/tests/integration/targets/filter_all/tasks/main.yml @@ -0,0 +1,31 @@ +# +# DO NOT EDIT THIS FILE! Please edit the files in tests/integration/common instead. +# +--- +- name: Check if cleanup.yml exists + ansible.builtin.stat: + path: "{{ role_path }}/tasks/cleanup.yml" + register: cleanup_file + +- name: Check if prepare.yml exists + ansible.builtin.stat: + path: "{{ role_path }}/tasks/prepare.yml" + register: prepare_file + +- name: Include cleanup tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml" + when: cleanup_file.stat.exists + +- name: Include prepare tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/prepare.yml" + when: prepare_file.stat.exists + +- name: Run tests + block: + - name: Include test tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/test.yml" + + always: + - name: Include cleanup tasks + ansible.builtin.include_tasks: "{{ role_path }}/tasks/cleanup.yml" + when: cleanup_file.stat.exists diff --git a/tests/integration/targets/filter_all/tasks/test.yml b/tests/integration/targets/filter_all/tasks/test.yml new file mode 100644 index 0000000..55d8092 --- /dev/null +++ b/tests/integration/targets/filter_all/tasks/test.yml @@ -0,0 +1,21 @@ +- name: Test filter load_balancer_status + block: + - name: Load data + ansible.builtin.set_fact: + load_balancer_status_healthy: >- + {{ { "targets": [ + {"health_status": [{"status": "healthy"}]}, + {"health_status": [{"status": "healthy"}]}, + ]} | hetzner.hcloud.load_balancer_status }} + + load_balancer_status_healthy_and_unhealthy: >- + {{ { "targets": [ + {"health_status": [{"status": "healthy"}, {"status": "unhealthy"}]}, + {"health_status": [{"status": "healthy"}, {"status": "healthy"}]}, + ]} | hetzner.hcloud.load_balancer_status }} + + - name: Verify filter load_balancer_status + ansible.builtin.assert: + that: + - load_balancer_status_healthy == "healthy" + - load_balancer_status_healthy_and_unhealthy == "unhealthy" diff --git a/tests/unit/filter/test_all.py b/tests/unit/filter/test_all.py new file mode 100644 index 0000000..5065c42 --- /dev/null +++ b/tests/unit/filter/test_all.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import pytest + +from plugins.filter.all import load_balancer_status + +LOAD_BALANCER_STATUS_TEST_CASES = ( + ({"targets": [{"health_status": []}]}, "unknown"), + ({"targets": [{"health_status": [{}]}]}, "unknown"), + ({"targets": [{"health_status": [{"status": "unknown"}]}]}, "unknown"), + ({"targets": [{"health_status": [{"status": "unhealthy"}]}]}, "unhealthy"), + ({"targets": [{"health_status": [{"status": "healthy"}]}]}, "healthy"), + ( + { + "targets": [ + {"health_status": [{"status": "healthy"}]}, + {"health_status": [{"status": "healthy"}]}, + ] + }, + "healthy", + ), + ( + { + "targets": [ + {"health_status": [{"status": "healthy"}, {"status": "unhealthy"}]}, + {"health_status": [{"status": "healthy"}, {"status": "unknown"}]}, + ] + }, + "unhealthy", + ), + ( + { + "targets": [ + {"health_status": [{"status": "healthy"}]}, + {"health_status": [{"status": "unhealthy"}]}, + ] + }, + "unhealthy", + ), +) + + +@pytest.mark.parametrize(("value", "expected"), LOAD_BALANCER_STATUS_TEST_CASES) +def test_load_balancer_status(value, expected): + assert expected == load_balancer_status(value)