diff --git a/lib/ansible/modules/cloud/cloudscale/cloudscale_server.py b/lib/ansible/modules/cloud/cloudscale/cloudscale_server.py index 9793fd4b4c8..d5088b9d3de 100644 --- a/lib/ansible/modules/cloud/cloudscale/cloudscale_server.py +++ b/lib/ansible/modules/cloud/cloudscale/cloudscale_server.py @@ -92,7 +92,15 @@ options: anti_affinity_with: description: - UUID of another server to create an anti-affinity group with. + - Mutually exclusive with I(server_groups). + - Deprecated, removed in version 2.11. type: str + server_groups: + description: + - List of UUID or names of server groups. + - Mutually exclusive with I(anti_affinity_with). + type: list + version_added: '2.8' user_data: description: - Cloud-init configuration (cloud-config) data to use for the server. @@ -109,28 +117,29 @@ extends_documentation_fragment: cloudscale ''' EXAMPLES = ''' -# Start a server (if it does not exist) and register the server details +# Create and start a server with an existing server group (shiny-group) - name: Start cloudscale.ch server cloudscale_server: name: my-shiny-cloudscale-server image: debian-8 flavor: flex-4 ssh_keys: ssh-rsa XXXXXXXXXX...XXXX ansible@cloudscale + server_groups: shiny-group use_private_network: True bulk_volume_size_gb: 100 api_token: xxxxxx - register: server1 -# Start another server in anti-affinity to the first one +# Start another server in anti-affinity (server group shiny-group) - name: Start second cloudscale.ch server cloudscale_server: name: my-other-shiny-server image: ubuntu-16.04 flavor: flex-8 ssh_keys: ssh-rsa XXXXXXXXXXX ansible@cloudscale - anti_affinity_with: '{{ server1.uuid }}' + server_groups: shiny-group api_token: xxxxxx + # Force to update the flavor of a running server - name: Start cloudscale.ch server cloudscale_server: @@ -224,10 +233,18 @@ ssh_host_keys: type: list sample: ["ecdsa-sha2-nistp256 XXXXX", ... ] anti_affinity_with: - description: List of servers in the same anti-affinity group + description: + - List of servers in the same anti-affinity group + - Deprecated, removed in version 2.11. returned: success when not state == absent - type: str + type: list sample: [] +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"} ] + version_added: '2.8' ''' from datetime import datetime, timedelta @@ -378,12 +395,42 @@ class AnsibleCloudscaleServer(AnsibleCloudscaleBase): 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 data = deepcopy(self._module.params) for i in ('uuid', 'state', 'force', 'api_timeout', 'api_token'): 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) @@ -396,6 +443,14 @@ class AnsibleCloudscaleServer(AnsibleCloudscaleBase): 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.") + server_info = self._update_param('flavor', server_info, requires_stop=True) server_info = self._update_param('name', server_info) @@ -450,7 +505,8 @@ def main(): use_public_network=dict(type='bool', default=True), use_private_network=dict(type='bool', default=False), use_ipv6=dict(type='bool', default=True), - anti_affinity_with=dict(), + anti_affinity_with=dict(removed_in_version='2.11'), + server_groups=dict(type='list'), user_data=dict(), force=dict(type='bool', default=False) )) @@ -458,6 +514,7 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, required_one_of=(('name', 'uuid'),), + mutually_exclusive=(('anti_affinity_with', 'server_groups'),), supports_check_mode=True, ) diff --git a/test/integration/targets/cloudscale_server/tasks/failures.yml b/test/integration/targets/cloudscale_server/tasks/failures.yml new file mode 100644 index 00000000000..2c975d76bd7 --- /dev/null +++ b/test/integration/targets/cloudscale_server/tasks/failures.yml @@ -0,0 +1,53 @@ +--- +- name: Fail missing params + cloudscale_server: + register: srv + ignore_errors: True +- name: 'VERIFY: Fail name and UUID' + assert: + that: + - srv is failed + +- name: Fail unexisting server group + cloudscale_server: + name: '{{ cloudscale_resource_prefix }}-test-group' + flavor: '{{ cloudscale_test_flavor }}' + image: '{{ cloudscale_test_image }}' + password: '{{ cloudscale_test_password }}' + server_groups: '{{ cloudscale_resource_prefix }}-unexist-group' + ignore_errors: True + register: srv +- name: 'VERIFY: Fail unexisting server group' + assert: + that: + - srv is failed + - srv.msg.startswith('Server group name or UUID not found') + +- 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 to use server groups with identical name + cloudscale_server: + name: '{{ cloudscale_resource_prefix }}-test-group' + flavor: '{{ cloudscale_test_flavor }}' + image: '{{ cloudscale_test_image }}' + password: '{{ cloudscale_test_password }}' + server_groups: '{{ cloudscale_resource_prefix }}-duplicate' + ignore_errors: True + register: srv +- name: 'VERIFY: Fail unexisting server group' + assert: + that: + - srv is failed + - srv.msg.startswith('More than one server group with name exists') diff --git a/test/integration/targets/cloudscale_server/tasks/main.yml b/test/integration/targets/cloudscale_server/tasks/main.yml index 14db856ddae..cb53757eb8f 100644 --- a/test/integration/targets/cloudscale_server/tasks/main.yml +++ b/test/integration/targets/cloudscale_server/tasks/main.yml @@ -1,7 +1,11 @@ --- - block: + - import_tasks: failures.yml - import_tasks: tests.yml always: - import_role: name: cloudscale_common tasks_from: cleanup_servers + - import_role: + name: cloudscale_common + tasks_from: cleanup_server_groups \ No newline at end of file diff --git a/test/integration/targets/cloudscale_server/tasks/tests.yml b/test/integration/targets/cloudscale_server/tasks/tests.yml index e9936e078db..36abc8f9ff7 100644 --- a/test/integration/targets/cloudscale_server/tasks/tests.yml +++ b/test/integration/targets/cloudscale_server/tasks/tests.yml @@ -1,10 +1,17 @@ --- +- name: Setup server groups + cloudscale_server_group: + name: '{{ cloudscale_resource_prefix }}-group-{{ item }}' + type: anti-affinity + with_sequence: count=2 + - name: Test create a running server in check mode cloudscale_server: name: '{{ cloudscale_resource_prefix }}-test' flavor: '{{ cloudscale_test_flavor }}' image: '{{ cloudscale_test_image }}' ssh_keys: '{{ cloudscale_test_ssh_key }}' + server_groups: '{{ cloudscale_resource_prefix }}-group-1' register: server check_mode: yes - name: Verify create a running server in check mode @@ -19,12 +26,14 @@ flavor: '{{ cloudscale_test_flavor }}' image: '{{ cloudscale_test_image }}' ssh_keys: '{{ cloudscale_test_ssh_key }}' + server_groups: '{{ cloudscale_resource_prefix }}-group-1' register: server - name: Verify create a running server assert: that: - server is changed - server.state == 'running' + - server.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' - name: Test create a running server idempotence cloudscale_server: @@ -32,12 +41,14 @@ flavor: '{{ cloudscale_test_flavor }}' image: '{{ cloudscale_test_image }}' ssh_keys: '{{ cloudscale_test_ssh_key }}' + server_groups: '{{ cloudscale_resource_prefix }}-group-1' register: server - name: Verify create a running server idempotence assert: that: - server is not changed - server.state == 'running' + - server.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' - name: Test update flavor of a running server without force in check mode cloudscale_server: @@ -54,6 +65,7 @@ - server is not changed - server.state == 'running' - server.flavor.slug == '{{ cloudscale_test_flavor }}' + - server.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' - name: Test update flavor of a running server without force cloudscale_server: @@ -69,6 +81,7 @@ - server is not changed - server.state == 'running' - server.flavor.slug == '{{ cloudscale_test_flavor }}' + - server.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' - name: Test update flavor of a running server without force idempotence cloudscale_server: @@ -84,6 +97,7 @@ - server is not changed - server.state == 'running' - server.flavor.slug == '{{ cloudscale_test_flavor }}' + - server.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' - name: Test update flavor and name of a running server without force in check mode cloudscale_server: @@ -196,7 +210,7 @@ flavor: '{{ cloudscale_test_flavor }}' image: '{{ cloudscale_test_image }}' ssh_keys: '{{ cloudscale_test_ssh_key }}' - anti_affinity_with: '{{ running_server_uuid }}' + server_groups: '{{ cloudscale_resource_prefix }}-group-1' use_public_network: no use_private_network: yes state: stopped @@ -214,7 +228,7 @@ flavor: '{{ cloudscale_test_flavor }}' image: '{{ cloudscale_test_image }}' ssh_keys: '{{ cloudscale_test_ssh_key }}' - anti_affinity_with: '{{ running_server_uuid }}' + server_groups: '{{ cloudscale_resource_prefix }}-group-1' use_public_network: no use_private_network: yes state: stopped @@ -226,6 +240,7 @@ - server_stopped.state == 'stopped' - server_stopped.anti_affinity_with.0.uuid == running_server_uuid - server_stopped.interfaces.0.type == 'private' + - server_stopped.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' - name: Test create server stopped in anti affinity and private network only idempotence cloudscale_server: @@ -233,7 +248,7 @@ flavor: '{{ cloudscale_test_flavor }}' image: '{{ cloudscale_test_image }}' ssh_keys: '{{ cloudscale_test_ssh_key }}' - anti_affinity_with: '{{ running_server_uuid }}' + server_groups: '{{ cloudscale_resource_prefix }}-group-1' use_public_network: no use_private_network: yes state: stopped @@ -245,6 +260,27 @@ - server_stopped.state == 'stopped' - server_stopped.anti_affinity_with.0.uuid == running_server_uuid - server_stopped.interfaces.0.type == 'private' + - server_stopped.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' + +- name: Test change server group not changed + cloudscale_server: + name: '{{ cloudscale_resource_prefix }}-test-stopped' + flavor: '{{ cloudscale_test_flavor }}' + image: '{{ cloudscale_test_image }}' + ssh_keys: '{{ cloudscale_test_ssh_key }}' + server_groups: '{{ cloudscale_resource_prefix }}-group-2' + use_public_network: no + use_private_network: yes + state: stopped + register: server_stopped +- name: Verify Test update server group not changed + assert: + that: + - server_stopped is not changed + - server_stopped.state == 'stopped' + - server_stopped.anti_affinity_with.0.uuid == running_server_uuid + - server_stopped.interfaces.0.type == 'private' + - server_stopped.server_groups.0.name == '{{ cloudscale_resource_prefix }}-group-1' - name: Test create server with password in check mode cloudscale_server: @@ -371,7 +407,6 @@ - server.flavor.slug == '{{ cloudscale_test_flavor }}' - server.name == '{{ cloudscale_resource_prefix }}-test' - - name: Test update a stopped server idempotence cloudscale_server: uuid: '{{ server.uuid }}'