diff --git a/lib/ansible/modules/cloud/hcloud/hcloud_floating_ip.py b/lib/ansible/modules/cloud/hcloud/hcloud_floating_ip.py new file mode 100644 index 00000000000..9cfa0e6fab8 --- /dev/null +++ b/lib/ansible/modules/cloud/hcloud/hcloud_floating_ip.py @@ -0,0 +1,328 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, 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 + +ANSIBLE_METADATA = { + "metadata_version": "1.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = """ +--- +module: hcloud_floating_ip + +short_description: Create and manage cloud Floating IPs on the Hetzner Cloud. + +version_added: "2.10" + +description: + - Create, update and manage cloud Floating IPs on the Hetzner Cloud. + +author: + - Lukas Kaemmerling (@lkaemmerling) + +options: + id: + description: + - The ID of the Hetzner Cloud Floating IPs to manage. + - Only required if no Floating IP I(name) is given. + type: int + name: + description: + - The Name of the Hetzner Cloud Floating IPs to manage. + - Only required if no Floating IP I(id) is given or a Floating IP does not exists. + type: str + description: + description: + - The Description of the Hetzner Cloud Floating IPs. + type: str + home_location: + description: + - Home Location of the Hetzner Cloud Floating IP. + - Required if no I(server) is given and Floating IP does not exists. + type: str + server: + description: + - Server Name the Floating IP should be assigned to. + - Required if no I(home_location) is given and Floating IP does not exists. + type: str + type: + description: + - Type of the Floating IP. + - Required if Floating IP does not exists + choices: [ ipv4, ipv6 ] + type: str + force: + description: + - Force the assignment or deletion of the Floating IP. + type: bool + labels: + description: + - User-defined labels (key-value pairs). + type: dict + state: + description: + - State of the Floating IP. + default: present + choices: [ absent, present ] + type: str + +requirements: + - hcloud-python >= 1.6.0 + +extends_documentation_fragment: hcloud +""" + +EXAMPLES = """ +- name: Create a basic IPv4 Floating IP + hcloud_floating_ip: + name: my-floating-ip + home_location: fsn1 + type: ipv4 + state: present +- name: Create a basic IPv6 Floating IP + hcloud_floating_ip: + name: my-floating-ip + home_location: fsn1 + type: ipv6 + state: present +- name: Assign a Floating IP to a server + hcloud_floating_ip: + name: my-floating-ip + server: 1234 + state: present +- name: Assign a Floating IP to another server + hcloud_floating_ip: + name: my-floating-ip + server: 1234 + force: yes + state: present +- name: Floating IP should be absent + hcloud_floating_ip: + name: my-floating-ip + state: absent +""" + +RETURN = """ +hcloud_floating_ip: + description: The Floating IP instance + returned: Always + type: complex + contains: + id: + description: ID of the Floating IP + type: int + returned: Always + sample: 12345 + name: + description: Name of the Floating IP + type: string + returned: Always + sample: my-floating-ip + description: + description: Description of the Floating IP + type: string + returned: Always + sample: my-floating-ip + ip: + description: IP Address of the Floating IP + type: string + returned: Always + sample: 116.203.104.109 + type: + description: Type of the Floating IP + type: string + returned: Always + sample: ipv4 + home_location: + description: Name of the home location of the Floating IP + type: string + returned: Always + sample: fsn1 + server: + description: Name of the server the Floating IP is assigned to. + type: string + returned: Always + sample: "my-server" + labels: + description: User-defined labels (key-value pairs) + type: dict + returned: Always + sample: + key: value + mylabel: 123 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.hcloud import Hcloud + +try: + from hcloud import APIException +except ImportError: + APIException = None + + +class AnsibleHcloudFloatingIP(Hcloud): + def __init__(self, module): + super(AnsibleHcloudFloatingIP, self).__init__(module, "hcloud_floating_ip") + self.hcloud_floating_ip = None + + def _prepare_result(self): + server = None + + if self.hcloud_floating_ip.server is not None: + server = to_native(self.hcloud_floating_ip.server.name) + return { + "id": to_native(self.hcloud_floating_ip.id), + "name": to_native(self.hcloud_floating_ip.name), + "description": to_native(self.hcloud_floating_ip.description), + "ip": to_native(self.hcloud_floating_ip.ip), + "type": to_native(self.hcloud_floating_ip.type), + "home_location": to_native(self.hcloud_floating_ip.home_location.name), + "labels": self.hcloud_floating_ip.labels, + "server": server, + } + + def _get_floating_ip(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_floating_ip = self.client.floating_ips.get_by_id( + self.module.params.get("id") + ) + else: + self.hcloud_floating_ip = self.client.floating_ips.get_by_name( + self.module.params.get("name") + ) + except APIException as e: + self.module.fail_json(msg=e.message) + + def _create_floating_ip(self): + self.module.fail_on_missing_params( + required_params=["type"] + ) + params = { + "description": self.module.params.get("description"), + "type": self.module.params.get("type"), + "name": self.module.params.get("name"), + } + if self.module.params.get("home_location") is not None: + params["home_location"] = self.client.locations.get_by_name( + self.module.params.get("home_location") + ) + elif self.module.params.get("server") is not None: + params["server"] = self.client.servers.get_by_name( + self.module.params.get("server") + ) + else: + self.module.fail_json(msg="one of the following is required: home_location, server") + + if self.module.params.get("labels") is not None: + params["labels"] = self.module.params.get("labels") + if not self.module.check_mode: + resp = self.client.floating_ips.create(**params) + self.hcloud_floating_ip = resp.floating_ip + + self._mark_as_changed() + self._get_floating_ip() + + def _update_floating_ip(self): + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_floating_ip.labels: + if not self.module.check_mode: + self.hcloud_floating_ip.update(labels=labels) + self._mark_as_changed() + + description = self.module.params.get("description") + if description is not None and description != self.hcloud_floating_ip.description: + if not self.module.check_mode: + self.hcloud_floating_ip.update(description=description) + self._mark_as_changed() + + server = self.module.params.get("server") + if server is not None: + if self.module.params.get("force") or self.hcloud_floating_ip.server is None: + if not self.module.check_mode: + self.hcloud_floating_ip.assign( + self.client.servers.get_by_name(self.module.params.get("server")) + ) + else: + self.module.warn( + "Floating IP is already assigned to server %s. You need to unassign the Floating IP or use force=yes." + % self.hcloud_floating_ip.server.name + ) + self._mark_as_changed() + elif server is None and self.hcloud_floating_ip.server is not None: + if not self.module.check_mode: + self.hcloud_floating_ip.unassign() + self._mark_as_changed() + + self._get_floating_ip() + + def present_floating_ip(self): + self._get_floating_ip() + if self.hcloud_floating_ip is None: + self._create_floating_ip() + else: + self._update_floating_ip() + + def delete_floating_ip(self): + self._get_floating_ip() + if self.hcloud_floating_ip is not None: + if self.module.params.get("force") or self.hcloud_floating_ip.server is None: + if not self.module.check_mode: + self.client.floating_ips.delete(self.hcloud_floating_ip) + else: + self.module.warn( + "Floating IP is currently assigned to server %s. You need to unassign the Floating IP or use force=yes." + % self.hcloud_floating_ip.server.name + ) + self._mark_as_changed() + self.hcloud_floating_ip = None + + @staticmethod + def define_module(): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + description={"type": "str"}, + server={"type": "str"}, + home_location={"type": "str"}, + force={"type": "bool"}, + type={"choices": ["ipv4", "ipv6"]}, + labels={"type": "dict"}, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **Hcloud.base_module_arguments() + ), + required_one_of=[['id', 'name']], + mutually_exclusive=[['home_location', 'server']], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHcloudFloatingIP.define_module() + + hcloud = AnsibleHcloudFloatingIP(module) + state = module.params["state"] + if state == "absent": + hcloud.delete_floating_ip() + elif state == "present": + hcloud.present_floating_ip() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/test/integration/targets/hcloud_floating_ip/aliases b/test/integration/targets/hcloud_floating_ip/aliases new file mode 100644 index 00000000000..18dc30b6c31 --- /dev/null +++ b/test/integration/targets/hcloud_floating_ip/aliases @@ -0,0 +1,2 @@ +cloud/hcloud +shippable/hcloud/group1 diff --git a/test/integration/targets/hcloud_floating_ip/defaults/main.yml b/test/integration/targets/hcloud_floating_ip/defaults/main.yml new file mode 100644 index 00000000000..fc60d281849 --- /dev/null +++ b/test/integration/targets/hcloud_floating_ip/defaults/main.yml @@ -0,0 +1,6 @@ +# Copyright: (c) 2019, 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_floating_ip_name: "{{hcloud_prefix}}-integration" +hcloud_server_name: "{{hcloud_prefix}}-fip-tests" \ No newline at end of file diff --git a/test/integration/targets/hcloud_floating_ip/tasks/main.yml b/test/integration/targets/hcloud_floating_ip/tasks/main.yml new file mode 100644 index 00000000000..deeebf3acee --- /dev/null +++ b/test/integration/targets/hcloud_floating_ip/tasks/main.yml @@ -0,0 +1,325 @@ +# Copyright: (c) 2019, Hetzner Cloud GmbH +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +--- +- name: setup server + hcloud_server: + name: "{{ hcloud_server_name }}" + server_type: cx11 + image: ubuntu-18.04 + state: started + location: "fsn1" + register: main_server +- name: verify setup server + assert: + that: + - main_server is changed + +- name: setup another server + hcloud_server: + name: "{{ hcloud_server_name }}2" + server_type: cx11 + image: ubuntu-18.04 + state: started + register: main_server2 +- name: verify setup another server + assert: + that: + - main_server2 is changed + +- name: test missing type parameter on create Floating IP + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + register: result + ignore_errors: yes +- name: verify fail test missing type parameter on create Floating IP + assert: + that: + - result is failed + - 'result.msg == "missing required arguments: type"' + +- name: test missing required parameters on create Floating IP + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + register: result + ignore_errors: yes +- name: verify fail test missing required parameters on create Floating IP + assert: + that: + - result is failed + - 'result.msg == "one of the following is required: home_location, server"' + +- name: test missing type parameter on delete Floating IP + hcloud_floating_ip: + type: ipv4 + home_location: "fsn1" + state: "absent" + register: result + ignore_errors: yes +- name: verify fail test missing type parameter on delete Floating IP + assert: + that: + - result is failed + - 'result.msg == "one of the following is required: id, name"' + +- name: test create Floating IP with check mode + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "Web Server" + type: ipv4 + home_location: "fsn1" + register: floatingIP + check_mode: yes +- name: verify test create Floating IP with check mode + assert: + that: + - floatingIP is changed + +- name: test create Floating IP + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "Web Server" + type: ipv4 + home_location: "fsn1" + register: floatingIP +- name: verify test create Floating IP + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.name ==hcloud_floating_ip_name + - floatingIP.hcloud_floating_ip.description == "Web Server" + - floatingIP.hcloud_floating_ip.home_location == "fsn1" + +- name: test create Floating IP idempotency + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "Web Server" + type: ipv4 + home_location: "fsn1" + register: floatingIP +- name: verify test create Floating IP idempotency + assert: + that: + - floatingIP is not changed + +- name: test update Floating IP with check mode + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "changed-description" + type: ipv4 + home_location: "fsn1" + check_mode: yes + register: floatingIP +- name: verify test create Floating IP with check mode + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.description == "Web Server" + +- name: test update Floating IP + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "changed-description" + type: ipv4 + home_location: "fsn1" + labels: + key: value + register: floatingIP +- name: verify test update Floating IP + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.description == "changed-description" + +- name: test update Floating IP idempotency + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "changed-description" + type: ipv4 + home_location: "fsn1" + labels: + key: value + register: floatingIP +- name: verify test update Floating IP idempotency + assert: + that: + - floatingIP is not changed + +- name: test update Floating IP with same labels + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + home_location: "fsn1" + labels: + key: value + register: floatingIP +- name: verify test update Floating IP with same labels + assert: + that: + - floatingIP is not changed + +- name: test update Floating IP with other labels + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + home_location: "fsn1" + labels: + key: value + other: label + register: floatingIP +- name: verify test update Floating IP with other labels + assert: + that: + - floatingIP is changed + +- name: test update Floating IP with other labels in different order + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + home_location: "fsn1" + labels: + other: label + key: value + register: floatingIP +- name: verify test update Floating IP with other labels in different order + assert: + that: + - floatingIP is not changed + +- name: test assign Floating IP with checkmode + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "changed-description" + type: ipv4 + server: "{{ main_server.hcloud_server.name }}" + check_mode: yes + register: floatingIP +- name: verify test assign Floating IP with checkmode + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.server != "{{ main_server.hcloud_server.name }}" + +- name: test assign Floating IP + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + description: "changed-description" + type: ipv4 + server: "{{ main_server.hcloud_server.name }}" + register: floatingIP +- name: verify test assign Floating IP + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.server == "{{ main_server.hcloud_server.name }}" + +- name: test unassign Floating IP + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + home_location: "fsn1" + register: floatingIP +- name: verify test unassign Floating IP + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.server != "{{ main_server.hcloud_server.name }}" + +- name: test unassign Floating IP idempotency + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + home_location: "fsn1" + register: floatingIP +- name: verify test unassign Floating IPidempotency + assert: + that: + - floatingIP is not changed + +- name: test assign Floating IP again + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + server: "{{ main_server.hcloud_server.name }}" + register: floatingIP +- name: verify test assign Floating IP again + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.server == "{{ main_server.hcloud_server.name }}" + +- name: test already assigned Floating IP assign without force + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + server: "{{ main_server2.hcloud_server.name }}" + register: floatingIP +- name: verify test already assigned Floating IP assign without force + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.server == "{{ main_server.hcloud_server.name }}" + +- name: test already assigned Floating IP assign with force + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv4 + force: yes + server: "{{ main_server2.hcloud_server.name }}" + register: floatingIP +- name: verify test already assigned Floating IP assign with force + assert: + that: + - floatingIP is changed + - floatingIP.hcloud_floating_ip.server == "{{ main_server2.hcloud_server.name }}" + +- name: test delete floating ip + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + state: "absent" + register: result +- name: verify test delete floating ip + assert: + that: + - result is changed + +- name: test create ipv6 floating ip + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + type: ipv6 + home_location: "fsn1" + state: "present" + register: result +- name: verify test create ipv6 floating ip + assert: + that: + - result is changed + +- name: test delete ipv6 floating ip + hcloud_floating_ip: + name: "{{ hcloud_floating_ip_name }}" + state: "absent" + register: result +- name: verify test delete ipv6 floating ip + assert: + that: + - result is changed + +- name: cleanup + hcloud_server: + name: "{{ hcloud_server_name }}" + state: absent + register: result +- name: verify cleanup + assert: + that: + - result is changed +- name: cleanup another server + hcloud_server: + name: "{{ main_server2.hcloud_server.name }}" + state: absent + register: result +- name: verify cleanup another server + assert: + that: + - result is changed \ No newline at end of file diff --git a/test/integration/targets/hcloud_floating_ip_info/tasks/main.yml b/test/integration/targets/hcloud_floating_ip_info/tasks/main.yml index 4b1ba555c56..9c58f83f1db 100644 --- a/test/integration/targets/hcloud_floating_ip_info/tasks/main.yml +++ b/test/integration/targets/hcloud_floating_ip_info/tasks/main.yml @@ -7,7 +7,7 @@ - name: verify test gather hcloud floating ip infos in check mode assert: that: - - hcloud_floating_ips.hcloud_floating_ip_info| list | count == 1 + - hcloud_floating_ips.hcloud_floating_ip_info| list | count >= 1 - name: test gather hcloud floating ip infos in check mode hcloud_floating_ip_info: @@ -17,7 +17,7 @@ - name: verify test gather hcloud floating ip infos in check mode assert: that: - - hcloud_floating_ips.hcloud_floating_ip_info| list | count == 1 + - hcloud_floating_ips.hcloud_floating_ip_info| list | count >= 1 - name: test gather hcloud floating ip infos with correct label selector diff --git a/test/integration/targets/hcloud_network_info/tasks/main.yml b/test/integration/targets/hcloud_network_info/tasks/main.yml index 80d3ee8f906..3bd7508d10a 100644 --- a/test/integration/targets/hcloud_network_info/tasks/main.yml +++ b/test/integration/targets/hcloud_network_info/tasks/main.yml @@ -53,8 +53,7 @@ assert: that: - hcloud_network.hcloud_network_info | selectattr('name','equalto','{{ hcloud_network_name }}') | list | count >= 1 - - hcloud_network.hcloud_network_info[0].subnetworks | list | count >= 1 - - hcloud_network.hcloud_network_info[0].routes | list | count >= 1 + - name: test gather hcloud network info with correct label selector hcloud_network_info: @@ -82,6 +81,8 @@ assert: that: - hcloud_network.hcloud_network_info | selectattr('name','equalto','{{ hcloud_network_name }}') | list | count == 1 + - hcloud_network.hcloud_network_info[0].subnetworks | list | count >= 1 + - hcloud_network.hcloud_network_info[0].routes | list | count >= 1 - name: test gather hcloud network info with wrong name hcloud_network_info: diff --git a/test/lib/ansible_test/_data/requirements/integration.cloud.hcloud.txt b/test/lib/ansible_test/_data/requirements/integration.cloud.hcloud.txt index 1d298b0a663..b6b791777b9 100644 --- a/test/lib/ansible_test/_data/requirements/integration.cloud.hcloud.txt +++ b/test/lib/ansible_test/_data/requirements/integration.cloud.hcloud.txt @@ -1 +1 @@ -hcloud>=1.4.1 ; python_version >= '2.7' # Python 2.6 is not supported (sanity_ok); Only hcloud >= 1.3.0 supports Networks; hcloud >= 1.4.1 is needed to allow a wider range of requests versions to be used +hcloud>=1.6.0 ; python_version >= '2.7' # Python 2.6 is not supported (sanity_ok); Only hcloud >= 1.6.0 supports Floating IPs with names