summaryrefslogtreecommitdiffstats
path: root/ansible_collections/cloudscale_ch/cloud/plugins/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/cloudscale_ch/cloud/plugins/modules
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/cloudscale_ch/cloud/plugins/modules')
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/__init__.py0
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/custom_image.py468
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/floating_ip.py285
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer.py240
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_health_monitor.py398
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_listener.py271
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool.py217
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool_member.py319
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/network.py197
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/objects_user.py161
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/server.py737
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/server_group.py171
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/subnet.py322
-rw-r--r--ansible_collections/cloudscale_ch/cloud/plugins/modules/volume.py265
14 files changed, 4051 insertions, 0 deletions
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/__init__.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/__init__.py
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/custom_image.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/custom_image.py
new file mode 100644
index 000000000..61e2f77f7
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/custom_image.py
@@ -0,0 +1,468 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2021, Ciril Troxler <ciril.troxler@cloudscale.ch>
+# 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 = r'''
+---
+module: custom_image
+short_description: Manage custom images on the cloudscale.ch IaaS service
+description:
+ - Import, modify and delete custom images.
+notes:
+ - To import a new custom-image the I(url) and I(name) options are required.
+author:
+ - Ciril Troxler (@ctx)
+ - Gaudenz Steinlin (@gaudenz)
+version_added: 2.2.0
+options:
+ url:
+ description:
+ - The URL used to download the image.
+ type: str
+ force_retry:
+ description:
+ - Retry the image import even if a failed import using the same name and
+ URL already exists. This is necessary to recover from download errors.
+ default: false
+ type: bool
+ name:
+ description:
+ - The human readable name of the custom image. Either name or UUID must
+ be present to change an existing image.
+ type: str
+ uuid:
+ description:
+ - The unique identifier of the custom image import. Either name or UUID
+ must be present to change an existing image.
+ type: str
+ slug:
+ description:
+ - A string identifying the custom image for use within the API.
+ type: str
+ user_data_handling:
+ description:
+ - How user_data will be handled when creating a server. There are
+ currently two options, "pass-through" and "extend-cloud-config".
+ type: str
+ choices: [ pass-through, extend-cloud-config ]
+ zones:
+ description:
+ - Specify zones in which the custom image will be available (e.g. C(lpg1)
+ or C(rma1)).
+ type: list
+ elements: str
+ source_format:
+ description:
+ - The file format of the image referenced in the url. Currently only raw
+ is supported.
+ type: str
+ firmware_type:
+ description:
+ - The firmware type that will be used for servers created
+ with this image.
+ type: str
+ choices: [ bios, uefi ]
+ default: bios
+ tags:
+ description:
+ - The tags assigned to the custom image.
+ type: dict
+ state:
+ description: State of the coustom image.
+ choices: [ present, absent ]
+ default: present
+ type: str
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = r'''
+- name: Import custom image
+ cloudscale_ch.cloud.custom_image:
+ name: "My Custom Image"
+ url: https://ubuntu.com/downloads/hirsute.img
+ slug: my-custom-image
+ user_data_handling: extend-cloud-config
+ zones: lpg1
+ tags:
+ project: luna
+ state: present
+ register: my_custom_image
+
+- name: Wait until import succeeded
+ cloudscale_ch.cloud.custom_image:
+ uuid: "{{ my_custom_image.uuid }}"
+ retries: 15
+ delay: 5
+ register: image
+ until: image.import_status == 'success'
+ failed_when: image.import_status == 'failed'
+
+- name: Import custom image and wait until import succeeded
+ cloudscale_ch.cloud.custom_image:
+ name: "My Custom Image"
+ url: https://ubuntu.com/downloads/hirsute.img
+ slug: my-custom-image
+ user_data_handling: extend-cloud-config
+ zones: lpg1
+ tags:
+ project: luna
+ state: present
+ retries: 15
+ delay: 5
+ register: image
+ until: image.import_status == 'success'
+ failed_when: image.import_status == 'failed'
+
+- name: Import custom image with UEFI firmware type
+ cloudscale_ch.cloud.custom_image:
+ name: "My Custom UEFI Image"
+ url: https://ubuntu.com/downloads/hirsute.img
+ slug: my-custom-uefi-image
+ user_data_handling: extend-cloud-config
+ zones: lpg1
+ firmware_type: uefi
+ tags:
+ project: luna
+ state: present
+ register: my_custom_image
+
+- name: Update custom image
+ cloudscale_ch.cloud.custom_image:
+ name: "My Custom Image"
+ slug: my-custom-image
+ user_data_handling: extend-cloud-config
+ tags:
+ project: luna
+ state: present
+
+- name: Delete custom image
+ cloudscale_ch.cloud.custom_image:
+ uuid: '{{ my_custom_image.uuid }}'
+ state: absent
+
+- name: List all custom images
+ uri:
+ url: 'https://api.cloudscale.ch/v1/custom-images'
+ headers:
+ Authorization: 'Bearer {{ query("env", "CLOUDSCALE_API_TOKEN") }}'
+ status_code: 200
+ register: image_list
+- name: Search the image list for all images with name 'My Custom Image'
+ set_fact:
+ my_custom_images: '{{ image_list.json | selectattr("name","search", "My Custom Image" ) }}'
+'''
+
+RETURN = r'''
+href:
+ description: The API URL to get details about this resource.
+ returned: success when state == present
+ type: str
+ sample: https://api.cloudscale.ch/v1/custom-imges/11111111-1864-4608-853a-0771b6885a3a
+uuid:
+ description: The unique identifier of the custom image.
+ returned: success
+ type: str
+ sample: 11111111-1864-4608-853a-0771b6885a3a
+name:
+ description: The human readable name of the custom image.
+ returned: success
+ type: str
+ sample: alan
+created_at:
+ description: The creation date and time of the resource.
+ returned: success
+ type: str
+ sample: "2020-05-29T13:18:42.511407Z"
+slug:
+ description: A string identifying the custom image for use within the API.
+ returned: success
+ type: str
+ sample: foo
+checksums:
+ description: The checksums of the custom image as key and value pairs. The
+ algorithm (e.g. sha256) name is in the key and the checksum in the value.
+ The set of algorithms used might change in the future.
+ returned: success
+ type: dict
+ sample: {
+ "md5": "5b3a1f21cde154cfb522b582f44f1a87",
+ "sha256": "5b03bcbd00b687e08791694e47d235a487c294e58ca3b1af704120123aa3f4e6"
+ }
+user_data_handling:
+ description: How user_data will be handled when creating a server. There are
+ currently two options, "pass-through" and "extend-cloud-config".
+ returned: success
+ type: str
+ sample: "pass-through"
+tags:
+ description: Tags assosiated with the custom image.
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+import_status:
+ description: Shows the progress of an import. Values are one of
+ "started", "in_progress", "success" or "failed".
+ returned: success
+ type: str
+ sample: "in_progress"
+error_message:
+ description: Error message in case of a failed import.
+ returned: success
+ type: str
+ sample: "Expected HTTP 200, got HTTP 403"
+state:
+ description: The current status of the custom image.
+ returned: success
+ type: str
+ sample: present
+'''
+
+
+from ansible.module_utils.basic import (
+ AnsibleModule,
+)
+from ansible.module_utils.urls import (
+ fetch_url
+)
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+from ansible.module_utils._text import (
+ to_text
+)
+
+
+class AnsibleCloudscaleCustomImage(AnsibleCloudscaleBase):
+
+ def _transform_import_to_image(self, imp):
+ # Create a stub image from the import
+ img = imp.get('custom_image', {})
+ return {
+ 'href': img.get('href'),
+ 'uuid': imp['uuid'],
+ 'name': img.get('name'),
+ 'created_at': None,
+ 'size_gb': None,
+ 'checksums': None,
+ 'tags': imp['tags'],
+ 'url': imp['url'],
+ 'import_status': imp['status'],
+ 'error_message': imp.get('error_message', ''),
+ # Even failed image imports are reported as present. This then
+ # represents a failed import resource.
+ 'state': 'present',
+ # These fields are not present on the import, assume they are
+ # unchanged from the module parameters
+ 'user_data_handling': self._module.params['user_data_handling'],
+ 'zones': self._module.params['zones'],
+ 'slug': self._module.params['slug'],
+ 'firmware_type': self._module.params['firmware_type'],
+ }
+
+ # This method can be replaced by calling AnsibleCloudscaleBase._get form
+ # AnsibleCloudscaleCustomImage._get once the API bug is fixed.
+ def _get_url(self, url):
+
+ response, info = fetch_url(self._module,
+ url,
+ headers=self._auth_header,
+ method='GET',
+ timeout=self._module.params['api_timeout'])
+
+ if info['status'] == 200:
+ response = self._module.from_json(
+ to_text(response.read(),
+ errors='surrogate_or_strict'),
+ )
+ elif info['status'] == 404:
+ # Return None to be compatible with AnsibleCloudscaleBase._get
+ response = None
+ elif info['status'] == 500 and url.startswith(self._api_url + self.resource_name + '/import/'):
+ # Workaround a bug in the cloudscale.ch API which wrongly returns
+ # 500 instead of 404
+ response = None
+ else:
+ self._module.fail_json(
+ msg='Failure while calling the cloudscale.ch API with GET for '
+ '"%s"' % url,
+ fetch_url_info=info,
+ )
+
+ return response
+
+ def _get(self, api_call):
+
+ # Split api_call into components
+ api_url, call_uuid = api_call.split(self.resource_name)
+
+ # If the api_call does not contain the API URL
+ if not api_url:
+ api_url = self._api_url
+
+ # Fetch image(s) from the regular API endpoint
+ response = self._get_url(api_url + self.resource_name + call_uuid) or []
+
+ # Additionally fetch image(s) from the image import API endpoint
+ response_import = self._get_url(
+ api_url + self.resource_name + '/import' + call_uuid,
+ ) or []
+
+ # No image was found
+ if call_uuid and response == [] and response_import == []:
+ return None
+
+ # Convert single image responses (call with UUID) into a list
+ if call_uuid and response:
+ response = [response]
+ if call_uuid and response_import:
+ response_import = [response_import]
+
+ # Transform lists into UUID keyed dicts
+ response = dict([(i['uuid'], i) for i in response])
+ response_import = dict([(i['uuid'], i) for i in response_import])
+
+ # Filter the import list so that successfull and in_progress imports
+ # shadow failed imports
+ response_import_filtered = dict([(k, v) for k, v
+ in response_import.items()
+ if v['status'] in ('success',
+ 'in_progress')])
+ # Only add failed imports if no import with the same name exists
+ # Only add the last failed import in the list (there is no timestamp on
+ # imports)
+ import_names = set([v['custom_image']['name'] for k, v
+ in response_import_filtered.items()])
+ for k, v in reversed(list(response_import.items())):
+ name = v['custom_image']['name']
+ if (v['status'] == 'failed' and name not in import_names):
+ import_names.add(name)
+ response_import_filtered[k] = v
+
+ # Merge import list into image list
+ for uuid, imp in response_import_filtered.items():
+ if uuid in response:
+ # Merge addtional fields only present on the import
+ response[uuid].update(
+ url=imp['url'],
+ import_status=imp['status'],
+ error_message=imp.get('error_message', ''),
+ )
+ else:
+ response[uuid] = self._transform_import_to_image(imp)
+
+ if not call_uuid:
+ return response.values()
+ else:
+ return next(iter(response.values()))
+
+ def _post(self, api_call, data=None):
+ # Only new image imports are supported, no direct POST call to image
+ # resources are supported by the API
+ if not api_call.endswith('custom-images'):
+ self._module.fail_json(msg="Error: Bad api_call URL.")
+ # Custom image imports use a different endpoint
+ api_call += '/import'
+
+ if self._module.params['url']:
+ return self._transform_import_to_image(
+ self._post_or_patch("%s" % api_call, 'POST', data),
+ )
+ else:
+ self._module.fail_json(msg="Cannot import a new image without url.")
+
+ def present(self):
+ resource = self.query()
+
+ # If the module passes the firmware_type argument,
+ # and the module argument and API response are not the same for
+ # argument firmware_type.
+ if (resource.get('firmware_type') is not None
+ and resource.get('firmware_type') !=
+ self._module.params['firmware_type']):
+ # Custom error if the module tries to change the firmware_type.
+ msg = "Cannot change firmware type of an existing custom image"
+ self._module.fail_json(msg)
+
+ if resource['state'] == "absent":
+ resource = self.create(resource)
+ else:
+ # If this is a failed upload and the URL changed or the "force_retry"
+ # parameter is used, create a new image import.
+ if (resource.get('import_status') == 'failed'
+ and (resource['url'] != self._module.params['url']
+ or self._module.params['force_retry'])):
+ resource = self.create(resource)
+ else:
+ resource = self.update(resource)
+
+ return self.get_result(resource)
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str'),
+ slug=dict(type='str'),
+ url=dict(type='str'),
+ force_retry=dict(type='bool', default=False),
+ user_data_handling=dict(type='str',
+ choices=('pass-through',
+ 'extend-cloud-config')),
+ uuid=dict(type='str'),
+ firmware_type=dict(type='str',
+ choices=('bios',
+ 'uefi'),
+ default=('bios')),
+ tags=dict(type='dict'),
+ state=dict(type='str', default='present',
+ choices=('present', 'absent')),
+ zones=dict(type='list', elements='str'),
+ source_format=dict(type='str'),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(('name', 'uuid'),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_custom_image = AnsibleCloudscaleCustomImage(
+ module,
+ resource_name='custom-images',
+ resource_key_uuid='uuid',
+ resource_key_name='name',
+ resource_create_param_keys=[
+ 'name',
+ 'slug',
+ 'url',
+ 'user_data_handling',
+ 'firmware_type',
+ 'tags',
+ 'zones',
+ 'source_format',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'slug',
+ 'user_data_handling',
+ 'firmware_type',
+ 'tags',
+ ],
+ )
+
+ if module.params['state'] == "absent":
+ result = cloudscale_custom_image.absent()
+ else:
+ result = cloudscale_custom_image.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/floating_ip.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/floating_ip.py
new file mode 100644
index 000000000..7f578d18c
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/floating_ip.py
@@ -0,0 +1,285 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# 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: floating_ip
+short_description: Manages floating IPs on the cloudscale.ch IaaS service
+description:
+ - Create, assign and delete floating IPs on the cloudscale.ch IaaS service.
+notes:
+ - Once a floating_ip is created, all parameters except C(server), C(reverse_ptr) and C(tags) are read-only.
+author:
+ - Gaudenz Steinlin (@gaudenz)
+ - Denis Krienbühl (@href)
+ - René Moser (@resmo)
+version_added: 1.0.0
+options:
+ network:
+ description:
+ - Floating IP address to change.
+ - One of I(network) or I(name) is required to identify the floating IP.
+ aliases: [ ip ]
+ type: str
+ name:
+ description:
+ - Name to identifiy the floating IP address for idempotency.
+ - One of I(network) or I(name) is required to identify the floating IP.
+ - Required for assigning a new floating IP.
+ version_added: 1.3.0
+ type: str
+ state:
+ description:
+ - State of the floating IP.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ ip_version:
+ description:
+ - IP protocol version of the floating IP.
+ - Required when assigning a new floating IP.
+ choices: [ 4, 6 ]
+ type: int
+ server:
+ description:
+ - UUID of the server assigned to this floating IP.
+ type: str
+ type:
+ description:
+ - The type of the floating IP.
+ choices: [ regional, global ]
+ type: str
+ default: regional
+ region:
+ description:
+ - Region in which the floating IP resides (e.g. C(lpg) or C(rma)).
+ If omitted, the region of the project default zone is used.
+ This parameter must be omitted if I(type) is set to C(global).
+ type: str
+ prefix_length:
+ description:
+ - Only valid if I(ip_version) is 6.
+ - Prefix length for the IPv6 network. Currently only a prefix of /56 can be requested. If no I(prefix_length) is present, a
+ single address is created.
+ choices: [ 56 ]
+ type: int
+ reverse_ptr:
+ description:
+ - Reverse PTR entry for this address.
+ - You cannot set a reverse PTR entry for IPv6 floating networks. Reverse PTR entries are only allowed for single addresses.
+ type: str
+ tags:
+ description:
+ - Tags associated with the floating IP. Set this to C({}) to clear any tags.
+ type: dict
+ version_added: 1.1.0
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Request a new floating IP without assignment to a server
+- name: Request a floating IP
+ cloudscale_ch.cloud.floating_ip:
+ name: IP to my server
+ ip_version: 4
+ reverse_ptr: my-server.example.com
+ api_token: xxxxxx
+
+# Request a new floating IP with assignment
+- name: Request a floating IP
+ cloudscale_ch.cloud.floating_ip:
+ name: web
+ ip_version: 4
+ server: 47cec963-fcd2-482f-bdb6-24461b2d47b1
+ reverse_ptr: my-server.example.com
+ api_token: xxxxxx
+
+# Assign an existing floating IP to a different server by its IP address
+- name: Move floating IP to backup server
+ cloudscale_ch.cloud.floating_ip:
+ ip: 192.0.2.123
+ server: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
+ api_token: xxxxxx
+
+# Assign an existing floating IP to a different server by name
+- name: Move floating IP to backup server
+ cloudscale_ch.cloud.floating_ip:
+ name: IP to my server
+ server: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
+ api_token: xxxxxx
+
+# Request a new floating IPv6 network
+- name: Request a floating IP
+ cloudscale_ch.cloud.floating_ip:
+ name: IPv6 to my server
+ ip_version: 6
+ prefix_length: 56
+ server: 47cec963-fcd2-482f-bdb6-24461b2d47b1
+ api_token: xxxxxx
+ region: lpg1
+
+# Assign an existing floating network to a different server
+- name: Move floating IP to backup server
+ cloudscale_ch.cloud.floating_ip:
+ ip: '{{ floating_ip.ip }}'
+ server: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
+ api_token: xxxxxx
+
+# Remove a floating IP
+- name: Release floating IP
+ cloudscale_ch.cloud.floating_ip:
+ ip: 192.0.2.123
+ state: absent
+ api_token: xxxxxx
+
+# Remove a floating IP by name
+- name: Release floating IP
+ cloudscale_ch.cloud.floating_ip:
+ name: IP to my server
+ state: absent
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+name:
+ description: The name of the floating IP.
+ returned: success
+ type: str
+ sample: my floating ip
+ version_added: 1.3.0
+href:
+ description: The API URL to get details about this floating IP.
+ returned: success when state == present
+ type: str
+ sample: https://api.cloudscale.ch/v1/floating-ips/2001:db8::cafe
+network:
+ description: The CIDR notation of the network that is routed to your server.
+ returned: success
+ type: str
+ sample: 2001:db8::cafe/128
+next_hop:
+ description: Your floating IP is routed to this IP address.
+ returned: success when state == present
+ type: str
+ sample: 2001:db8:dead:beef::42
+reverse_ptr:
+ description: The reverse pointer for this floating IP address.
+ returned: success when state == present
+ type: str
+ sample: 185-98-122-176.cust.cloudscale.ch
+server:
+ description: The floating IP is routed to this server.
+ returned: success when state == present
+ type: str
+ sample: 47cec963-fcd2-482f-bdb6-24461b2d47b1
+ip:
+ description: The floating IP address.
+ returned: success when state == present
+ type: str
+ sample: 185.98.122.176
+region:
+ description: The region of the floating IP.
+ returned: success when state == present
+ type: dict
+ sample: {'slug': 'lpg'}
+state:
+ description: The current status of the floating IP.
+ returned: success
+ type: str
+ sample: present
+tags:
+ description: Tags assosiated with the floating IP.
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+ version_added: 1.1.0
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+
+class AnsibleCloudscaleFloatingIp(AnsibleCloudscaleBase):
+
+ def __init__(self, module):
+ super(AnsibleCloudscaleFloatingIp, self).__init__(
+ module=module,
+ resource_key_uuid='network',
+ resource_name='floating-ips',
+ resource_create_param_keys=[
+ 'ip_version',
+ 'server',
+ 'prefix_length',
+ 'reverse_ptr',
+ 'type',
+ 'region',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'server',
+ 'reverse_ptr',
+ 'tags',
+ ],
+ )
+ self.use_tag_for_name = True
+ self.query_constraint_keys = ['ip_version']
+
+ def pre_transform(self, resource):
+ if 'server' in resource and isinstance(resource['server'], dict):
+ resource['server'] = resource['server']['uuid']
+ return resource
+
+ def create(self, resource):
+ # Fail when missing params for creation
+ self._module.fail_on_missing_params(['ip_version', 'name'])
+ return super(AnsibleCloudscaleFloatingIp, self).create(resource)
+
+ def get_result(self, resource):
+ network = resource.get('network')
+ if network:
+ self._result['ip'] = network.split('/')[0]
+ return super(AnsibleCloudscaleFloatingIp, self).get_result(resource)
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str'),
+ state=dict(default='present', choices=('present', 'absent'), type='str'),
+ network=dict(aliases=('ip',), type='str'),
+ ip_version=dict(choices=(4, 6), type='int'),
+ server=dict(type='str'),
+ type=dict(type='str', choices=('regional', 'global'), default='regional'),
+ region=dict(type='str'),
+ prefix_length=dict(choices=(56,), type='int'),
+ reverse_ptr=dict(type='str'),
+ tags=dict(type='dict'),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(('network', 'name'),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_floating_ip = AnsibleCloudscaleFloatingIp(module)
+
+ if module.params['state'] == 'absent':
+ result = cloudscale_floating_ip.absent()
+ else:
+ result = cloudscale_floating_ip.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer.py
new file mode 100644
index 000000000..4e5682979
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer.py
@@ -0,0 +1,240 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2023, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# Copyright: (c) 2023, Kenneth Joss <kenneth.joss@cloudscale.ch>
+# 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: load_balancer
+short_description: Manages load balancers on the cloudscale.ch IaaS service
+description:
+ - Get, create, update, delete load balancers on the cloudscale.ch IaaS service.
+notes:
+ - If I(uuid) option is provided, it takes precedence over I(name) for load balancer selection. This allows to update the load balancers's name.
+ - If no I(uuid) option is provided, I(name) is used for load balancer selection. If more than one load balancer with this name exists, execution is aborted.
+author:
+ - Gaudenz Steinlin (@gaudenz)
+ - Kenneth Joss (@k-304)
+version_added: "2.3.0"
+options:
+ state:
+ description:
+ - State of the load balancer.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ name:
+ description:
+ - Name of the load balancer.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ uuid:
+ description:
+ - UUID of the load balancer.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ flavor:
+ description:
+ - Flavor of the load balancer.
+ default: lb-standard
+ type: str
+ vip_addresses:
+ description:
+ - See the [API documentation](https://www.cloudscale.ch/en/api/v1#vip_addresses-attribute-specification) for details about this parameter.
+ type: list
+ elements: dict
+ suboptions:
+ subnet:
+ description:
+ - Create a VIP address on the subnet identified by this UUID.
+ type: str
+ address:
+ description:
+ - Use this address.
+ - Must be in the same range as subnet.
+ - If empty, a radom address will be used.
+ type: str
+ zone:
+ description:
+ - Zone in which the load balancer resides (e.g. C(lpg1) or C(rma1)).
+ type: str
+ tags:
+ description:
+ - Tags assosiated with the load balancer. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Create and start a load balancer
+- name: Start cloudscale.ch load balancer
+ cloudscale_ch.cloud.load_balancer:
+ name: my-shiny-cloudscale-load-balancer
+ flavor: lb-standard
+ zone: rma1
+ tags:
+ project: my project
+ api_token: xxxxxx
+
+# Create and start a load balancer with specific subnet
+- name: Start cloudscale.ch load balancer
+ cloudscale_ch.cloud.load_balancer:
+ name: my-shiny-cloudscale-load-balancer
+ flavor: lb-standard
+ vip_addresses:
+ - subnet: d7b82c9b-5900-436c-9296-e94dca01c7a0
+ address: 172.25.12.1
+ zone: lpg1
+ tags:
+ project: my project
+ api_token: xxxxxx
+
+# Get load balancer facts by name
+- name: Get facts of a load balancer
+ cloudscale_ch.cloud.load_balancer:
+ name: my-shiny-cloudscale-load-balancer
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+href:
+ description: API URL to get details about this load balancer
+ returned: success when not state == absent
+ type: str
+ sample: https://api.cloudscale.ch/v1/load-balancers/0f62e0a7-f459-4fc4-9c25-9e57b6cb4b2f
+uuid:
+ description: The unique identifier for this load balancer
+ returned: success
+ type: str
+ sample: cfde831a-4e87-4a75-960f-89b0148aa2cc
+name:
+ description: The display name of the load balancer
+ returned: success
+ type: str
+ sample: web-lb
+created_at:
+ description: The creation date and time of the load balancer
+ returned: success when not state == absent
+ type: str
+ sample: "2023-02-07T15:32:02.308041Z"
+status:
+ description: The current operational status of the load balancer
+ returned: success
+ type: str
+ sample: running
+state:
+ description: The current state of the load balancer
+ returned: success
+ type: str
+ sample: present
+zone:
+ description: The zone used for this load balancer
+ returned: success when not state == absent
+ type: dict
+ sample: { 'slug': 'lpg1' }
+flavor:
+ description: The flavor that has been used for this load balancer
+ returned: success when not state == absent
+ type: list
+ sample: { "slug": "lb-standard", "name": "LB-Standard" }
+vip_addresses:
+ description: List of vip_addresses for this load balancer
+ returned: success when not state == absent
+ type: dict
+ sample: [ {"version": "4", "address": "192.0.2.110",
+ "subnet": [
+ "href": "https://api.cloudscale.ch/v1/subnets/92c70b2f-99cb-4811-8823-3d46572006e4",
+ "uuid": "92c70b2f-99cb-4811-8823-3d46572006e4",
+ "cidr": "192.0.2.0/24"
+ ]} ]
+tags:
+ description: Tags assosiated with the load balancer
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+ALLOWED_STATES = ('present',
+ 'absent',
+ )
+
+
+class AnsibleCloudscaleLoadBalancer(AnsibleCloudscaleBase):
+
+ def __init__(self, module):
+ super(AnsibleCloudscaleLoadBalancer, self).__init__(
+ module,
+ resource_name='load-balancers',
+ resource_create_param_keys=[
+ 'name',
+ 'flavor',
+ 'zone',
+ 'vip_addresses',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'tags',
+ ],
+ )
+
+ def create(self, resource, data=None):
+ super().create(resource)
+ if not self._module.check_mode:
+ resource = self.wait_for_state('status', ('running', ))
+ return resource
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str'),
+ uuid=dict(type='str'),
+ flavor=dict(type='str', default='lb-standard'),
+ zone=dict(type='str'),
+ vip_addresses=dict(
+ type='list',
+ elements='dict',
+ options=dict(
+ subnet=dict(type='str'),
+ address=dict(type='str'),
+ ),
+ ),
+ tags=dict(type='dict'),
+ state=dict(type='str', default='present', choices=ALLOWED_STATES),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(),
+ required_one_of=(('name', 'uuid'),),
+ required_if=(('state', 'present', ('name',),),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_load_balancer = AnsibleCloudscaleLoadBalancer(module)
+ cloudscale_load_balancer.query_constraint_keys = [
+ 'zone',
+ ]
+
+ if module.params['state'] == "absent":
+ result = cloudscale_load_balancer.absent()
+ else:
+ result = cloudscale_load_balancer.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_health_monitor.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_health_monitor.py
new file mode 100644
index 000000000..cf99e0cd3
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_health_monitor.py
@@ -0,0 +1,398 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2023, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# Copyright: (c) 2023, Kenneth Joss <kenneth.joss@cloudscale.ch>
+# 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: load_balancer_health_monitor
+short_description: Manages load balancers on the cloudscale.ch IaaS service
+description:
+ - Get, create, update, delete health monitors on the cloudscale.ch IaaS service.
+notes:
+ - Health monitors do not have names. I(uuid)'s are used to reference a health monitors.
+author:
+ - Gaudenz Steinlin (@gaudenz)
+ - Kenneth Joss (@k-304)
+version_added: "2.3.0"
+options:
+ state:
+ description:
+ - State of the load balancer health monitor.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ uuid:
+ description:
+ - UUID of the load balancer health monitor.
+ type: str
+ pool:
+ description:
+ - The pool of the health monitor.
+ type: str
+ delay_s:
+ description:
+ - The delay between two successive checks in seconds.
+ type: int
+ timeout_s:
+ description:
+ - The maximum time allowed for an individual check in seconds.
+ type: int
+ up_threshold:
+ description:
+ - The number of checks that need to be successful before the monitor_status of a pool member changes to "up".
+ type: int
+ down_threshold:
+ description:
+ - The number of checks that need to fail before the monitor_status of a pool member changes to "down".
+ type: int
+ type:
+ description:
+ - The type of the health monitor.
+ - See the [API documentation](https://www.cloudscale.ch/en/api/v1#create-a-health-monitor) for allowed options.
+ type: str
+ http:
+ description:
+ - Advanced options for health monitors with type "http" or "https".
+ type: dict
+ suboptions:
+ expected_codes:
+ description:
+ - The HTTP status codes allowed for a check to be considered successful.
+ - See the [API documentation](https://www.cloudscale.ch/en/api/v1#http-attribute-specification) for details.
+ type: list
+ elements: str
+ method:
+ description:
+ - The HTTP method used for the check.
+ type: str
+ url_path:
+ description:
+ - The URL used for the check.
+ type: str
+ version:
+ description:
+ - The HTTP version used for the check.
+ type: str
+ host:
+ description:
+ - The server name in the HTTP Host header used for the check.
+ - Requires version to be set to "1.1".
+ type: str
+ tags:
+ description:
+ - Tags assosiated with the load balancer. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Create a simple health monitor for a pool
+- name: Create a load balancer pool
+ cloudscale_ch.cloud.load_balancer_pool:
+ name: 'swimming-pool'
+ load_balancer: '3d41b118-f95c-4897-ad74-2260fea783fc'
+ algorithm: 'round_robin'
+ protocol: 'tcp'
+ api_token: xxxxxx
+ register: load_balancer_pool
+
+- name: Create a load balancer health monitor (ping)
+ cloudscale_ch.cloud.load_balancer_health_monitor:
+ pool: '{{ load_balancer_pool.uuid }}'
+ type: 'ping'
+ api_token: xxxxxx
+ register: load_balancer_health_monitor
+
+# Get load balancer health monitor facts by UUID
+- name: Get facts of a load balancer health monitor by UUID
+ cloudscale_ch.cloud.load_balancer_health_monitor:
+ uuid: '{{ load_balancer_health_monitor.uuid }}'
+ api_token: xxxxxx
+
+# Update a health monitor
+- name: Update HTTP method of a load balancer health monitor from GET to CONNECT
+ cloudscale_ch.cloud.load_balancer_health_monitor:
+ uuid: '{{ load_balancer_health_monitor_http.uuid }}'
+ delay_s: 2
+ timeout_s: 1
+ up_threshold: 2
+ down_threshold: 3
+ type: 'http'
+ http:
+ expected_codes:
+ - 200
+ - 202
+ method: 'CONNECT'
+ url_path: '/'
+ version: '1.1'
+ host: 'host1'
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+ register: load_balancer_health_monitor
+'''
+
+RETURN = '''
+href:
+ description: API URL to get details about this load balancer health monitor
+ returned: success when not state == absent
+ type: str
+ sample: https://api.cloudscale.ch/v1/load-balancers/health-monitors/ee4952d4-2eba-4dec-8957-7911b3ce245b
+uuid:
+ description: The unique identifier for this load balancer health monitor
+ returned: success
+ type: str
+ sample: ee4952d4-2eba-4dec-8957-7911b3ce245b
+created_at:
+ description: The creation date and time of the load balancer health monitor
+ returned: success when not state == absent
+ type: str
+ sample: "2023-02-22T09:55:38.285018Z"
+pool:
+ description: The pool of the health monitor
+ returned: success when not state == absent
+ type: dict
+ sample: [
+ "href": "https://api.cloudscale.ch/v1/load-balancers/pools/618a6cc8-d757-4fab-aa10-d49dc47e667b",
+ "uuid": "618a6cc8-d757-4fab-aa10-d49dc47e667b",
+ "name": "swimming pool"
+ ]
+delay_s:
+ description: The delay between two successive checks in seconds
+ returned: success when not state == absent
+ type: int
+ sample: 2
+timeout_s:
+ description: The maximum time allowed for an individual check in seconds
+ returned: success when not state == absent
+ type: int
+ sample: 1
+up_threshold:
+ description: The number of checks that need to be successful before the monitor_status of a pool member changes to "up"
+ returned: success when not state == absent
+ type: int
+ sample: 2
+down_threshold:
+ description: The number of checks that need to fail before the monitor_status of a pool member changes to "down"
+ returned: success when not state == absent
+ type: int
+ sample: 3
+type:
+ description: The type of the health monitor
+ returned: success when not state == absent
+ type: str
+http:
+ description: Advanced options for health monitors with type "http" or "https"
+ returned: success when not state == absent
+ type: dict
+ sample: [ {
+ "expected_codes": [
+ "200"
+ ],
+ "method": "GET",
+ "url_path": "/",
+ "version": "1.0",
+ "host": null
+ } ]
+tags:
+ description: Tags assosiated with the load balancer
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+ALLOWED_STATES = ('present',
+ 'absent',
+ )
+ALLOWED_HTTP_POST_PARAMS = ('expected_codes',
+ 'host',
+ 'method',
+ 'url_path')
+
+
+class AnsibleCloudscaleLoadBalancerHealthMonitor(AnsibleCloudscaleBase):
+
+ def __init__(self, module):
+ super(AnsibleCloudscaleLoadBalancerHealthMonitor, self).__init__(
+ module,
+ resource_name='load-balancers/health-monitors',
+ resource_key_name='pool',
+ resource_create_param_keys=[
+ 'pool',
+ 'timeout_s',
+ 'up_threshold',
+ 'down_threshold',
+ 'type',
+ 'http',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'delay_s',
+ 'timeout_s',
+ 'up_threshold',
+ 'down_threshold',
+ 'expected_codes',
+ 'http',
+ 'tags',
+ ],
+ )
+
+ def query(self):
+ # Initialize
+ self._resource_data = self.init_resource()
+
+ resource_key_pool = 'pool'
+ uuid = self._module.params[self.resource_key_uuid]
+ pool = self._module.params[resource_key_pool]
+ matching = []
+
+ # Either search by given health monitor's UUID or
+ # search the health monitor by its acossiated pool UUID (1:1)
+ if uuid is not None:
+ super().query()
+ else:
+ pool = self._module.params[resource_key_pool]
+ if pool is not None:
+
+ resources = self._get('%s' % (self.resource_name))
+
+ if resources:
+ for health_monitor in resources:
+ if health_monitor[resource_key_pool]['uuid'] == pool:
+ matching.append(health_monitor)
+
+ # Fail on more than one resource with identical name
+ if len(matching) > 1:
+ self._module.fail_json(
+ msg="More than one %s resource for pool '%s' exists." % (
+ self.resource_name,
+ resource_key_pool
+ )
+ )
+ elif len(matching) == 1:
+ self._resource_data = matching[0]
+ self._resource_data['state'] = "present"
+
+ return self.pre_transform(self._resource_data)
+
+ def update(self, resource):
+ updated = False
+ for param in self.resource_update_param_keys:
+ if param == 'http' and self._module.params.get('http') is not None:
+ for subparam in ALLOWED_HTTP_POST_PARAMS:
+ updated = self._http_param_updated(subparam, resource) or updated
+ else:
+ updated = self._param_updated(param, resource) or updated
+
+ # Refresh if resource was updated in live mode
+ if updated and not self._module.check_mode:
+ resource = self.query()
+ return resource
+
+ def _http_param_updated(self, key, resource):
+ param_http = self._module.params.get('http')
+ param = param_http[key]
+
+ if param is None:
+ return False
+
+ if not resource or key not in resource['http']:
+ return False
+
+ is_different = self.find_http_difference(key, resource, param)
+
+ if is_different:
+ self._result['changed'] = True
+
+ patch_data = {
+ 'http': {
+ key: param
+ }
+ }
+
+ before_data = {
+ 'http': {
+ key: resource['http'][key]
+ }
+ }
+
+ self._result['diff']['before'].update(before_data)
+ self._result['diff']['after'].update(patch_data)
+
+ if not self._module.check_mode:
+ href = resource.get('href')
+ if not href:
+ self._module.fail_json(msg='Unable to update %s, no href found.' % key)
+
+ self._patch(href, patch_data)
+ return True
+ return False
+
+ def find_http_difference(self, key, resource, param):
+ is_different = False
+
+ if param != resource['http'][key]:
+ is_different = True
+
+ return is_different
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ uuid=dict(type='str'),
+ pool=dict(type='str'),
+ delay_s=dict(type='int'),
+ timeout_s=dict(type='int'),
+ up_threshold=dict(type='int'),
+ down_threshold=dict(type='int'),
+ type=dict(type='str'),
+ http=dict(
+ type='dict',
+ options=dict(
+ expected_codes=dict(type='list', elements='str'),
+ method=dict(type='str'),
+ url_path=dict(type='str'),
+ version=dict(type='str'),
+ host=dict(type='str'),
+ )
+ ),
+ tags=dict(type='dict'),
+ state=dict(default='present', choices=ALLOWED_STATES),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(),
+ required_one_of=(),
+ required_if=(),
+ supports_check_mode=True,
+ )
+
+ cloudscale_load_balancer_health_monitor = AnsibleCloudscaleLoadBalancerHealthMonitor(module)
+ cloudscale_load_balancer_health_monitor.query_constraint_keys = []
+
+ if module.params['state'] == "absent":
+ result = cloudscale_load_balancer_health_monitor.absent()
+ else:
+ result = cloudscale_load_balancer_health_monitor.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_listener.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_listener.py
new file mode 100644
index 000000000..be91ac0b5
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_listener.py
@@ -0,0 +1,271 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2023, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# Copyright: (c) 2023, Kenneth Joss <kenneth.joss@cloudscale.ch>
+# 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: load_balancer_listener
+short_description: Manages load balancer listeners on the cloudscale.ch IaaS service
+description:
+ - Get, create, update, delete listeners on the cloudscale.ch IaaS service.
+notes:
+ - If I(uuid) option is provided, it takes precedence over I(name) for load balancer listener selection. This allows to update the listener's name.
+ - If no I(uuid) option is provided, I(name) is used for load balancer listener selection.
+ - If more than one load balancer with this name exists, execution is aborted.
+author:
+ - Gaudenz Steinlin (@gaudenz)
+ - Kenneth Joss (@k-304)
+version_added: "2.3.0"
+options:
+ state:
+ description:
+ - State of the load balancer listener.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ name:
+ description:
+ - Name of the load balancer listener.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ uuid:
+ description:
+ - UUID of the load balancer listener.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ pool:
+ description:
+ - The pool of the listener.
+ type: str
+ protocol:
+ description:
+ - The protocol used for receiving traffic.
+ type: str
+ protocol_port:
+ description:
+ - The port on which traffic is received.
+ type: int
+ allowed_cidrs:
+ description:
+ - Restrict the allowed source IPs for this listener.
+ - Empty means that any source IP is allowed. If the list is non-empty, traffic from source IPs not included is denied.
+ type: list
+ elements: str
+ timeout_client_data_ms:
+ description:
+ - Client inactivity timeout in milliseconds.
+ type: int
+ timeout_member_connect_ms:
+ description:
+ - Pool member connection timeout in milliseconds.
+ type: int
+ timeout_member_data_ms:
+ description:
+ - Pool member inactivity timeout in milliseconds.
+ type: int
+ tags:
+ description:
+ - Tags assosiated with the load balancer. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Create a load balancer listener for a pool using registered variables
+- name: Create a load balancer pool
+ cloudscale_ch.cloud.load_balancer_pool:
+ name: 'swimming-pool'
+ load_balancer: '3d41b118-f95c-4897-ad74-2260fea783fc'
+ algorithm: 'round_robin'
+ protocol: 'tcp'
+ api_token: xxxxxx
+ register: load_balancer_pool
+
+- name: Create a load balancer listener
+ cloudscale_ch.cloud.load_balancer_listener:
+ name: 'swimming-pool-listener'
+ pool: '{{ load_balancer_pool.uuid }}'
+ protocol: 'tcp'
+ protocol_port: 8080
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+
+# Create a load balancer listener for a pool with restriction
+- name: Create a load balancer listener with ip restriction
+ cloudscale_ch.cloud.load_balancer_listener:
+ name: 'new-listener2'
+ pool: '618a6cc8-d757-4fab-aa10-d49dc47e667b'
+ protocol: 'tcp'
+ protocol_port: 8080
+ allowed_cidrs:
+ - '192.168.3.0/24'
+ - '2001:db8:85a3:8d3::/64'
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+
+# Get load balancer listener facts by name
+- name: Get facts of a load balancer listener by name
+ cloudscale_ch.cloud.load_balancer_listener:
+ name: '{{ cloudscale_resource_prefix }}-test'
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+href:
+ description: API URL to get details about this load balancer lintener
+ returned: success when not state == absent
+ type: str
+ sample: https://api.cloudscale.ch/v1/load-balancers/listeners/9fa91f17-fdb4-431f-8a59-78473f64e661
+uuid:
+ description: The unique identifier for this load balancer listener
+ returned: success
+ type: str
+ sample: 9fa91f17-fdb4-431f-8a59-78473f64e661
+name:
+ description: The display name of the load balancer listener
+ returned: success
+ type: str
+ sample: new-listener
+created_at:
+ description: The creation date and time of the load balancer listener
+ returned: success when not state == absent
+ type: str
+ sample: "2023-02-07T15:32:02.308041Z"
+pool:
+ description: The pool of the load balancer listener
+ returned: success when not state == absent
+ type: complex
+ contains:
+ href:
+ description: API URL to get details about the pool.
+ returned: success
+ type: str
+ sample: https://api.cloudscale.ch/v1/load-balancers/pools/618a6cc8-d757-4fab-aa10-d49dc47e667b
+ uuid:
+ description: The unique identifier for the pool.
+ returned: success
+ type: str
+ sample: 618a6cc8-d757-4fab-aa10-d49dc47e667b
+ name:
+ description: The name of the pool.
+ returned: success
+ type: str
+ sample: new-listener
+protocol:
+ description: The protocol used for receiving traffic
+ returned: success when not state == absent
+ type: str
+ sample: tcp
+protocol_port:
+ description: The port on which traffic is received
+ returned: success when not state == absent
+ type: int
+ sample: 8080
+allowed_cidrs:
+ description: Restrict the allowed source IPs for this listener
+ returned: success when not state == absent
+ type: list
+ sample: ["192.168.3.0/24", "2001:db8:85a3:8d3::/64"]
+timeout_client_data_ms:
+ description: Client inactivity timeout in milliseconds
+ returned: success when not state == absent
+ type: int
+ sample: 50000
+timeout_member_connect_ms:
+ description: Pool member connection timeout in milliseconds
+ returned: success when not state == absent
+ type: int
+ sample: 50000
+timeout_member_data_ms:
+ description: Pool member inactivity timeout in milliseconds
+ returned: success when not state == absent
+ type: int
+ sample: 50000
+tags:
+ description: Tags assosiated with the load balancer listener
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+ALLOWED_STATES = ('present',
+ 'absent',
+ )
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str'),
+ uuid=dict(type='str'),
+ pool=dict(type='str'),
+ protocol=dict(type='str'),
+ protocol_port=dict(type='int'),
+ allowed_cidrs=dict(type='list', elements='str'),
+ timeout_client_data_ms=dict(type='int'),
+ timeout_member_connect_ms=dict(type='int'),
+ timeout_member_data_ms=dict(type='int'),
+ tags=dict(type='dict'),
+ state=dict(type='str', default='present', choices=ALLOWED_STATES),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(),
+ required_one_of=(('name', 'uuid'),),
+ required_if=(('state', 'present', ('name',),),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_load_balancer_listener = AnsibleCloudscaleBase(
+ module,
+ resource_name='load-balancers/listeners',
+ resource_create_param_keys=[
+ 'name',
+ 'pool',
+ 'protocol',
+ 'protocol_port',
+ 'allowed_cidrs',
+ 'timeout_client_data_ms',
+ 'timeout_member_connect_ms',
+ 'timeout_member_data_ms',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'allowed_cidrs',
+ 'timeout_client_data_ms',
+ 'timeout_member_connect_ms',
+ 'timeout_member_data_ms',
+ 'tags',
+ ],
+ )
+
+ if module.params['state'] == "absent":
+ result = cloudscale_load_balancer_listener.absent()
+ else:
+ result = cloudscale_load_balancer_listener.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool.py
new file mode 100644
index 000000000..6b794a3e3
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool.py
@@ -0,0 +1,217 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2023, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# Copyright: (c) 2023, Kenneth Joss <kenneth.joss@cloudscale.ch>
+# 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: load_balancer_pool
+short_description: Manages load balancer pools on the cloudscale.ch IaaS service
+description:
+ - Get, create, update, delete pools on the cloudscale.ch IaaS service.
+notes:
+ - If I(uuid) option is provided, it takes precedence over I(name) for pool selection. This allows to update the load balancer pool's name.
+ - If no I(uuid) option is provided, I(name) is used for pool selection. If more than one pool with this name exists, execution is aborted.
+author:
+ - Gaudenz Steinlin (@gaudenz)
+ - Kenneth Joss (@k-304)
+version_added: "2.3.0"
+options:
+ state:
+ description:
+ - State of the load balancer pool.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ name:
+ description:
+ - Name of the load balancer pool.
+ type: str
+ uuid:
+ description:
+ - UUID of the load balancer pool.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ load_balancer:
+ description:
+ - UUID of the load balancer for this pool.
+ type: str
+ algorithm:
+ description:
+ - The algorithm according to which the incoming traffic is distributed between the pool members.
+ - See the [API documentation](https://www.cloudscale.ch/en/api/v1#pool-algorithms) for supported distribution algorithms.
+ type: str
+ protocol:
+ description:
+ - The protocol used for traffic between the load balancer and the pool members.
+ - See the [API documentation](https://www.cloudscale.ch/en/api/v1#pool-protocols) for supported protocols.
+ type: str
+ tags:
+ description:
+ - Tags assosiated with the load balancer. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Create a pool for a load balancer using registered variables
+- name: Create a running load balancer
+ cloudscale_ch.cloud.load_balancer:
+ name: 'lb1'
+ flavor: 'lb-standard'
+ zone: 'lpg1'
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+ register: load_balancer
+
+- name: Create a load balancer pool
+ cloudscale_ch.cloud.load_balancer_pool:
+ name: 'swimming-pool'
+ load_balancer: '{{ load_balancer.uuid }}'
+ algorithm: 'round_robin'
+ protocol: 'tcp'
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+ register: load_balancer_pool
+
+# Create a load balancer pool with algorithm: round_robin and protocol: tcp
+- name: Create a load balancer pool
+ cloudscale_ch.cloud.load_balancer_pool:
+ name: 'cloudscale-loadbalancer-pool1'
+ load_balancer: '3766c579-3012-4a85-8192-2bbb4ef85b5f'
+ algorithm: 'round_robin'
+ protocol: 'tcp'
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+
+# Get load balancer pool facts by name
+- name: Get facts of a load balancer pool
+ cloudscale_ch.cloud.load_balancer_pool:
+ name: cloudscale-loadbalancer-pool1
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+href:
+ description: API URL to get details about this load balancer
+ returned: success when not state == absent
+ type: str
+ sample: https://api.cloudscale.ch/v1/load-balancers/pools/
+uuid:
+ description: The unique identifier for this load balancer pool
+ returned: success
+ type: str
+ sample: 3766c579-3012-4a85-8192-2bbb4ef85b5f
+name:
+ description: The display name of the load balancer pool
+ returned: success
+ type: str
+ sample: web-lb-pool1
+created_at:
+ description: The creation date and time of the load balancer pool
+ returned: success when not state == absent
+ type: str
+ sample: "2023-02-07T15:32:02.308041Z"
+load_balancer:
+ description: The load balancer this pool is connected to
+ returned: success when not state == absent
+ type: list
+ sample: {
+ "href": "https://api.cloudscale.ch/v1/load-balancers/15264769-ac69-4809-a8e4-4d73f8f92496",
+ "uuid": "15264769-ac69-4809-a8e4-4d73f8f92496",
+ "name": "web-lb"
+ }
+algorithm:
+ description: The algorithm according to which the incoming traffic is distributed between the pool members
+ returned: success
+ type: str
+ sample: round_robin
+protocol:
+ description: The protocol used for traffic between the load balancer and the pool members
+ returned: success
+ type: str
+ sample: tcp
+state:
+ description: The current state of the load balancer pool
+ returned: success
+ type: str
+ sample: present
+tags:
+ description: Tags assosiated with the load balancer
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+ALLOWED_STATES = ('present',
+ 'absent',
+ )
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(),
+ uuid=dict(),
+ load_balancer=dict(),
+ algorithm=dict(type='str'),
+ protocol=dict(type='str'),
+ tags=dict(type='dict'),
+ state=dict(default='present', choices=ALLOWED_STATES),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(),
+ required_one_of=(('name', 'uuid'),),
+ required_if=(('state', 'present', ('name',),),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_load_balancer_pool = AnsibleCloudscaleBase(
+ module,
+ resource_name='load-balancers/pools',
+ resource_create_param_keys=[
+ 'name',
+ 'load_balancer',
+ 'algorithm',
+ 'protocol',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'tags',
+ ],
+ )
+ cloudscale_load_balancer_pool.query_constraint_keys = []
+
+ if module.params['state'] == "absent":
+ result = cloudscale_load_balancer_pool.absent()
+ else:
+ result = cloudscale_load_balancer_pool.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool_member.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool_member.py
new file mode 100644
index 000000000..49a2124d9
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/load_balancer_pool_member.py
@@ -0,0 +1,319 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2023, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# Copyright: (c) 2023, Kenneth Joss <kenneth.joss@cloudscale.ch>
+# 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: load_balancer_pool_member
+short_description: Manages load balancer pool members on the cloudscale.ch IaaS service
+description:
+ - Get, create, update, delete pool members on the cloudscale.ch IaaS service.
+notes:
+ - If I(uuid) option is provided, it takes precedence over I(name) for pool member selection. This allows to update the member's name.
+ - If no I(uuid) option is provided, I(name) is used for pool member selection. If more than one load balancer with this name exists, execution is aborted.
+author:
+ - Gaudenz Steinlin (@gaudenz)
+ - Kenneth Joss (@k-304)
+version_added: "2.3.0"
+options:
+ state:
+ description:
+ - State of the load balancer pool member.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ name:
+ description:
+ - Name of the load balancer pool member.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ uuid:
+ description:
+ - UUID of the load balancer.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ load_balancer_pool:
+ description:
+ - UUID of the load balancer pool.
+ type: str
+ enabled:
+ description:
+ - Pool member will not receive traffic if false. Default is true.
+ default: true
+ type: bool
+ protocol_port:
+ description:
+ - The port to which actual traffic is sent.
+ type: int
+ monitor_port:
+ description:
+ - The port to which health monitor checks are sent.
+ - If not specified, protocol_port will be used. Default is null.
+ default: null
+ type: int
+ address:
+ description:
+ - The IP address to which traffic is sent.
+ type: str
+ subnet:
+ description:
+ - The subnet of the address must be specified here.
+ type: str
+ tags:
+ description:
+ - Tags assosiated with the load balancer. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Create a pool member for a load balancer pool using registered variables
+- name: Create a load balancer pool
+ cloudscale_ch.cloud.load_balancer_pool:
+ name: 'swimming-pool'
+ load_balancer: '514064c2-cfd4-4b0c-8a4b-c68c552ff84f'
+ algorithm: 'round_robin'
+ protocol: 'tcp'
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+ register: load_balancer_pool
+
+- name: Create a load balancer pool member
+ cloudscale_ch.cloud.load_balancer_pool_member:
+ name: 'my-shiny-swimming-pool-member'
+ load_balancer_pool: '{{ load_balancer_pool.uuid }}'
+ enabled: true
+ protocol_port: 8080
+ monitor_port: 8081
+ subnet: '70d282ab-2a01-4abb-ada5-34e56a5a7eee'
+ address: '172.16.0.100'
+ tags:
+ project: ansible-test
+ stage: production
+ sla: 24-7
+ api_token: xxxxxx
+
+# Get load balancer pool member facts by name
+- name: Get facts of a load balancer pool member by name
+ cloudscale_ch.cloud.load_balancer_pool_member:
+ name: 'my-shiny-swimming-pool-member'
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+href:
+ description: API URL to get details about this load balancer
+ returned: success when not state == absent
+ type: str
+ sample: https://api.cloudscale.ch/v1/load-balancers/pools/20a7eb11-3e17-4177-b46d-36e13b101d1c/members/b9991773-857d-47f6-b20b-0a03709529a9
+uuid:
+ description: The unique identifier for this load balancer pool member
+ returned: success
+ type: str
+ sample: cfde831a-4e87-4a75-960f-89b0148aa2cc
+name:
+ description: The display name of the load balancer pool member
+ returned: success
+ type: str
+ sample: web-lb-pool
+enabled:
+ description: THe status of the load balancer pool member
+ returned: success
+ type: bool
+ sample: true
+created_at:
+ description: The creation date and time of the load balancer pool member
+ returned: success when not state == absent
+ type: str
+ sample: "2023-02-07T15:32:02.308041Z"
+pool:
+ description: The pool of the pool member
+ returned: success
+ type: dict
+ sample: {
+ "href": "https://api.cloudscale.ch/v1/load-balancers/pools/20a7eb11-3e17-4177-b46d-36e13b101d1c",
+ "uuid": "20a7eb11-3e17-4177-b46d-36e13b101d1c",
+ "name": "web-lb-pool"
+ }
+protocol_port:
+ description: The port to which actual traffic is sent
+ returned: success
+ type: int
+ sample: 8080
+monitor_port:
+ description: The port to which health monitor checks are sent
+ returned: success
+ type: int
+ sample: 8081
+address:
+ description: The IP address to which traffic is sent
+ returned: success
+ type: str
+ sample: 10.11.12.3
+subnet:
+ description: The subnet in a private network in which address is located
+ returned: success
+ type: dict
+ sample: {
+ "href": "https://api.cloudscale.ch/v1/subnets/70d282ab-2a01-4abb-ada5-34e56a5a7eee",
+ "uuid": "70d282ab-2a01-4abb-ada5-34e56a5a7eee",
+ "cidr": "10.11.12.0/24"
+ }
+monitor_status:
+ description: The status of the pool's health monitor check for this member
+ returned: success
+ type: str
+ sample: up
+tags:
+ description: Tags assosiated with the load balancer
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+ALLOWED_STATES = ('present',
+ 'absent',
+ )
+
+
+class AnsibleCloudscaleLoadBalancerPoolMember(AnsibleCloudscaleBase):
+
+ def __init__(self, module):
+ super(AnsibleCloudscaleLoadBalancerPoolMember, self).__init__(
+ module,
+ resource_name='load-balancers/pools/%s/members' % module.params['load_balancer_pool'],
+ resource_create_param_keys=[
+ 'name',
+ 'enabled',
+ 'protocol_port',
+ 'monitor_port',
+ 'address',
+ 'subnet',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'enabled',
+ 'tags',
+ ],
+ )
+
+ def query(self):
+ # Initialize
+ self._resource_data = self.init_resource()
+
+ # Query by UUID
+ uuid = self._module.params[self.resource_key_uuid]
+ if uuid is not None:
+
+ # network id case
+ if "/" in uuid:
+ uuid = uuid.split("/")[0]
+
+ resource = self._get('%s/%s' % (self.resource_name, uuid))
+ if resource:
+ self._resource_data = resource
+ self._resource_data['state'] = "present"
+
+ # Query by name
+ else:
+ name = self._module.params[self.resource_key_name]
+
+ # Resource has no name field, we use a defined tag as name
+ if self.use_tag_for_name:
+ resources = self._get('%s?tag:%s=%s' % (self.resource_name, self.resource_name_tag, name))
+ else:
+ resources = self._get('%s' % self.resource_name)
+
+ matching = []
+ if resources is None:
+ self._module.fail_json(
+ msg="The load balancer pool %s does not exist."
+ % (self.resource_name,)
+ )
+ for resource in resources:
+ if self.use_tag_for_name:
+ resource[self.resource_key_name] = resource['tags'].get(self.resource_name_tag)
+
+ # Skip resource if constraints is not given e.g. in case of floating_ip the ip_version differs
+ for constraint_key in self.query_constraint_keys:
+ if self._module.params[constraint_key] is not None:
+ if constraint_key == 'zone':
+ resource_value = resource['zone']['slug']
+ else:
+ resource_value = resource[constraint_key]
+
+ if resource_value != self._module.params[constraint_key]:
+ break
+ else:
+ if resource[self.resource_key_name] == name:
+ matching.append(resource)
+
+ # Fail on more than one resource with identical name
+ if len(matching) > 1:
+ self._module.fail_json(
+ msg="More than one %s resource with '%s' exists: %s. "
+ "Use the '%s' parameter to identify the resource." % (
+ self.resource_name,
+ self.resource_key_name,
+ name,
+ self.resource_key_uuid
+ )
+ )
+ elif len(matching) == 1:
+ self._resource_data = matching[0]
+ self._resource_data['state'] = "present"
+
+ return self.pre_transform(self._resource_data)
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(),
+ uuid=dict(),
+ load_balancer_pool=dict(type='str'),
+ enabled=dict(type='bool', default=True),
+ protocol_port=dict(type='int'),
+ monitor_port=dict(type='int'),
+ subnet=dict(type='str'),
+ address=dict(type='str'),
+ tags=dict(type='dict'),
+ state=dict(default='present', choices=ALLOWED_STATES),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(),
+ required_one_of=(('name', 'uuid'),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_load_balancer_pool_member = AnsibleCloudscaleLoadBalancerPoolMember(module)
+ cloudscale_load_balancer_pool_member.query_constraint_keys = []
+
+ if module.params['state'] == "absent":
+ result = cloudscale_load_balancer_pool_member.absent()
+ else:
+ result = cloudscale_load_balancer_pool_member.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/network.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/network.py
new file mode 100644
index 000000000..7b1da5b2e
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/network.py
@@ -0,0 +1,197 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, René Moser <mail@renemoser.net>
+# 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: network
+short_description: Manages networks on the cloudscale.ch IaaS service
+description:
+ - Create, update and remove networks.
+author:
+ - René Moser (@resmo)
+version_added: "1.2.0"
+options:
+ name:
+ description:
+ - Name of the network.
+ - Either I(name) or I(uuid) is required.
+ type: str
+ uuid:
+ description:
+ - UUID of the network.
+ - Either I(name) or I(uuid) is required.
+ type: str
+ mtu:
+ description:
+ - The MTU of the network.
+ default: 9000
+ type: int
+ auto_create_ipv4_subnet:
+ description:
+ - Whether to automatically create an IPv4 subnet in the network or not.
+ default: true
+ type: bool
+ zone:
+ description:
+ - Zone slug of the network (e.g. C(lpg1) or C(rma1)).
+ type: str
+ state:
+ description:
+ - State of the network.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ tags:
+ description:
+ - Tags assosiated with the networks. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+---
+- name: Ensure network exists
+ cloudscale_ch.cloud.network:
+ name: my network
+ api_token: xxxxxx
+
+- name: Ensure network in a specific zone
+ cloudscale_ch.cloud.network:
+ name: my network
+ zone: lpg1
+ api_token: xxxxxx
+
+- name: Ensure a network is absent
+ cloudscale_ch.cloud.network:
+ name: my network
+ state: absent
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+---
+href:
+ description: API URL to get details about this network.
+ returned: success
+ type: str
+ sample: https://api.cloudscale.ch/v1/networks/cfde831a-4e87-4a75-960f-89b0148aa2cc
+uuid:
+ description: The unique identifier for the network.
+ returned: success
+ type: str
+ sample: cfde831a-4e87-4a75-960f-89b0148aa2cc
+name:
+ description: The name of the network.
+ returned: success
+ type: str
+ sample: my network
+created_at:
+ description: The creation date and time of the network.
+ returned: success
+ type: str
+ sample: "2019-05-29T13:18:42.511407Z"
+subnets:
+ description: A list of subnets objects of the network.
+ returned: success
+ type: complex
+ contains:
+ href:
+ description: API URL to get details about the subnet.
+ returned: success
+ type: str
+ sample: https://api.cloudscale.ch/v1/subnets/33333333-1864-4608-853a-0771b6885a3
+ uuid:
+ description: The unique identifier for the subnet.
+ returned: success
+ type: str
+ sample: 33333333-1864-4608-853a-0771b6885a3
+ cidr:
+ description: The CIDR of the subnet.
+ returned: success
+ type: str
+ sample: 172.16.0.0/24
+mtu:
+ description: The MTU of the network.
+ returned: success
+ type: int
+ sample: 9000
+zone:
+ description: The zone of the network.
+ returned: success
+ type: dict
+ sample: { 'slug': 'rma1' }
+state:
+ description: State of the network.
+ returned: success
+ type: str
+ sample: present
+tags:
+ description: Tags assosiated with the network.
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str'),
+ uuid=dict(type='str'),
+ mtu=dict(type='int', default=9000),
+ auto_create_ipv4_subnet=dict(type='bool', default=True),
+ zone=dict(type='str'),
+ tags=dict(type='dict'),
+ state=dict(default='present', choices=['absent', 'present']),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(('name', 'uuid'),),
+ required_if=(('state', 'present', ('name',),),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_network = AnsibleCloudscaleBase(
+ module,
+ resource_name='networks',
+ resource_create_param_keys=[
+ 'name',
+ 'mtu',
+ 'auto_create_ipv4_subnet',
+ 'zone',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'mtu',
+ 'tags',
+ ],
+ )
+
+ cloudscale_network.query_constraint_keys = [
+ 'zone',
+ ]
+
+ if module.params['state'] == 'absent':
+ result = cloudscale_network.absent()
+ else:
+ result = cloudscale_network.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/objects_user.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/objects_user.py
new file mode 100644
index 000000000..d4d117817
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/objects_user.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, René Moser <mail@renemoser.net>
+# 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 = r'''
+---
+module: objects_user
+short_description: Manages objects users on the cloudscale.ch IaaS service
+description:
+ - Create, update and remove objects users cloudscale.ch IaaS service.
+author:
+ - Rene Moser (@resmo)
+version_added: 1.1.0
+options:
+ display_name:
+ description:
+ - Display name of the objects user.
+ - Either I(display_name) or I(id) is required.
+ type: str
+ aliases:
+ - name
+ id:
+ description:
+ - Name of the objects user.
+ - Either I(display_name) or I(id) is required.
+ type: str
+ tags:
+ description:
+ - Tags associated with the objects user. Set this to C({}) to clear any tags.
+ type: dict
+ state:
+ description:
+ - State of the objects user.
+ default: present
+ choices: [ present, absent ]
+ type: str
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = r'''
+- name: Create an objects user
+ cloudscale_ch.cloud.objects_user:
+ display_name: alan
+ tags:
+ project: luna
+ api_token: xxxxxx
+ register: object_user
+
+- name: print keys
+ debug:
+ var: object_user.keys
+
+- name: Update an objects user
+ cloudscale_ch.cloud.objects_user:
+ display_name: alan
+ tags:
+ project: gemini
+ api_token: xxxxxx
+
+- name: Remove an objects user
+ cloudscale_ch.cloud.objects_user:
+ display_name: alan
+ state: absent
+ api_token: xxxxxx
+'''
+
+RETURN = r'''
+href:
+ description: The API URL to get details about this resource.
+ returned: success when state == present
+ type: str
+ sample: https://api.cloudscale.ch/v1/objects-users/6fe39134bf4178747eebc429f82cfafdd08891d4279d0d899bc4012db1db6a15
+display_name:
+ description: The display name of the objects user.
+ returned: success
+ type: str
+ sample: alan
+id:
+ description: The ID of the objects user.
+ returned: success
+ type: str
+ sample: 6fe39134bf4178747eebc429f82cfafdd08891d4279d0d899bc4012db1db6a15
+keys:
+ description: List of key objects.
+ returned: success
+ type: complex
+ contains:
+ access_key:
+ description: The access key.
+ returned: success
+ type: str
+ sample: 0ZTAIBKSGYBRHQ09G11W
+ secret_key:
+ description: The secret key.
+ returned: success
+ type: str
+ sample: bn2ufcwbIa0ARLc5CLRSlVaCfFxPHOpHmjKiH34T
+tags:
+ description: Tags assosiated with the objects user.
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+state:
+ description: The current status of the objects user.
+ returned: success
+ type: str
+ sample: present
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ..module_utils.api import AnsibleCloudscaleBase, cloudscale_argument_spec
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ display_name=dict(type='str', aliases=['name']),
+ id=dict(type='str'),
+ tags=dict(type='dict'),
+ state=dict(type='str', default='present', choices=('present', 'absent')),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(('display_name', 'id'),),
+ required_if=(('state', 'present', ('display_name',),),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_objects_user = AnsibleCloudscaleBase(
+ module,
+ resource_name='objects-users',
+ resource_key_uuid='id',
+ resource_key_name='display_name',
+ resource_create_param_keys=[
+ 'display_name',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'display_name',
+ 'tags',
+ ],
+ )
+
+ if module.params['state'] == "absent":
+ result = cloudscale_objects_user.absent()
+ else:
+ result = cloudscale_objects_user.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/server.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/server.py
new file mode 100644
index 000000000..d912750c1
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/server.py
@@ -0,0 +1,737 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2017, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# Copyright: (c) 2019, René Moser <mail@renemoser.net>
+# 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: server
+short_description: Manages servers on the cloudscale.ch IaaS service
+description:
+ - Create, update, start, stop and delete servers on the cloudscale.ch IaaS service.
+notes:
+ - If I(uuid) option is provided, it takes precedence over I(name) for server selection. This allows to update the server's name.
+ - If no I(uuid) option is provided, I(name) is used for server selection. If more than one server with this name exists, execution is aborted.
+ - Only the I(name) and I(flavor) are evaluated for the update.
+ - The option I(force=true) must be given to allow the reboot of existing running servers for applying the changes.
+author:
+ - Gaudenz Steinlin (@gaudenz)
+ - René Moser (@resmo)
+ - Denis Krienbühl (@href)
+version_added: "1.0.0"
+options:
+ state:
+ description:
+ - State of the server.
+ choices: [ running, stopped, absent ]
+ default: running
+ type: str
+ name:
+ description:
+ - Name of the Server.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ uuid:
+ description:
+ - UUID of the server.
+ - Either I(name) or I(uuid) are required.
+ type: str
+ flavor:
+ description:
+ - Flavor of the server.
+ type: str
+ image:
+ description:
+ - Image used to create the server.
+ type: str
+ zone:
+ description:
+ - Zone in which the server resides (e.g. C(lpg1) or C(rma1)).
+ type: str
+ volume_size_gb:
+ description:
+ - Size of the root volume in GB.
+ default: 10
+ type: int
+ bulk_volume_size_gb:
+ description:
+ - Size of the bulk storage volume in GB.
+ - No bulk storage volume if not set.
+ type: int
+ ssh_keys:
+ description:
+ - List of SSH public keys.
+ - Use the full content of your .pub file here.
+ type: list
+ elements: str
+ password:
+ description:
+ - Password for the server.
+ type: str
+ use_public_network:
+ description:
+ - Attach a public network interface to the server.
+ type: bool
+ use_private_network:
+ description:
+ - Attach a private network interface to the server.
+ type: bool
+ use_ipv6:
+ description:
+ - Enable IPv6 on the public network interface.
+ default: true
+ type: bool
+ interfaces:
+ description:
+ - List of network interface objects specifying the interfaces to be attached to the server.
+ See U(https://www.cloudscale.ch/en/api/v1/#interfaces-attribute-specification) for more details.
+ type: list
+ elements: dict
+ version_added: 1.4.0
+ suboptions:
+ network:
+ description:
+ - Create a network interface on the network identified by UUID.
+ Use 'public' instead of an UUID to attach a public network interface.
+ Can be omitted if a subnet is provided under addresses.
+ type: str
+ addresses:
+ description:
+ - Attach a private network interface and configure a subnet and/or an IP address.
+ type: list
+ elements: dict
+ suboptions:
+ subnet:
+ description:
+ - UUID of the subnet from which an address will be assigned.
+ type: str
+ address:
+ description:
+ - The static IP address of the interface. Use '[]' to avoid assigning an IP address via DHCP.
+ type: str
+ server_groups:
+ description:
+ - List of UUID or names of server groups.
+ type: list
+ elements: str
+ user_data:
+ description:
+ - Cloud-init configuration (cloud-config) data to use for the server.
+ type: str
+ force:
+ description:
+ - Allow to stop the running server for updating if necessary.
+ default: false
+ type: bool
+ tags:
+ description:
+ - Tags assosiated with the servers. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Create and start a server with an existing server group (shiny-group)
+- name: Start cloudscale.ch server
+ cloudscale_ch.cloud.server:
+ name: my-shiny-cloudscale-server
+ image: debian-10
+ flavor: flex-4-4
+ ssh_keys:
+ - ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
+ server_groups: shiny-group
+ zone: lpg1
+ use_private_network: true
+ bulk_volume_size_gb: 100
+ api_token: xxxxxx
+
+# Start another server in anti-affinity (server group shiny-group)
+- name: Start second cloudscale.ch server
+ cloudscale_ch.cloud.server:
+ name: my-other-shiny-server
+ image: ubuntu-16.04
+ flavor: flex-8-2
+ ssh_keys:
+ - ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
+ server_groups: shiny-group
+ zone: lpg1
+ api_token: xxxxxx
+
+# Force to update the flavor of a running server
+- name: Start cloudscale.ch server
+ cloudscale_ch.cloud.server:
+ name: my-shiny-cloudscale-server
+ image: debian-10
+ flavor: flex-8-2
+ force: true
+ ssh_keys:
+ - ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
+ use_private_network: true
+ bulk_volume_size_gb: 100
+ api_token: xxxxxx
+ register: server1
+
+# Stop the first server
+- name: Stop my first server
+ cloudscale_ch.cloud.server:
+ uuid: '{{ server1.uuid }}'
+ state: stopped
+ api_token: xxxxxx
+
+# Delete my second server
+- name: Delete my second server
+ cloudscale_ch.cloud.server:
+ name: my-other-shiny-server
+ state: absent
+ api_token: xxxxxx
+
+# Start a server and wait for the SSH host keys to be generated
+- name: Start server and wait for SSH host keys
+ cloudscale_ch.cloud.server:
+ name: my-cloudscale-server-with-ssh-key
+ image: debian-10
+ flavor: flex-4-2
+ ssh_keys:
+ - ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
+ api_token: xxxxxx
+ register: server
+ until: server is not failed
+ retries: 5
+ delay: 2
+
+# Start a server with two network interfaces:
+#
+# A public interface with IPv4/IPv6
+# A private interface on a specific private network with an IPv4 address
+
+- name: Start a server with a public and private network interface
+ cloudscale_ch.cloud.server:
+ name: my-cloudscale-server-with-two-network-interfaces
+ image: debian-10
+ flavor: flex-4-2
+ ssh_keys:
+ - ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
+ api_token: xxxxxx
+ interfaces:
+ - network: 'public'
+ - addresses:
+ - subnet: UUID_of_private_subnet
+
+# Start a server with a specific IPv4 address from subnet range
+- name: Start a server with a specific IPv4 address from subnet range
+ cloudscale_ch.cloud.server:
+ name: my-cloudscale-server-with-specific-address
+ image: debian-10
+ flavor: flex-4-2
+ ssh_keys:
+ - ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
+ api_token: xxxxxx
+ interfaces:
+ - addresses:
+ - subnet: UUID_of_private_subnet
+ address: 'A.B.C.D'
+
+# Start a server with two network interfaces:
+#
+# A public interface with IPv4/IPv6
+# A private interface on a specific private network with no IPv4 address
+
+- name: Start a server with a private network interface and no IP address
+ cloudscale_ch.cloud.server:
+ name: my-cloudscale-server-with-specific-address
+ image: debian-10
+ flavor: flex-4-2
+ ssh_keys:
+ - ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale
+ api_token: xxxxxx
+ interfaces:
+ - network: 'public'
+ - network: UUID_of_private_network
+ addresses: []
+'''
+
+RETURN = '''
+href:
+ description: API URL to get details about this server
+ returned: success when not state == absent
+ type: str
+ sample: https://api.cloudscale.ch/v1/servers/cfde831a-4e87-4a75-960f-89b0148aa2cc
+uuid:
+ description: The unique identifier for this server
+ returned: success
+ type: str
+ sample: cfde831a-4e87-4a75-960f-89b0148aa2cc
+name:
+ description: The display name of the server
+ returned: success
+ type: str
+ sample: its-a-me-mario.cloudscale.ch
+state:
+ description: The current status of the server
+ returned: success
+ type: str
+ sample: running
+flavor:
+ description: The flavor that has been used for this server
+ returned: success when not state == absent
+ type: dict
+ sample: { "slug": "flex-4-2", "name": "Flex-4-2", "vcpu_count": 2, "memory_gb": 4 }
+image:
+ description: The image used for booting this server
+ returned: success when not state == absent
+ type: dict
+ sample: { "default_username": "ubuntu", "name": "Ubuntu 18.04 LTS", "operating_system": "Ubuntu", "slug": "ubuntu-18.04" }
+zone:
+ description: The zone used for booting this server
+ returned: success when not state == absent
+ type: dict
+ sample: { 'slug': 'lpg1' }
+volumes:
+ description: List of volumes attached to the server
+ returned: success when not state == absent
+ type: list
+ sample: [ {"type": "ssd", "device": "/dev/vda", "size_gb": "50"} ]
+interfaces:
+ description: List of network ports attached to the server
+ returned: success when not state == absent
+ type: list
+ sample: [ { "type": "public", "addresses": [ ... ] } ]
+ssh_fingerprints:
+ description: A list of SSH host key fingerprints. Will be null until the host keys could be retrieved from the server.
+ returned: success when not state == absent
+ type: list
+ sample: ["ecdsa-sha2-nistp256 SHA256:XXXX", ... ]
+ssh_host_keys:
+ description: A list of SSH host keys. Will be null until the host keys could be retrieved from the server.
+ returned: success when not state == absent
+ type: list
+ sample: ["ecdsa-sha2-nistp256 XXXXX", ... ]
+server_groups:
+ description: List of server groups
+ returned: success when not state == absent
+ type: list
+ sample: [ {"href": "https://api.cloudscale.ch/v1/server-groups/...", "uuid": "...", "name": "db-group"} ]
+tags:
+ description: Tags assosiated with the server.
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from datetime import datetime, timedelta
+from time import sleep
+from copy import deepcopy
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+ALLOWED_STATES = ('running',
+ 'stopped',
+ 'absent',
+ )
+
+
+class AnsibleCloudscaleServer(AnsibleCloudscaleBase):
+
+ def __init__(self, module):
+ super(AnsibleCloudscaleServer, self).__init__(module)
+
+ # Initialize server dictionary
+ self._info = {}
+
+ def _init_server_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 _get_server_info(self, refresh=False):
+ if self._info and not refresh:
+ return self._info
+
+ self._info = self._init_server_container()
+
+ uuid = self._info.get('uuid')
+ if uuid is not None:
+ server_info = self._get('servers/%s' % uuid)
+ if server_info:
+ self._info = self._transform_state(server_info)
+
+ else:
+ name = self._info.get('name')
+ if name is not None:
+ servers = self._get('servers') or []
+ matching_server = []
+ for server in servers:
+ if server['name'] == name:
+ matching_server.append(server)
+
+ if len(matching_server) == 1:
+ self._info = self._transform_state(matching_server[0])
+ elif len(matching_server) > 1:
+ self._module.fail_json(msg="More than one server with name '%s' exists. "
+ "Use the 'uuid' parameter to identify the server." % name)
+
+ return self._info
+
+ @staticmethod
+ def _transform_state(server):
+ if 'status' in server:
+ server['state'] = server['status']
+ del server['status']
+ else:
+ server['state'] = 'absent'
+ return server
+
+ def _wait_for_state(self, states):
+ start = datetime.now()
+ timeout = self._module.params['api_timeout'] * 2
+ while datetime.now() - start < timedelta(seconds=timeout):
+ server_info = self._get_server_info(refresh=True)
+ if server_info.get('state') in states:
+ return server_info
+ sleep(1)
+
+ # Timeout succeeded
+ if server_info.get('name') is not None:
+ msg = "Timeout while waiting for a state change on server %s to states %s. " \
+ "Current state is %s." % (server_info.get('name'), states, server_info.get('state'))
+ else:
+ name_uuid = self._module.params.get('name') or self._module.params.get('uuid')
+ msg = 'Timeout while waiting to find the server %s' % name_uuid
+
+ self._module.fail_json(msg=msg)
+
+ def _start_stop_server(self, server_info, target_state="running", ignore_diff=False):
+ actions = {
+ 'stopped': 'stop',
+ 'running': 'start',
+ }
+
+ server_state = server_info.get('state')
+ if server_state != target_state:
+ self._result['changed'] = True
+
+ if not ignore_diff:
+ self._result['diff']['before'].update({
+ 'state': server_info.get('state'),
+ })
+ self._result['diff']['after'].update({
+ 'state': target_state,
+ })
+ if not self._module.check_mode:
+ self._post('servers/%s/%s' % (server_info['uuid'], actions[target_state]))
+ server_info = self._wait_for_state((target_state, ))
+
+ return server_info
+
+ def _update_param(self, param_key, server_info, requires_stop=False):
+ param_value = self._module.params.get(param_key)
+ if param_value is None:
+ return server_info
+
+ if 'slug' in server_info[param_key]:
+ server_v = server_info[param_key]['slug']
+ else:
+ server_v = server_info[param_key]
+
+ if server_v != param_value:
+ # Set the diff output
+ self._result['diff']['before'].update({param_key: server_v})
+ self._result['diff']['after'].update({param_key: param_value})
+
+ if server_info.get('state') == "running":
+ if requires_stop and not self._module.params.get('force'):
+ self._module.warn("Some changes won't be applied to running servers. "
+ "Use force=true to allow the server '%s' to be stopped/started." % server_info['name'])
+ return server_info
+
+ # Either the server is stopped or change is forced
+ self._result['changed'] = True
+ if not self._module.check_mode:
+
+ if requires_stop:
+ self._start_stop_server(server_info, target_state="stopped", ignore_diff=True)
+
+ patch_data = {
+ param_key: param_value,
+ }
+
+ # Response is 204: No Content
+ self._patch('servers/%s' % server_info['uuid'], patch_data)
+
+ # State changes to "changing" after update, waiting for stopped/running
+ server_info = self._wait_for_state(('stopped', 'running'))
+
+ return server_info
+
+ def _get_server_group_ids(self):
+ server_group_params = self._module.params['server_groups']
+ if not server_group_params:
+ return None
+
+ matching_group_names = []
+ results = []
+ server_groups = self._get('server-groups')
+ for server_group in server_groups:
+ if server_group['uuid'] in server_group_params:
+ results.append(server_group['uuid'])
+ server_group_params.remove(server_group['uuid'])
+
+ elif server_group['name'] in server_group_params:
+ results.append(server_group['uuid'])
+ server_group_params.remove(server_group['name'])
+ # Remember the names found
+ matching_group_names.append(server_group['name'])
+
+ # Names are not unique, verify if name already found in previous iterations
+ elif server_group['name'] in matching_group_names:
+ self._module.fail_json(msg="More than one server group with name exists: '%s'. "
+ "Use the 'uuid' parameter to identify the server group." % server_group['name'])
+
+ if server_group_params:
+ self._module.fail_json(msg="Server group name or UUID not found: %s" % ', '.join(server_group_params))
+
+ return results
+
+ def _create_server(self, server_info):
+ self._result['changed'] = True
+ self.normalize_interfaces_param()
+
+ data = deepcopy(self._module.params)
+ for i in ('uuid', 'state', 'force', 'api_timeout', 'api_token', 'api_url'):
+ del data[i]
+ data['server_groups'] = self._get_server_group_ids()
+
+ self._result['diff']['before'] = self._init_server_container()
+ self._result['diff']['after'] = deepcopy(data)
+ if not self._module.check_mode:
+ self._post('servers', data)
+ server_info = self._wait_for_state(('running', ))
+ return server_info
+
+ def _update_server(self, server_info):
+
+ previous_state = server_info.get('state')
+
+ # The API doesn't support to update server groups.
+ # Show a warning to the user if the desired state does not match.
+ desired_server_group_ids = self._get_server_group_ids()
+ if desired_server_group_ids is not None:
+ current_server_group_ids = [grp['uuid'] for grp in server_info['server_groups']]
+ if desired_server_group_ids != current_server_group_ids:
+ self._module.warn("Server groups can not be mutated, server needs redeployment to change groups.")
+
+ # Remove interface properties that were not filled out by the user
+ self.normalize_interfaces_param()
+
+ # Compare the interfaces as specified by the user, with the interfaces
+ # as received by the API. The structures are somewhat different, so
+ # they need to be evaluated in detail
+ wanted = self._module.params.get('interfaces')
+ actual = server_info.get('interfaces')
+
+ try:
+ update_interfaces = not self.has_wanted_interfaces(wanted, actual)
+ except KeyError as e:
+ self._module.fail_json(
+ msg="Error checking 'interfaces', missing key: %s" % e.args[0])
+
+ if update_interfaces:
+ server_info = self._update_param('interfaces', server_info)
+
+ if not self._result['changed']:
+ self._result['changed'] = server_info['interfaces'] != actual
+
+ server_info = self._update_param('flavor', server_info, requires_stop=True)
+ server_info = self._update_param('name', server_info)
+ server_info = self._update_param('tags', server_info)
+
+ if previous_state == "running":
+ server_info = self._start_stop_server(server_info, target_state="running", ignore_diff=True)
+
+ return server_info
+
+ def present_server(self):
+ server_info = self._get_server_info()
+
+ if server_info.get('state') != "absent":
+
+ # If target state is stopped, stop before an potential update and force would not be required
+ if self._module.params.get('state') == "stopped":
+ server_info = self._start_stop_server(server_info, target_state="stopped")
+
+ server_info = self._update_server(server_info)
+
+ if self._module.params.get('state') == "running":
+ server_info = self._start_stop_server(server_info, target_state="running")
+ else:
+ server_info = self._create_server(server_info)
+ server_info = self._start_stop_server(server_info, target_state=self._module.params.get('state'))
+
+ return server_info
+
+ def absent_server(self):
+ server_info = self._get_server_info()
+ if server_info.get('state') != "absent":
+ self._result['changed'] = True
+ self._result['diff']['before'] = deepcopy(server_info)
+ self._result['diff']['after'] = self._init_server_container()
+ if not self._module.check_mode:
+ self._delete('servers/%s' % server_info['uuid'])
+ server_info = self._wait_for_state(('absent', ))
+ return server_info
+
+ def has_wanted_interfaces(self, wanted, actual):
+ """ Compares the interfaces as specified by the user, with the
+ interfaces as reported by the server.
+
+ """
+
+ if len(wanted or ()) != len(actual or ()):
+ return False
+
+ def match_interface(spec):
+
+ # First, find the interface that belongs to the spec
+ for interface in actual:
+
+ # If we have a public network, only look for the right type
+ if spec.get('network') == 'public':
+ if interface['type'] == 'public':
+ break
+
+ # If we have a private network, check the network's UUID
+ if spec.get('network') is not None:
+ if interface['type'] == 'private':
+ if interface['network']['uuid'] == spec['network']:
+ break
+
+ # If we only have an addresses block, match all subnet UUIDs
+ wanted_subnet_ids = set(
+ a['subnet'] for a in (spec.get('addresses') or ()))
+
+ actual_subnet_ids = set(
+ a['subnet']['uuid'] for a in interface['addresses'])
+
+ if wanted_subnet_ids == actual_subnet_ids:
+ break
+ else:
+ return False # looped through everything without match
+
+ # Fail if any of the addresses don't match
+ for wanted_addr in (spec.get('addresses') or ()):
+
+ # Unspecified, skip
+ if 'address' not in wanted_addr:
+ continue
+
+ addresses = set(a['address'] for a in interface['addresses'])
+ if wanted_addr['address'] not in addresses:
+ return False
+
+ # If the wanted address is an empty list, but the actual list is
+ # not, the user wants to remove automatically set addresses
+ if spec.get('addresses') == [] and interface['addresses'] != []:
+ return False
+
+ if interface['addresses'] == [] and spec.get('addresses') != []:
+ return False
+
+ return interface
+
+ for spec in wanted:
+
+ # If there is any interface that does not match, clearly not all
+ # wanted interfaces are present
+ if not match_interface(spec):
+ return False
+
+ return True
+
+ def normalize_interfaces_param(self):
+ """ Goes through the interfaces parameter and gets it ready to be
+ sent to the API. """
+
+ for spec in (self._module.params.get('interfaces') or ()):
+ if spec['addresses'] is None:
+ del spec['addresses']
+ if spec['network'] is None:
+ del spec['network']
+
+ for address in (spec.get('addresses') or ()):
+ if address['address'] is None:
+ del address['address']
+ if address['subnet'] is None:
+ del address['subnet']
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ state=dict(default='running', choices=ALLOWED_STATES),
+ name=dict(),
+ uuid=dict(),
+ flavor=dict(),
+ image=dict(),
+ zone=dict(),
+ volume_size_gb=dict(type='int', default=10),
+ bulk_volume_size_gb=dict(type='int'),
+ ssh_keys=dict(type='list', elements='str', no_log=False),
+ password=dict(no_log=True),
+ use_public_network=dict(type='bool'),
+ use_private_network=dict(type='bool'),
+ use_ipv6=dict(type='bool', default=True),
+ interfaces=dict(
+ type='list',
+ elements='dict',
+ options=dict(
+ network=dict(type='str'),
+ addresses=dict(
+ type='list',
+ elements='dict',
+ options=dict(
+ address=dict(type='str'),
+ subnet=dict(type='str'),
+ ),
+ ),
+ ),
+ ),
+ server_groups=dict(type='list', elements='str'),
+ user_data=dict(),
+ force=dict(type='bool', default=False),
+ tags=dict(type='dict'),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(
+ ['interfaces', 'use_public_network'],
+ ['interfaces', 'use_private_network'],
+ ),
+ required_one_of=(('name', 'uuid'),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_server = AnsibleCloudscaleServer(module)
+ if module.params['state'] == "absent":
+ server = cloudscale_server.absent_server()
+ else:
+ server = cloudscale_server.present_server()
+
+ result = cloudscale_server.get_result(server)
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/server_group.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/server_group.py
new file mode 100644
index 000000000..f4dc9c319
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/server_group.py
@@ -0,0 +1,171 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019, René Moser <mail@renemoser.net>
+# 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: server_group
+short_description: Manages server groups on the cloudscale.ch IaaS service
+description:
+ - Create, update and remove server groups.
+author:
+ - René Moser (@resmo)
+ - Denis Krienbühl (@href)
+version_added: "1.0.0"
+options:
+ name:
+ description:
+ - Name of the server group.
+ - Either I(name) or I(uuid) is required. These options are mutually exclusive.
+ type: str
+ uuid:
+ description:
+ - UUID of the server group.
+ - Either I(name) or I(uuid) is required. These options are mutually exclusive.
+ type: str
+ type:
+ description:
+ - Type of the server group.
+ default: anti-affinity
+ type: str
+ zone:
+ description:
+ - Zone slug of the server group (e.g. C(lpg1) or C(rma1)).
+ type: str
+ state:
+ description:
+ - State of the server group.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ tags:
+ description:
+ - Tags assosiated with the server groups. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+---
+- name: Ensure server group exists
+ cloudscale_ch.cloud.server_group:
+ name: my-name
+ type: anti-affinity
+ api_token: xxxxxx
+
+- name: Ensure server group in a specific zone
+ cloudscale_ch.cloud.server_group:
+ name: my-rma-group
+ type: anti-affinity
+ zone: lpg1
+ api_token: xxxxxx
+
+- name: Ensure a server group is absent
+ cloudscale_ch.cloud.server_group:
+ name: my-name
+ state: absent
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+---
+href:
+ description: API URL to get details about this server group
+ returned: if available
+ type: str
+ sample: https://api.cloudscale.ch/v1/server-group/cfde831a-4e87-4a75-960f-89b0148aa2cc
+uuid:
+ description: The unique identifier for this server
+ returned: always
+ type: str
+ sample: cfde831a-4e87-4a75-960f-89b0148aa2cc
+name:
+ description: The display name of the server group
+ returned: always
+ type: str
+ sample: load balancers
+type:
+ description: The type the server group
+ returned: if available
+ type: str
+ sample: anti-affinity
+zone:
+ description: The zone of the server group
+ returned: success
+ type: dict
+ sample: { 'slug': 'rma1' }
+servers:
+ description: A list of servers that are part of the server group.
+ returned: if available
+ type: list
+ sample: []
+state:
+ description: State of the server group.
+ returned: always
+ type: str
+ sample: present
+tags:
+ description: Tags assosiated with the server group.
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ name=dict(type='str'),
+ uuid=dict(type='str'),
+ type=dict(type='str', default='anti-affinity'),
+ zone=dict(type='str'),
+ tags=dict(type='dict'),
+ state=dict(default='present', choices=['absent', 'present']),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(('name', 'uuid'),),
+ required_if=(('state', 'present', ('name',),),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_server_group = AnsibleCloudscaleBase(
+ module,
+ resource_name='server-groups',
+ resource_create_param_keys=[
+ 'name',
+ 'type',
+ 'zone',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'tags',
+ ],
+ )
+ cloudscale_server_group.query_constraint_keys = [
+ 'zone',
+ ]
+
+ if module.params['state'] == 'absent':
+ result = cloudscale_server_group.absent()
+ else:
+ result = cloudscale_server_group.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/subnet.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/subnet.py
new file mode 100644
index 000000000..b5e50306b
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/subnet.py
@@ -0,0 +1,322 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, René Moser <rene.moser@cloudscale.ch>
+# 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: subnet
+short_description: Manages subnets on the cloudscale.ch IaaS service
+description:
+ - Create, update and remove subnets.
+author:
+ - René Moser (@resmo)
+version_added: "1.3.0"
+options:
+ uuid:
+ description:
+ - UUID of the subnet.
+ type: str
+ cidr:
+ description:
+ - The cidr of the subnet.
+ - Required if I(state=present).
+ type: str
+ network:
+ description:
+ - The name of the network the subnet is related to.
+ - Required if I(state=present).
+ type: dict
+ suboptions:
+ uuid:
+ description:
+ - The uuid of the network.
+ type: str
+ name:
+ description:
+ - The uuid of the network.
+ type: str
+ zone:
+ description:
+ - The zone the network allocated in.
+ type: str
+ gateway_address:
+ description:
+ - The gateway address of the subnet. If not set, no gateway is used.
+ - Cannot be within the DHCP range, which is the lowest .101-.254 in the subnet.
+ type: str
+ dns_servers:
+ description:
+ - A list of DNS resolver IP addresses, that act as DNS servers.
+ - If not set, the cloudscale.ch default resolvers are used.
+ type: list
+ elements: str
+ reset:
+ description:
+ - Resets I(gateway_address) and I(dns_servers) to default values by the API.
+ - "Note: Idempotency is not given."
+ type: bool
+ default: false
+ state:
+ description:
+ - State of the subnet.
+ choices: [ present, absent ]
+ default: present
+ type: str
+ tags:
+ description:
+ - Tags associated with the subnet. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+---
+- name: Ensure subnet exists
+ cloudscale_ch.cloud.subnet:
+ cidr: 172.16.0.0/24
+ network:
+ uuid: 2db69ba3-1864-4608-853a-0771b6885a3a
+ api_token: xxxxxx
+
+- name: Ensure subnet exists
+ cloudscale_ch.cloud.subnet:
+ cidr: 192.168.1.0/24
+ gateway_address: 192.168.1.1
+ dns_servers:
+ - 192.168.1.10
+ - 192.168.1.11
+ network:
+ name: private
+ zone: lpg1
+ api_token: xxxxxx
+
+- name: Ensure a subnet is absent
+ cloudscale_ch.cloud.subnet:
+ cidr: 172.16.0.0/24
+ network:
+ name: private
+ zone: lpg1
+ state: absent
+ api_token: xxxxxx
+'''
+
+RETURN = '''
+---
+href:
+ description: API URL to get details about the subnet.
+ returned: success
+ type: str
+ sample: https://api.cloudscale.ch/v1/subnets/33333333-1864-4608-853a-0771b6885a3
+uuid:
+ description: The unique identifier for the subnet.
+ returned: success
+ type: str
+ sample: 33333333-1864-4608-853a-0771b6885a3
+cidr:
+ description: The CIDR of the subnet.
+ returned: success
+ type: str
+ sample: 172.16.0.0/24
+network:
+ description: The network object of the subnet.
+ returned: success
+ type: complex
+ contains:
+ href:
+ description: API URL to get details about the network.
+ returned: success
+ type: str
+ sample: https://api.cloudscale.ch/v1/networks/33333333-1864-4608-853a-0771b6885a3
+ uuid:
+ description: The unique identifier for the network.
+ returned: success
+ type: str
+ sample: 33333333-1864-4608-853a-0771b6885a3
+ name:
+ description: The name of the network.
+ returned: success
+ type: str
+ sample: my network
+ zone:
+ description: The zone the network is allocated in.
+ returned: success
+ type: dict
+ sample: { 'slug': 'rma1' }
+ version_added: 1.4.0
+gateway_address:
+ description: The gateway address of the subnet.
+ returned: success
+ type: str
+ sample: "192.168.42.1"
+dns_servers:
+ description: List of DNS resolver IP addresses.
+ returned: success
+ type: list
+ sample: ["9.9.9.9", "149.112.112.112"]
+state:
+ description: State of the subnet.
+ returned: success
+ type: str
+ sample: present
+tags:
+ description: Tags associated with the subnet.
+ returned: success
+ type: dict
+ sample: { 'project': 'my project' }
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+
+class AnsibleCloudscaleSubnet(AnsibleCloudscaleBase):
+
+ def __init__(self, module):
+ super(AnsibleCloudscaleSubnet, self).__init__(
+ module=module,
+ resource_name='subnets',
+ resource_key_name='cidr',
+ resource_create_param_keys=[
+ 'cidr',
+ 'gateway_address',
+ 'dns_servers',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'gateway_address',
+ 'dns_servers',
+ 'tags',
+ ],
+ )
+ self._network = None
+
+ def query_network(self, uuid=None):
+ if self._network is not None:
+ return self._network
+
+ net_param = self._module.params['network']
+ net_uuid = uuid or net_param['uuid']
+
+ if net_uuid is not None:
+ network = self._get('networks/%s' % net_uuid)
+ if not network:
+ self._module.fail_json(msg="Network with 'uuid' not found: %s" % net_uuid)
+
+ elif net_param['name'] is not None:
+ networks_found = []
+ networks = self._get('networks')
+ for network in networks or []:
+ # Skip networks in other zones
+ if net_param['zone'] is not None and network['zone']['slug'] != net_param['zone']:
+ continue
+
+ if network.get('name') == net_param['name']:
+ networks_found.append(network)
+
+ if not networks_found:
+ msg = "Network with 'name' not found: %s" % net_param['name']
+ self._module.fail_json(msg=msg)
+
+ elif len(networks_found) == 1:
+ network = networks_found[0]
+
+ # We might have found more than one network with identical name
+ else:
+ msg = ("Multiple networks with 'name' not found: %s."
+ "Add the 'zone' to distinguish or use 'uuid' argument to specify the network." % net_param['name'])
+ self._module.fail_json(msg=msg)
+
+ else:
+ self._module.fail_json(msg="Either Network UUID or name is required.")
+
+ # For consistency, take a minimal network stub, but also include zone
+ self._network = dict()
+ for k, v in network.items():
+ if k in ['name', 'uuid', 'href', 'zone']:
+ self._network[k] = v
+
+ return self._network
+
+ def create(self, resource):
+ resource['network'] = self.query_network()
+
+ data = {
+ 'network': resource['network']['uuid'],
+ }
+ return super(AnsibleCloudscaleSubnet, self).create(resource, data)
+
+ def update(self, resource):
+ # Resets to default values by the API
+ if self._module.params.get('reset'):
+ for key in ('dns_servers', 'gateway_address',):
+ # No need to reset if user set the param anyway.
+ if self._module.params.get(key) is None:
+ self._result['changed'] = True
+ patch_data = {
+ key: None
+ }
+ if not self._module.check_mode:
+ href = resource.get('href')
+ if not href:
+ self._module.fail_json(msg='Unable to update %s, no href found.' % key)
+ self._patch(href, patch_data, filter_none=False)
+
+ if not self._module.check_mode:
+ resource = self.query()
+
+ return super(AnsibleCloudscaleSubnet, self).update(resource)
+
+ def get_result(self, resource):
+ if resource and 'network' in resource:
+ resource['network'] = self.query_network(uuid=resource['network']['uuid'])
+ return super(AnsibleCloudscaleSubnet, self).get_result(resource)
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ uuid=dict(type='str'),
+ cidr=dict(type='str'),
+ network=dict(
+ type='dict',
+ options=dict(
+ uuid=dict(type='str'),
+ name=dict(type='str'),
+ zone=dict(type='str'),
+ ),
+ ),
+ gateway_address=dict(type='str'),
+ dns_servers=dict(type='list', elements='str', default=None),
+ tags=dict(type='dict'),
+ reset=dict(type='bool', default=False),
+ state=dict(default='present', choices=['absent', 'present']),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(('cidr', 'uuid',),),
+ required_together=(('cidr', 'network',),),
+ required_if=(('state', 'present', ('cidr', 'network',),),),
+ supports_check_mode=True,
+ )
+
+ cloudscale_subnet = AnsibleCloudscaleSubnet(module)
+
+ if module.params['state'] == 'absent':
+ result = cloudscale_subnet.absent()
+ else:
+ result = cloudscale_subnet.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/cloudscale_ch/cloud/plugins/modules/volume.py b/ansible_collections/cloudscale_ch/cloud/plugins/modules/volume.py
new file mode 100644
index 000000000..ecc6cfcc6
--- /dev/null
+++ b/ansible_collections/cloudscale_ch/cloud/plugins/modules/volume.py
@@ -0,0 +1,265 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2018, Gaudenz Steinlin <gaudenz.steinlin@cloudscale.ch>
+# Copyright (c) 2019, René Moser <mail@renemoser.net>
+# 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: 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)
+version_added: "1.0.0"
+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(lpg1) or C(rma1)). Cannot be
+ changed after creating the volume. Defaults to the project default zone.
+ type: str
+ servers:
+ 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.
+ - The aliases C(server_uuids) and C(server_uuid) are deprecated and will
+ be removed in version 3.0.0 of this collection.
+ aliases: [ server_uuids, server_uuid ]
+ type: list
+ elements: str
+ tags:
+ description:
+ - Tags associated with the volume. Set this to C({}) to clear any tags.
+ type: dict
+extends_documentation_fragment: cloudscale_ch.cloud.api_parameters
+'''
+
+EXAMPLES = '''
+# Create a new SSD volume
+- name: Create an SSD volume
+ cloudscale_ch.cloud.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_ch.cloud.volume:
+ uuid: "{{ my_ssd_volume.uuid }}"
+ servers:
+ - ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
+ api_token: xxxxxx
+
+# Create and attach a volume to a server
+- name: Create and attach volume to server
+ cloudscale_ch.cloud.volume:
+ name: my_ssd_volume
+ zone: 'lpg1'
+ size_gb: 50
+ servers:
+ - ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48
+ api_token: xxxxxx
+
+# Detach volume from server
+- name: Detach volume from server
+ cloudscale_ch.cloud.volume:
+ uuid: "{{ my_ssd_volume.uuid }}"
+ servers: []
+ api_token: xxxxxx
+
+# Delete a volume
+- name: Delete volume
+ cloudscale_ch.cloud.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'}
+server_uuids:
+ description: The UUIDs of the servers this volume is attached to. This return
+ value is deprecated and will disappear in the future when the field is
+ removed from the API.
+ returned: state == present
+ type: list
+ sample: ['47cec963-fcd2-482f-bdb6-24461b2d47b1']
+servers:
+ description: The list of servers this volume is attached to.
+ returned: state == present
+ type: list
+ sample: [
+ {
+ "href": "https://api.cloudscale.ch/v1/servers/47cec963-fcd2-482f-bdb6-24461b2d47b1",
+ "name": "my_server",
+ "uuid": "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 ..module_utils.api import (
+ AnsibleCloudscaleBase,
+ cloudscale_argument_spec,
+)
+
+
+class AnsibleCloudscaleVolume(AnsibleCloudscaleBase):
+
+ def create(self, resource):
+ # Fail when missing params for creation
+ self._module.fail_on_missing_params(['name', 'size_gb'])
+ return super(AnsibleCloudscaleVolume, self).create(resource)
+
+ def find_difference(self, key, resource, param):
+ is_different = False
+
+ if key != 'servers':
+ return super(AnsibleCloudscaleVolume, self).find_difference(key, resource, param)
+
+ server_has = resource[key]
+ server_wanted = param
+ if len(server_wanted) != len(server_has):
+ is_different = True
+ else:
+ for has in server_has:
+ if has["uuid"] not in server_wanted:
+ is_different = True
+
+ return is_different
+
+
+def main():
+ argument_spec = cloudscale_argument_spec()
+ argument_spec.update(dict(
+ state=dict(type='str', default='present', choices=('present', 'absent')),
+ name=dict(type='str'),
+ uuid=dict(type='str'),
+ zone=dict(type='str'),
+ size_gb=dict(type='int'),
+ type=dict(type='str', choices=('ssd', 'bulk')),
+ servers=dict(type='list', elements='str', aliases=['server_uuids', 'server_uuid']),
+ tags=dict(type='dict'),
+ ))
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(('name', 'uuid'),),
+ supports_check_mode=True,
+ )
+
+ # TODO remove in version 3.0.0
+ if module.params.get('server_uuid') or module.params.get('server_uuids'):
+ module.deprecate('The aliases "server_uuid" and "server_uuids" have '
+ 'been deprecated and will be removed, use "servers" '
+ 'instead.',
+ version='3.0.0', collection_name='cloudscale_ch.cloud')
+
+ cloudscale_volume = AnsibleCloudscaleVolume(
+ module,
+ resource_name='volumes',
+ resource_create_param_keys=[
+ 'name',
+ 'type',
+ 'zone',
+ 'size_gb',
+ 'servers',
+ 'tags',
+ ],
+ resource_update_param_keys=[
+ 'name',
+ 'size_gb',
+ 'servers',
+ 'tags',
+ ],
+ )
+
+ if module.params['state'] == 'absent':
+ result = cloudscale_volume.absent()
+ else:
+ result = cloudscale_volume.present()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()