diff --git a/plugins/modules/hcloud_load_balancer.py b/plugins/modules/hcloud_load_balancer.py new file mode 100644 index 0000000..66d5fd8 --- /dev/null +++ b/plugins/modules/hcloud_load_balancer.py @@ -0,0 +1,283 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: hcloud_load_balancer + +short_description: Create and manage cloud Load Balancers on the Hetzner Cloud. + + +description: + - Create, update and manage cloud Load Balancers on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@LKaemmerling) + +options: + id: + description: + - The ID of the Hetzner Cloud Load Balancer to manage. + - Only required if no Load Balancer I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud Load Balancer to manage. + - Only required if no Load Balancer I(id) is given or a Load Balancer does not exists. + type: str + load_balancer_type: + description: + - The Load Balancer Type of the Hetzner Cloud Load Balancer to manage. + - Required if Load Balancer does not exists. + type: str + location: + description: + - Location of Load Balancer. + - Required if no I(network_zone) is given and Load Balancer does not exists. + type: str + network_zone: + description: + - Network Zone of Load Balancer. + - Required of no I(location) is given and Load Balancer does not exists. + type: str + labels: + description: + - User-defined labels (key-value pairs). + type: dict + delete_protection: + description: + - Protect the Load Balancer for deletion. + type: bool + state: + description: + - State of the Load Balancer. + default: present + choices: [ absent, present ] + type: str +extends_documentation_fragment: +- hetzner.hcloud.hcloud + +requirements: + - hcloud-python >= 1.8.0 +''' + +EXAMPLES = """ +- name: Create a basic Load Balancer + hcloud_load_balancer: + name: my-Load Balancer + load_balancer_type: lb11 + location: fsn1 + state: present + +- name: Ensure the Load Balancer is absent (remove if needed) + hcloud_load_balancer: + name: my-Load Balancer + state: absent + +""" + +RETURN = """ +hcloud_load_balancer: + description: The Load Balancer instance + returned: Always + type: complex + contains: + id: + description: Numeric identifier of the Load Balancer + returned: always + type: int + sample: 1937415 + name: + description: Name of the 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 + type: str + sample: cx11 + ipv4_address: + description: Public IPv4 address of the Load Balancer + returned: always + type: str + sample: 116.203.104.109 + ipv6_address: + description: Public IPv6 address of the Load Balancer + returned: always + type: str + sample: 2a01:4f8:1c1c:c140::1 + location: + description: Name of the location of the Load Balancer + returned: always + type: str + sample: fsn1 + labels: + description: User-defined labels (key-value pairs) + returned: always + type: dict + delete_protection: + description: True if Load Balancer is protected for deletion + type: bool + returned: always + sample: false +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible_collections.hetzner.hcloud.plugins.module_utils.hcloud import Hcloud + +try: + from hcloud.load_balancers.domain import LoadBalancer + from hcloud import APIException +except ImportError: + pass + + +class AnsibleHcloudLoadBalancer(Hcloud): + def __init__(self, module): + Hcloud.__init__(self, module, "hcloud_load_balancer") + self.hcloud_load_balancer = None + + def _prepare_result(self): + private_ipv4_address = None if self.hcloud_load_balancer.private_net.length == 0 else to_native(self.hcloud_load_balancer.private_net[0].ip) + return { + "id": to_native(self.hcloud_load_balancer.id), + "name": to_native(self.hcloud_load_balancer.name), + "ipv4_address": to_native(self.hcloud_load_balancer.public_net.ipv4.ip), + "ipv6_address": to_native(self.hcloud_load_balancer.public_net.ipv6.ip), + "private_ipv4_address": private_ipv4_address, + "load_balancer_type": to_native(self.hcloud_load_balancer.load_balancer_type.name), + "location": to_native(self.hcloud_load_balancer.datacenter.location.name), + "labels": self.hcloud_load_balancer.labels, + "delete_protection": self.hcloud_load_balancer.protection["delete"], + } + + def _get_load_balancer(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_load_balancer = self.client.load_balancers.get_by_id( + self.module.params.get("id") + ) + else: + self.hcloud_load_balancer = self.client.load_balancers.get_by_name( + self.module.params.get("name") + ) + except APIException as e: + self.module.fail_json(msg=e.message) + + def _create_load_balancer(self): + + self.module.fail_on_missing_params( + required_params=["name", "load_balancer_type"] + ) + + params = { + "name": self.module.params.get("name"), + "load_balancer_type": self.client.load_balancer_types.get_by_name( + self.module.params.get("load_balancer_type") + ), + } + + if self.module.params.get("location") is None and self.module.params.get("network_zone") is None: + self.module.fail_json(msg="one of the following is required: home_location, server") + elif self.module.params.get("location") is not None and self.module.params.get("network_zone") is None: + params["location"] = self.client.locations.get_by_name( + self.module.params.get("location") + ) + elif self.module.params.get("location") is None and self.module.params.get("network_zone") is not None: + params["network_zone"] = self.module.params.get("network_zone") + + if not self.module.check_mode: + resp = self.client.load_balancers.create(**params) + resp.action.wait_until_finished(max_retries=1000) + + self._mark_as_changed() + self._get_load_balancer() + + def _update_load_balancer(self): + try: + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_load_balancer.labels: + if not self.module.check_mode: + self.hcloud_load_balancer.update(labels=labels) + self._mark_as_changed() + + delete_protection = self.module.params.get("delete_protection") + if delete_protection is not None and delete_protection != self.hcloud_network.protection["delete"]: + if not self.module.check_mode: + self.hcloud_network.change_protection(delete=delete_protection).wait_until_finished() + self._mark_as_changed() + self._get_load_balancer() + except APIException as e: + self.module.fail_json(msg=e.message) + + def present_load_balancer(self): + self._get_load_balancer() + if self.hcloud_load_balancer is None: + self._create_load_balancer() + else: + self._update_load_balancer() + + def delete_load_balancer(self): + try: + self._get_load_balancer() + if self.hcloud_load_balancer is not None: + if not self.module.check_mode: + self.client.load_balancers.delete(self.hcloud_load_balancer).wait_until_finished() + self._mark_as_changed() + self.hcloud_load_balancer = None + except APIException as e: + self.module.fail_json(msg=e.message) + + @staticmethod + def define_module(): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + load_balancer_type={"type": "str"}, + location={"type": "str"}, + network_zone={"type": "str"}, + volumes={"type": "list"}, + labels={"type": "dict"}, + delete_protection={"type": "bool"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **Hcloud.base_module_arguments() + ), + required_one_of=[['id', 'name']], + mutually_exclusive=[["location", "network_zone"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHcloudLoadBalancer.define_module() + + hcloud = AnsibleHcloudLoadBalancer(module) + state = module.params.get("state") + if state == "absent": + hcloud.delete_load_balancer() + elif state == "present": + hcloud.present_load_balancer() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/hcloud_load_balancer/aliases b/tests/integration/targets/hcloud_load_balancer/aliases new file mode 100644 index 0000000..18dc30b --- /dev/null +++ b/tests/integration/targets/hcloud_load_balancer/aliases @@ -0,0 +1,2 @@ +cloud/hcloud +shippable/hcloud/group1 diff --git a/tests/integration/targets/hcloud_load_balancer/defaults/main.yml b/tests/integration/targets/hcloud_load_balancer/defaults/main.yml new file mode 100644 index 0000000..38e96f6 --- /dev/null +++ b/tests/integration/targets/hcloud_load_balancer/defaults/main.yml @@ -0,0 +1,5 @@ +# Copyright: (c) 2020, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +hcloud_prefix: "tests" +hcloud_load_balancer_name: "{{hcloud_prefix}}-integration" diff --git a/tests/integration/targets/hcloud_load_balancer/meta/main.yml b/tests/integration/targets/hcloud_load_balancer/meta/main.yml new file mode 100644 index 0000000..407c901 --- /dev/null +++ b/tests/integration/targets/hcloud_load_balancer/meta/main.yml @@ -0,0 +1,3 @@ +collections: + - community.general.ipfilter + - hetzner.cloud diff --git a/tests/integration/targets/hcloud_load_balancer/tasks/main.yml b/tests/integration/targets/hcloud_load_balancer/tasks/main.yml new file mode 100644 index 0000000..05be7e1 --- /dev/null +++ b/tests/integration/targets/hcloud_load_balancer/tasks/main.yml @@ -0,0 +1,178 @@ +# Copyright: (c) 2020, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: setup + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + state: absent + register: result +- name: verify setup + assert: + that: + - result is success +- name: test missing required parameters on create Load Balancer + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + register: result + ignore_errors: yes +- name: verify fail test missing required parameters on create Load Balancer + assert: + that: + - result is failed + - 'result.msg == "missing required arguments: load_balancer_type"' + +- name: test create Load Balancer with check mode + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + load_balancer_type: lb11 + network_zone: eu-central + state: present + register: result + check_mode: yes +- name: test create Load Balancer Load Balancer + assert: + that: + - result is changed + +- name: test create Load Balancer + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name}}" + load_balancer_type: lb11 + network_zone: eu-central + state: present + register: main_load_balancer +- name: verify create Load Balancer + assert: + that: + - main_load_balancer is changed + - main_load_balancer.hcloud_load_balancer.name == "{{ hcloud_load_balancer_name }}" + - main_load_balancer.hcloud_load_balancer.load_balancer_type == "lb11" + +- name: test create Load Balancer idempotence + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + state: started + register: result +- name: verify create Load Balancer idempotence + assert: + that: + - result is not changed + +- name: test update Load Balancer protection + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + delete_protection: true + state: present + register: result_after_test + ignore_errors: true +- name: verify update Load Balancer protection + assert: + that: + - result_after_test is changed + - result_after_test.hcloud_load_balancer.delete_protection is sameas true + - result_after_test.hcloud_load_balancer.rebuild_protection is sameas true + +- name: test Load Balancer without protection set to be idempotent + hcloud_load_balancer: + name: "{{hcloud_load_balancer_name}}" + register: result_after_test +- name: verify test Load Balancer without protection set to be idempotent + assert: + that: + - result_after_test is not changed + - result_after_test.hcloud_load_balancer.delete_protection is sameas true + - result_after_test.hcloud_load_balancer.rebuild_protection is sameas true + +- name: test delete Load Balancer fails if it is protected + hcloud_load_balancer: + name: "{{hcloud_load_balancer_name}}" + state: absent + ignore_errors: yes + register: result +- name: verify delete Load Balancer fails if it is protected + assert: + that: + - result is failed + - 'result.msg == "Load Balancer deletion is protected"' + +- name: test remove Load Balancer protection + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + delete_protection: false + state: present + register: result_after_test + ignore_errors: true +- name: verify remove Load Balancer protection + assert: + that: + - result_after_test is changed + - result_after_test.hcloud_load_balancer.delete_protection is sameas false + +- name: absent Load Balancer + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + state: absent + register: result +- name: verify absent Load Balancer + assert: + that: + - result is success + +- name: test create Load Balancer with labels + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name}}" + load_balancer_type: lb11 + network_zone: eu-central + labels: + key: value + mylabel: "val123" + state: started + register: main_load_balancer +- name: verify create Load Balancer with labels + assert: + that: + - main_load_balancer is changed + - main_load_balancer.hcloud_load_balancer.labels.key == "value" + - main_load_balancer.hcloud_load_balancer.labels.mylabel == "val123" + +- name: test update Load Balancer with labels + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name}}" + load_balancer_type: lb11 + network_zone: eu-central + labels: + key: other + mylabel: "val123" + state: started + register: main_load_balancer +- name: verify update Load Balancer with labels + assert: + that: + - main_load_balancer is changed + - main_load_balancer.hcloud_load_balancer.labels.key == "other" + - main_load_balancer.hcloud_load_balancer.labels.mylabel == "val123" + +- name: test update Load Balancer with labels in other order + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name}}" + load_balancer_type: lb11 + network_zone: eu-central + labels: + mylabel: "val123" + key: other + state: started + register: main_load_balancer +- name: verify update Load Balancer with labels in other order + assert: + that: + - main_load_balancer is not changed + +- name: cleanup with labels + hcloud_load_balancer: + name: "{{ hcloud_load_balancer_name }}" + state: absent + register: result +- name: verify cleanup + assert: + that: + - result is success diff --git a/tests/utils/gitlab/gitlab.sh b/tests/utils/gitlab/gitlab.sh index 93a999d..e76c1ee 100755 --- a/tests/utils/gitlab/gitlab.sh +++ b/tests/utils/gitlab/gitlab.sh @@ -55,7 +55,7 @@ retry ansible-galaxy -vvv collection install community.general retry ansible-galaxy -vvv collection install ansible.netcommon retry ansible-galaxy -vvv collection install community.internal_test_tools retry pip install netaddr --disable-pip-version-check -retry python -m pip install git+https://gitlab-ci-token:${CI_JOB_TOKEN}@git.hetzner.company/hc/backend/integrations/hcloud-python.git@v1.8.0-alpha.1 ## ToDo move to hcloud release version +retry python -m pip install git+https://gitlab-ci-token:${CI_JOB_TOKEN}@git.hetzner.company/hc/backend/integrations/hcloud-python.git@v1.9.0-alpha1 ## ToDo move to hcloud release version # END: HACK export PYTHONIOENCODING='utf-8'