diff --git a/lib/ansible/modules/cloud/cloudscale/cloudscale_server_group.py b/lib/ansible/modules/cloud/cloudscale/cloudscale_server_group.py new file mode 100644 index 00000000000..832561bba17 --- /dev/null +++ b/lib/ansible/modules/cloud/cloudscale/cloudscale_server_group.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: cloudscale_server_group +short_description: Manages server groups on the cloudscale.ch IaaS service +description: + - Create, update and remove server groups. +author: + - René Moser (@resmo) +version_added: '2.8' +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 + state: + description: + - State of the server group. + choices: [ present, absent ] + default: present + type: str +extends_documentation_fragment: cloudscale +''' + +EXAMPLES = ''' +--- +- name: Ensure server group exists + cloudscale_server_group: + name: my-name + type: anti-affinity + api_token: xxxxxx + +- name: Ensure a server group is absent + cloudscale_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 +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 +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.cloudscale import AnsibleCloudscaleBase, cloudscale_argument_spec + + +class AnsibleCloudscaleServerGroup(AnsibleCloudscaleBase): + + def __init__(self, module, namespace): + super(AnsibleCloudscaleServerGroup, self).__init__(module) + self._info = {} + + def _init_container(self): + return { + 'uuid': self._module.params.get('uuid') or self._info.get('uuid'), + 'name': self._module.params.get('name') or self._info.get('name'), + 'state': 'absent', + } + + def _create_server_group(self, server_group): + self._module.fail_on_missing_params(['name']) + self._result['changed'] = True + data = { + 'name': self._module.params.get('name'), + 'type': self._module.params.get('type'), + } + if not self._module.check_mode: + server_group = self._post('server-groups', data) + return server_group + + def _update_server_group(self, server_group): + data = { + 'name': self._module.params.get('name'), + } + if server_group['name'] != data['name']: + self._result['changed'] = True + + if not self._module.check_mode: + self._patch('server-groups/%s' % server_group['uuid'], data) + server_group = self.get_server_group() + return server_group + + def get_server_group(self): + self._info = self._init_container() + + uuid = self._info.get('uuid') + if uuid is not None: + server_group = self._get('server-groups/%s' % uuid) + if server_group: + self._info.update(server_group) + self._info.update(dict(state='present')) + + else: + name = self._info.get('name') + matching_server_groups = [] + for server_group in self._get('server-groups'): + if server_group['name'] == name: + matching_server_groups.append(server_group) + + if len(matching_server_groups) > 1: + self._module.fail_json(msg="More than one server group with name exists: '%s'. " + "Use the 'uuid' parameter to identify the server group." % name) + elif len(matching_server_groups) == 1: + self._info.update(matching_server_groups[0]) + self._info.update(dict(state='present')) + return self._info + + def present_group(self): + server_group = self.get_server_group() + if server_group.get('state') == 'absent': + server_group = self._create_server_group(server_group) + else: + server_group = self._update_server_group(server_group) + return server_group + + def absent_group(self): + server_group = self.get_server_group() + if server_group.get('state') != 'absent': + self._result['changed'] = True + if not self._module.check_mode: + self._delete('server-groups/%s' % server_group['uuid']) + return server_group + + +def main(): + argument_spec = cloudscale_argument_spec() + argument_spec.update(dict( + name=dict(), + uuid=dict(), + type=dict(default='anti-affinity'), + state=dict(default='present', choices=['absent', 'present']), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=(('name', 'uuid'),), + supports_check_mode=True, + ) + cloudscale_server_group = AnsibleCloudscaleServerGroup(module, 'cloudscale_server_group') + + if module.params['state'] == 'absent': + server_group = cloudscale_server_group.absent_group() + else: + server_group = cloudscale_server_group.present_group() + + result = cloudscale_server_group.get_result(server_group) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/cloudscale_common/tasks/cleanup_server_groups.yml b/test/integration/targets/cloudscale_common/tasks/cleanup_server_groups.yml new file mode 100644 index 00000000000..2bdb9e59398 --- /dev/null +++ b/test/integration/targets/cloudscale_common/tasks/cleanup_server_groups.yml @@ -0,0 +1,17 @@ +--- +- name: List all server groups + uri: + url: 'https://api.cloudscale.ch/v1/server-groups' + headers: + Authorization: 'Bearer {{ cloudscale_api_token }}' + status_code: 200 + register: server_group_list + +- name: Remove all server groups created by this test run + cloudscale_server_group: + uuid: '{{ item.uuid }}' + state: absent + when: cloudscale_resource_prefix in item.name + with_items: '{{ server_group_list.json }}' + loop_control: + label: '{{ item.name }} ({{ item.uuid }})' diff --git a/test/integration/targets/cloudscale_server_group/aliases b/test/integration/targets/cloudscale_server_group/aliases new file mode 100644 index 00000000000..c200a3d2c8a --- /dev/null +++ b/test/integration/targets/cloudscale_server_group/aliases @@ -0,0 +1,2 @@ +cloud/cloudscale +unsupported diff --git a/test/integration/targets/cloudscale_server_group/meta/main.yml b/test/integration/targets/cloudscale_server_group/meta/main.yml new file mode 100644 index 00000000000..8dd48f0337c --- /dev/null +++ b/test/integration/targets/cloudscale_server_group/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - cloudscale_common diff --git a/test/integration/targets/cloudscale_server_group/tasks/failures.yml b/test/integration/targets/cloudscale_server_group/tasks/failures.yml new file mode 100644 index 00000000000..9269a634580 --- /dev/null +++ b/test/integration/targets/cloudscale_server_group/tasks/failures.yml @@ -0,0 +1,45 @@ +--- +- name: Fail missing params + cloudscale_server_group: + register: grp + ignore_errors: True +- name: 'VERIFY: Fail name and UUID' + assert: + that: + - grp is failed + +- name: Create two server groups with the same name + uri: + url: 'https://api.cloudscale.ch/v1/server-groups' + method: POST + headers: + Authorization: 'Bearer {{ cloudscale_api_token }}' + body: + name: '{{ cloudscale_resource_prefix }}-duplicate' + type: 'anti-affinity' + body_format: json + status_code: 201 + register: duplicate + with_sequence: count=2 + +- name: Try access to duplicate name + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-duplicate' + register: grp + ignore_errors: True +- name: 'VERIFY: Try access to duplicate name' + assert: + that: + - grp is failed + - grp.msg.startswith('More than one server group with name exists') + +- name: Fail server group creation with UUID + cloudscale_server_group: + uuid: ea3b39a3-77a8-4d0b-881d-0bb00a1e7f48 + register: grp + ignore_errors: True +- name: 'VERIFY: Fail server group creation with UUID' + assert: + that: + - grp is failed + - grp.msg.startswith('missing required arguments') \ No newline at end of file diff --git a/test/integration/targets/cloudscale_server_group/tasks/main.yml b/test/integration/targets/cloudscale_server_group/tasks/main.yml new file mode 100644 index 00000000000..f8783414af8 --- /dev/null +++ b/test/integration/targets/cloudscale_server_group/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- block: + - import_tasks: failures.yml + - import_tasks: tests.yml + always: + - import_role: + name: cloudscale_common + tasks_from: cleanup_server_groups diff --git a/test/integration/targets/cloudscale_server_group/tasks/tests.yml b/test/integration/targets/cloudscale_server_group/tasks/tests.yml new file mode 100644 index 00000000000..6cf1a252e95 --- /dev/null +++ b/test/integration/targets/cloudscale_server_group/tasks/tests.yml @@ -0,0 +1,113 @@ +--- +- name: Create server group in check mode + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-grp' + register: grp + check_mode: yes +- name: 'VERIFY: Create server group in check mode' + assert: + that: + - grp is changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp' + - not grp.uuid + +- name: Create server group + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-grp' + register: grp +- name: 'VERIFY: Create server group' + assert: + that: + - grp is changed + - grp.type == 'anti-affinity' + - grp.name == '{{ cloudscale_resource_prefix }}-grp' + - grp.uuid + +- name: Remember uuid + set_fact: + server_group_uuid: '{{ grp.uuid }}' + +- name: Create server group idempotence + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-grp' + register: grp +- name: 'VERIFY: Create server group idempotence' + assert: + that: + - grp is not changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp' + - grp.uuid == server_group_uuid + +- name: Update server group in check mode + cloudscale_server_group: + uuid: '{{ server_group_uuid }}' + name: '{{ cloudscale_resource_prefix }}-grp2' + register: grp + check_mode: yes +- name: 'VERIFY: Update server group in check mode' + assert: + that: + - grp is changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp' + - grp.uuid == server_group_uuid + +- name: Update server group + cloudscale_server_group: + uuid: '{{ server_group_uuid }}' + name: '{{ cloudscale_resource_prefix }}-grp2' + register: grp +- name: 'VERIFY: Update server group' + assert: + that: + - grp is changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp2' + - grp.uuid == server_group_uuid + +- name: Update server group idempotence + cloudscale_server_group: + uuid: '{{ server_group_uuid }}' + name: '{{ cloudscale_resource_prefix }}-grp2' + register: grp +- name: 'VERIFY: Update server group idempotence' + assert: + that: + - grp is not changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp2' + - grp.uuid == server_group_uuid + +- name: Delete server group in check mode + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-grp2' + state: absent + register: grp + check_mode: yes +- name: 'VERIFY: Delete server group in check mode' + assert: + that: + - grp is changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp2' + - grp.uuid == server_group_uuid + +- name: Delete server group + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-grp2' + state: absent + register: grp +- name: 'VERIFY: Delete server group' + assert: + that: + - grp is changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp2' + - grp.uuid == server_group_uuid + +- name: Delete server group idempotence + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-grp2' + state: absent + register: grp +- name: 'VERIFY: Delete server group idempotence' + assert: + that: + - grp is not changed + - grp.name == '{{ cloudscale_resource_prefix }}-grp2' + - not grp.uuid