#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright (c) 2018, Gaudenz Steinlin # Copyright (c) 2019, René Moser # 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: cloudscale_volume short_description: Manages volumes on the cloudscale.ch IaaS service. description: - Create, attach/detach, update and delete volumes on the cloudscale.ch IaaS service. notes: - To create a new volume at least the I(name) and I(size_gb) options are required. - A volume can be created and attached to a server in the same task. author: - Gaudenz Steinlin (@gaudenz) - René Moser (@resmo) - Denis Krienbühl (@href) options: state: description: - State of the volume. default: present choices: [ present, absent ] type: str name: description: - Name of the volume. Either name or UUID must be present to change an existing volume. type: str uuid: description: - UUID of the volume. Either name or UUID must be present to change an existing volume. type: str size_gb: description: - Size of the volume in GB. type: int type: description: - Type of the volume. Cannot be changed after creating the volume. Defaults to C(ssd) on volume creation. choices: [ ssd, bulk ] type: str zone: description: - Zone in which the volume resides (e.g. C(lgp1) or C(rma1)). Cannot be changed after creating the volume. Defaults to the project default zone. type: str version_added: '0.2.0' server_uuids: description: - UUIDs of the servers this volume is attached to. Set this to C([]) to detach the volume. Currently a volume can only be attached to a single server. aliases: [ server_uuid ] type: list tags: description: - Tags associated with the volume. Set this to C({}) to clear any tags. type: dict extends_documentation_fragment: - community.general.cloudscale ''' EXAMPLES = ''' # Create a new SSD volume - name: Create an SSD volume cloudscale_volume: name: my_ssd_volume zone: 'lpg1' size_gb: 50 api_token: xxxxxx register: my_ssd_volume # Attach an existing volume to a server - name: Attach volume to server cloudscale_volume: uuid: my_ssd_volume.uuid server_uuids: - ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48 api_token: xxxxxx # Create and attach a volume to a server - name: Create and attach volume to server cloudscale_volume: name: my_ssd_volume zone: 'lpg1' size_gb: 50 server_uuids: - ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48 api_token: xxxxxx # Detach volume from server - name: Detach volume from server cloudscale_volume: uuid: my_ssd_volume.uuid server_uuids: [] api_token: xxxxxx # Delete a volume - name: Delete volume cloudscale_volume: name: my_ssd_volume state: absent api_token: xxxxxx ''' RETURN = ''' href: description: The API URL to get details about this volume. returned: state == present type: str sample: https://api.cloudscale.ch/v1/volumes/2db69ba3-1864-4608-853a-0771b6885a3a uuid: description: The unique identifier for this volume. returned: state == present type: str sample: 2db69ba3-1864-4608-853a-0771b6885a3a name: description: The display name of the volume. returned: state == present type: str sample: my_ssd_volume size_gb: description: The size of the volume in GB. returned: state == present type: str sample: 50 type: description: The type of the volume. returned: state == present type: str sample: bulk zone: description: The zone of the volume. returned: state == present type: dict sample: {'slug': 'lpg1'} version_added: '0.2.0' server_uuids: description: The UUIDs of the servers this volume is attached to. returned: state == present type: list sample: ['47cec963-fcd2-482f-bdb6-24461b2d47b1'] state: description: The current status of the volume. returned: success type: str sample: present tags: description: Tags associated with the volume. returned: state == present type: dict sample: { 'project': 'my project' } ''' from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.cloudscale import (AnsibleCloudscaleBase, cloudscale_argument_spec, ) class AnsibleCloudscaleVolume(AnsibleCloudscaleBase): def __init__(self, module): super(AnsibleCloudscaleVolume, self).__init__(module) self._info = {} def _init_container(self): return { 'uuid': self._module.params.get('uuid') or self._info.get('uuid'), 'name': self._module.params.get('name') or self._info.get('name'), 'state': 'absent', } def _create(self, volume): # Fail when missing params for creation self._module.fail_on_missing_params(['name', 'size_gb']) # Fail if a user uses a UUID and state=present but the volume was not found. if self._module.params.get('uuid'): self._module.fail_json(msg="The volume with UUID '%s' was not found " "and we would create a new one with different UUID, " "this is probably not want you have asked for." % self._module.params.get('uuid')) self._result['changed'] = True data = { 'name': self._module.params.get('name'), 'type': self._module.params.get('type'), 'zone': self._module.params.get('zone'), 'size_gb': self._module.params.get('size_gb') or 'ssd', 'server_uuids': self._module.params.get('server_uuids') or [], 'tags': self._module.params.get('tags'), } if not self._module.check_mode: volume = self._post('volumes', data) return volume def _update(self, volume): update_params = ( 'name', 'size_gb', 'server_uuids', 'tags', ) updated = False for param in update_params: updated = self._param_updated(param, volume) or updated # Refresh if resource was updated in live mode if updated and not self._module.check_mode: volume = self.get_volume() return volume def get_volume(self): self._info = self._init_container() uuid = self._info.get('uuid') if uuid is not None: volume = self._get('volumes/%s' % uuid) if volume: self._info.update(volume) self._info['state'] = 'present' else: name = self._info.get('name') matching_volumes = [] for volume in self._get('volumes'): if volume['name'] == name: matching_volumes.append(volume) if len(matching_volumes) > 1: self._module.fail_json(msg="More than one volume with name exists: '%s'. " "Use the 'uuid' parameter to identify the volume." % name) elif len(matching_volumes) == 1: self._info.update(matching_volumes[0]) self._info['state'] = 'present' return self._info def present(self): volume = self.get_volume() if volume.get('state') == 'absent': volume = self._create(volume) else: volume = self._update(volume) return volume def absent(self): volume = self.get_volume() if volume.get('state') != 'absent': self._result['changed'] = True if not self._module.check_mode: volume['state'] = "absent" self._delete('volumes/%s' % volume['uuid']) return volume def main(): argument_spec = cloudscale_argument_spec() argument_spec.update(dict( state=dict(default='present', choices=('present', 'absent')), name=dict(), uuid=dict(), zone=dict(), size_gb=dict(type='int'), type=dict(choices=('ssd', 'bulk')), server_uuids=dict(type='list', aliases=['server_uuid']), tags=dict(type='dict'), )) module = AnsibleModule( argument_spec=argument_spec, required_one_of=(('name', 'uuid'),), supports_check_mode=True, ) cloudscale_volume = AnsibleCloudscaleVolume(module) if module.params['state'] == 'absent': server_group = cloudscale_volume.absent() else: server_group = cloudscale_volume.present() result = cloudscale_volume.get_result(server_group) module.exit_json(**result) if __name__ == '__main__': main()