diff --git a/lib/ansible/modules/cloud/hcloud/hcloud_volume.py b/lib/ansible/modules/cloud/hcloud/hcloud_volume.py new file mode 100644 index 00000000000..1902c89a838 --- /dev/null +++ b/lib/ansible/modules/cloud/hcloud/hcloud_volume.py @@ -0,0 +1,312 @@ +#!/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_volume + +short_description: Create and manage block volumes on the Hetzner Cloud. + +version_added: "2.8" + +description: + - Create, update and attach/detach block volumes on the Hetzner Cloud. + +author: + - Christopher Schmitt (@cschmitt-hcloud) + +options: + id: + description: + - The ID of the Hetzner Cloud Block Volume to manage. + - Only required if no volume I(name) is given + type: int + name: + description: + - The Name of the Hetzner Cloud Block Volume to manage. + - Only required if no volume I(id) is given or a volume does not exists. + type: str + size: + description: + - The size of the Block Volume. + - Required if volume does not yet exists. + type: int + automount: + description: + - Automatically mount the Volume. + type: bool + format: + description: + - Automatically Format the volume on creation + - Can only be used in case the Volume does not exists. + type: str + choices: [xfs, ext4] + location: + description: + - Location of the Hetzner Cloud Volume. + - Required if no I(server) is given and Volume does not exists. + type: str + server: + description: + - Server Name the Volume should be assigned to. + - Required if no I(location) is given and Volume does not exists. + type: str + labels: + description: + - User-defined key-value pairs. + type: dict + state: + description: + - State of the volume. + default: present + choices: [absent, present] + type: str +extends_documentation_fragment: hcloud +""" + +EXAMPLES = """ +- name: Create a volume + hcloud_volume: + name: my-volume + location: fsn1 + size: 100 + state: present +- name: Create a volume and format it with ext4 + hcloud_volume: + name: my-volume + location: fsn + format: ext4 + size: 100 + state: present +- name: Mount a existing volume and automount + hcloud_volume: + name: my-volume + server: my-server + automount: yes + state: present +- name: Mount a existing volume and automount + hcloud_volume: + name: my-volume + server: my-server + automount: yes + state: present +- name: Ensure the volume is absent (remove if needed) + hcloud_volume: + name: my-volume + state: absent +""" + +RETURN = """ +hcloud_volume: + description: The block volume + returned: Always + type: complex + contains: + id: + description: ID of the volume + type: int + returned: Always + sample: 12345 + name: + description: Name of the volume + type: string + returned: Always + sample: my-volume + size: + description: Size in MB of the volume + type: int + returned: Always + sample: 1337 + location: + description: Location name where the volume is located at + type: string + returned: Always + sample: "fsn1" + labels: + description: User-defined labels (key-value pairs) + type: dict + returned: Always + sample: + key: value + mylabel: 123 + server: + description: Server name where the volume is attached to + type: string + returned: Always + sample: "my-server" +""" + +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.volumes.domain import Volume + from hcloud.servers.domain import Server + import hcloud +except ImportError: + pass + + +class AnsibleHcloudVolume(Hcloud): + def __init__(self, module): + Hcloud.__init__(self, module, "hcloud_volume") + self.hcloud_volume = None + + def _prepare_result(self): + server_name = None + if self.hcloud_volume.server is not None: + server_name = self.hcloud_volume.server.name + + return { + "id": to_native(self.hcloud_volume.id), + "name": to_native(self.hcloud_volume.name), + "size": self.hcloud_volume.size, + "location": to_native(self.hcloud_volume.location.name), + "labels": self.hcloud_volume.labels, + "server": to_native(server_name), + } + + def _get_volume(self): + try: + if self.module.params.get("id") is not None: + self.hcloud_volume = self.client.volumes.get_by_id( + self.module.params.get("id") + ) + else: + self.hcloud_volume = self.client.volumes.get_by_name( + self.module.params.get("name") + ) + except hcloud.APIException as e: + self.module.fail_json(msg=e.message) + + def _create_volume(self): + self.module.fail_on_missing_params( + required_params=["name", "size"] + ) + params = { + "name": self.module.params.get("name"), + "size": self.module.params.get("size"), + "automount": self.module.params.get("automount"), + "format": self.module.params.get("format"), + "labels": self.module.params.get("labels") + } + if self.module.params.get("server") is not None: + params['server'] = self.client.servers.get_by_name(self.module.params.get("server")) + elif self.module.params.get("location") is not None: + params['location'] = self.client.locations.get_by_name(self.module.params.get("location")) + else: + self.module.fail_json(msg="server or location is required") + + if not self.module.check_mode: + resp = self.client.volumes.create(**params) + resp.action.wait_until_finished() + [action.wait_until_finished() for action in resp.next_actions] + + self._mark_as_changed() + self._get_volume() + + def _update_volume(self): + size = self.module.params.get("size") + if size: + if self.hcloud_volume.size < size: + if not self.module.check_mode: + self.hcloud_volume.resize(size).wait_until_finished() + self._mark_as_changed() + elif self.hcloud_volume.size > size: + self.module.warn("Shrinking of volumes is not supported") + + server_name = self.module.params.get("server") + if server_name: + server = self.client.servers.get_by_name(server_name) + if self.hcloud_volume.server != server: + if not self.module.check_mode: + automount = self.module.params.get("automount", False) + self.hcloud_volume.attach(server, automount=automount).wait_until_finished() + self._mark_as_changed() + else: + if self.hcloud_volume.server is not None: + if not self.module.check_mode: + self.hcloud_volume.detach().wait_until_finished() + self._mark_as_changed() + + labels = self.module.params.get("labels") + if labels is not None and labels != self.hcloud_volume.labels: + if not self.module.check_mode: + self.hcloud_volume.update(labels=labels) + self._mark_as_changed() + + self._get_volume() + + def present_volume(self): + self._get_volume() + if self.hcloud_volume is None: + self._create_volume() + else: + self._update_volume() + + def delete_volume(self): + self._get_volume() + if self.hcloud_volume is not None: + if not self.module.check_mode: + self.client.volumes.delete(self.hcloud_volume) + self._mark_as_changed() + self.hcloud_volume = None + + @staticmethod + def define_module(): + return AnsibleModule( + argument_spec=dict( + id={"type": "int"}, + name={"type": "str"}, + size={"type": "int"}, + location={"type": "str"}, + server={"type": "str"}, + labels={"type": "dict"}, + automount={"type": "bool", "default": False}, + format={"type": "str", + "choices": ['xfs', 'ext4'], + }, + state={ + "choices": ["absent", "present"], + "default": "present", + }, + **Hcloud.base_module_arguments() + ), + required_one_of=[['id', 'name']], + mutually_exclusive=[["location", "server"]], + supports_check_mode=True, + ) + + +def main(): + module = AnsibleHcloudVolume.define_module() + + hcloud = AnsibleHcloudVolume(module) + state = module.params.get("state") + if state == "absent": + module.fail_on_missing_params( + required_params=["name"] + ) + hcloud.delete_volume() + else: + hcloud.present_volume() + + module.exit_json(**hcloud.get_result()) + + +if __name__ == "__main__": + main() diff --git a/test/integration/targets/hcloud_volume/aliases b/test/integration/targets/hcloud_volume/aliases new file mode 100644 index 00000000000..51742ee23f7 --- /dev/null +++ b/test/integration/targets/hcloud_volume/aliases @@ -0,0 +1,2 @@ +cloud/hcloud +unsupported diff --git a/test/integration/targets/hcloud_volume/defaults/main.yml b/test/integration/targets/hcloud_volume/defaults/main.yml new file mode 100644 index 00000000000..ad7469fb296 --- /dev/null +++ b/test/integration/targets/hcloud_volume/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_volume_name: "{{hcloud_prefix}}-integ" +hcloud_server_name: "{{hcloud_prefix}}-integ-server" diff --git a/test/integration/targets/hcloud_volume/tasks/main.yml b/test/integration/targets/hcloud_volume/tasks/main.yml new file mode 100644 index 00000000000..ba59af06b16 --- /dev/null +++ b/test/integration/targets/hcloud_volume/tasks/main.yml @@ -0,0 +1,177 @@ +# 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: test missing size parameter on create Volume + hcloud_volume: + name: "{{hcloud_volume_name}}" + server: "{{hcloud_server_name}}" + register: result + ignore_errors: yes +- name: verify fail test missing size parameter on create Volume + assert: + that: + - result is failed + - 'result.msg == "missing required arguments: size"' + +- name: test create volume with check mode + hcloud_volume: + name: "{{hcloud_volume_name}}" + size: 10 + location: "fsn1" + register: result + check_mode: yes +- name: verify create volume with check mode result + assert: + that: + - result is changed + +- name: test create volume + hcloud_volume: + name: "{{hcloud_volume_name}}" + size: 10 + location: "fsn1" + register: volume +- name: verify test create volume + assert: + that: + - volume is changed + - volume.hcloud_volume.name == "{{hcloud_volume_name}}" + - volume.hcloud_volume.location == "fsn1" + - volume.hcloud_volume.size == 10 + - volume.hcloud_volume.server != "{{hcloud_server_name}}" + +- name: test create volume idempotence + hcloud_volume: + name: "{{hcloud_volume_name}}" + size: 10 + location: "fsn1" + register: volume +- name: verify test create volume + assert: + that: + - volume is not changed + +- name: test attach volume with checkmode + hcloud_volume: + name: "{{hcloud_volume_name}}" + server: "{{hcloud_server_name}}" + check_mode: yes + register: volume +- name: verify test attach volume with checkmode + assert: + that: + - volume is changed + - volume.hcloud_volume.server != "{{hcloud_server_name}}" + +- name: test attach volume + hcloud_volume: + name: "{{hcloud_volume_name}}" + server: "{{hcloud_server_name}}" + register: volume +- name: verify attach volume + assert: + that: + - volume is changed + - volume.hcloud_volume.server == "{{hcloud_server_name}}" + - main_server is changed + +- name: test detach volume with checkmode + hcloud_volume: + name: "{{hcloud_volume_name}}" + check_mode: yes + register: volume +- name: verify detach volume with checkmode + assert: + that: + - volume is changed + - volume.hcloud_volume.server == "{{hcloud_server_name}}" + +- name: test detach volume + hcloud_volume: + name: "{{hcloud_volume_name}}" + register: volume +- name: verify detach volume + assert: + that: + - volume is changed + - volume.hcloud_volume.location == "fsn1" + - volume.hcloud_volume.server != "{{hcloud_server_name}}" + - main_server is changed + +- name: test update volume label + hcloud_volume: + name: "{{hcloud_volume_name}}" + labels: + key: value + register: volume +- name: verify test update volume lable + assert: + that: + - volume is changed + - volume.hcloud_volume.labels.key == "value" + +- name: test update volume label with the same label + hcloud_volume: + name: "{{hcloud_volume_name}}" + labels: + key: value + register: volume +- name: verify test update volume lable with the same label + assert: + that: + - volume is not changed + +- name: test increase volume size + hcloud_volume: + name: "{{hcloud_volume_name}}" + size: 11 + register: volume +- name: verify test increase volume size + assert: + that: + - volume is changed + - volume.hcloud_volume.size == 11 + +- name: test decreace volume size + hcloud_volume: + name: "{{hcloud_volume_name}}" + size: 10 + register: volume +- name: verify test decreace volume size + assert: + that: + - volume is not changed + - volume.hcloud_volume.size == 11 + +- name: test delete volume + hcloud_volume: + name: "{{hcloud_volume_name}}" + state: absent + register: result +- name: verify delete volume + assert: + that: + - result is success + +- name: cleanup + hcloud_server: + name: "{{ hcloud_server_name }}" + state: absent + register: result +- name: verify cleanup + assert: + that: + - result is success