From accbcdeccbbefbb3dba1aee2f8caf71071edf5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Mon, 11 Feb 2019 16:28:55 +0100 Subject: [PATCH] Add a Scaleway load-balancer module (#51741) --- lib/ansible/module_utils/scaleway.py | 12 + .../modules/cloud/scaleway/scaleway_lb.py | 348 ++++++++++++++++++ .../roles/scaleway_lb/defaults/main.yml | 8 + test/legacy/roles/scaleway_lb/tasks/main.yml | 216 +++++++++++ test/legacy/scaleway.yml | 1 + 5 files changed, 585 insertions(+) create mode 100644 lib/ansible/modules/cloud/scaleway/scaleway_lb.py create mode 100644 test/legacy/roles/scaleway_lb/defaults/main.yml create mode 100644 test/legacy/roles/scaleway_lb/tasks/main.yml diff --git a/lib/ansible/module_utils/scaleway.py b/lib/ansible/module_utils/scaleway.py index d6a49bac2a7..537b3de30a6 100644 --- a/lib/ansible/module_utils/scaleway.py +++ b/lib/ansible/module_utils/scaleway.py @@ -167,3 +167,15 @@ SCALEWAY_LOCATION = { 'ams1': {'name': 'Amsterdam 1', 'country': 'NL', "api_endpoint": 'https://cp-ams1.scaleway.com'}, 'EMEA-NL-EVS': {'name': 'Amsterdam 1', 'country': 'NL', "api_endpoint": 'https://cp-ams1.scaleway.com'} } + +SCALEWAY_ENDPOINT = "https://api-world.scaleway.com" + +SCALEWAY_REGIONS = [ + "fr-par", + "nl-ams", +] + +SCALEWAY_ZONES = [ + "fr-par-1", + "nl-ams-1", +] diff --git a/lib/ansible/modules/cloud/scaleway/scaleway_lb.py b/lib/ansible/modules/cloud/scaleway/scaleway_lb.py new file mode 100644 index 00000000000..03dc816de26 --- /dev/null +++ b/lib/ansible/modules/cloud/scaleway/scaleway_lb.py @@ -0,0 +1,348 @@ +#!/usr/bin/python +# +# Scaleway Load-balancer management module +# +# Copyright (C) 2018 Online SAS. +# https://www.scaleway.com +# +# 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: scaleway_lb +short_description: Scaleway load-balancer management module +version_added: "2.8" +author: Remy Leone (@sieben) +description: + - "This module manages load-balancers on Scaleway." +extends_documentation_fragment: scaleway + +options: + + name: + description: + - Name of the load-balancer + required: true + + organization_id: + description: + - Organization identifier + required: true + + state: + description: + - Indicate desired state of the instance. + default: present + choices: + - present + - absent + + region: + description: + - Scaleway zone + required: true + choices: + - nl-ams + - fr-par + + tags: + description: + - List of tags to apply to the load-balancer + + wait: + description: + - Wait for the load-balancer to reach its desired state before returning. + type: bool + default: 'no' + + wait_timeout: + description: + - Time to wait for the load-balancer to reach the expected state + required: false + default: 300 + + wait_sleep_time: + description: + - Time to wait before every attempt to check the state of the load-balancer + required: false + default: 3 +''' + +EXAMPLES = ''' +- name: Create a load-balancer + scaleway_lb: + name: foobar + state: present + organization_id: 951df375-e094-4d26-97c1-ba548eeb9c42 + region: fr-par + tags: + - hello + +- name: Delete a load-balancer + scaleway_lb: + name: foobar + state: absent + organization_id: 951df375-e094-4d26-97c1-ba548eeb9c42 + region: fr-par +''' + +RETURNS = ''' +{ + "scaleway_lb": { + "backend_count": 0, + "frontend_count": 0, + "description": "Description of my load-balancer", + "id": "00000000-0000-0000-0000-000000000000", + "instances": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "ip_address": "10.0.0.1", + "region": "fr-par", + "status": "ready" + }, + { + "id": "00000000-0000-0000-0000-000000000000", + "ip_address": "10.0.0.2", + "region": "fr-par", + "status": "ready" + } + ], + "ip": [ + { + "id": "00000000-0000-0000-0000-000000000000", + "ip_address": "192.168.0.1", + "lb_id": "00000000-0000-0000-0000-000000000000", + "region": "fr-par", + "organization_id": "00000000-0000-0000-0000-000000000000", + "reverse": "" + } + ], + "name": "lb_ansible_test", + "organization_id": "00000000-0000-0000-0000-000000000000", + "region": "fr-par", + "status": "ready", + "tags": [ + "first_tag", + "second_tag" + ] + } +} +''' + +import datetime +import time +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.scaleway import SCALEWAY_REGIONS, SCALEWAY_ENDPOINT, scaleway_argument_spec, Scaleway + +STABLE_STATES = ( + "ready", + "absent" +) + +MUTABLE_ATTRIBUTES = ( + "name", + "description" +) + + +def payload_from_wished_lb(wished_lb): + return { + "organization_id": wished_lb["organization_id"], + "name": wished_lb["name"], + "tags": wished_lb["tags"], + "description": wished_lb["description"] + } + + +def fetch_state(api, lb): + api.module.debug("fetch_state of load-balancer: %s" % lb["id"]) + response = api.get(path=api.api_path + "/%s" % lb["id"]) + + if response.status_code == 404: + return "absent" + + if not response.ok: + msg = 'Error during state fetching: (%s) %s' % (response.status_code, response.json) + api.module.fail_json(msg=msg) + + try: + api.module.debug("Load-balancer %s in state: %s" % (lb["id"], response.json["status"])) + return response.json["status"] + except KeyError: + api.module.fail_json(msg="Could not fetch state in %s" % response.json) + + +def wait_to_complete_state_transition(api, lb, force_wait=False): + wait = api.module.params["wait"] + if not (wait or force_wait): + return + wait_timeout = api.module.params["wait_timeout"] + wait_sleep_time = api.module.params["wait_sleep_time"] + + start = datetime.datetime.utcnow() + end = start + datetime.timedelta(seconds=wait_timeout) + while datetime.datetime.utcnow() < end: + api.module.debug("We are going to wait for the load-balancer to finish its transition") + state = fetch_state(api, lb) + if state in STABLE_STATES: + api.module.debug("It seems that the load-balancer is not in transition anymore.") + api.module.debug("load-balancer in state: %s" % fetch_state(api, lb)) + break + time.sleep(wait_sleep_time) + else: + api.module.fail_json(msg="Server takes too long to finish its transition") + + +def lb_attributes_should_be_changed(target_lb, wished_lb): + diff = { + attr: wished_lb[attr] for attr in MUTABLE_ATTRIBUTES if target_lb[attr] != wished_lb[attr] + } + if diff: + return {attr: wished_lb[attr] for attr in MUTABLE_ATTRIBUTES} + else: + return diff + + +def present_strategy(api, wished_lb): + changed = False + + response = api.get(path=api.api_path) + if not response.ok: + api.module.fail_json(msg='Error getting load-balancers [{0}: {1}]'.format( + response.status_code, response.json['message'])) + + lbs_list = response.json["lbs"] + lb_lookup = dict((lb["name"], lb) + for lb in lbs_list) + + if wished_lb["name"] not in lb_lookup.keys(): + changed = True + if api.module.check_mode: + return changed, {"status": "A load-balancer would be created."} + + # Create Load-balancer + api.warn(payload_from_wished_lb(wished_lb)) + creation_response = api.post(path=api.api_path, + data=payload_from_wished_lb(wished_lb)) + + if not creation_response.ok: + msg = "Error during lb creation: %s: '%s' (%s)" % (creation_response.info['msg'], + creation_response.json['message'], + creation_response.json) + api.module.fail_json(msg=msg) + + wait_to_complete_state_transition(api=api, lb=creation_response.json) + response = api.get(path=api.api_path + "/%s" % creation_response.json["id"]) + return changed, response.json + + target_lb = lb_lookup[wished_lb["name"]] + patch_payload = lb_attributes_should_be_changed(target_lb=target_lb, + wished_lb=wished_lb) + + if not patch_payload: + return changed, target_lb + + changed = True + if api.module.check_mode: + return changed, {"status": "Load-balancer attributes would be changed."} + + lb_patch_response = api.put(path=api.api_path + "/%s" % target_lb["id"], + data=patch_payload) + + if not lb_patch_response.ok: + api.module.fail_json(msg='Error during load-balancer attributes update: [{0}: {1}]'.format( + lb_patch_response.status_code, lb_patch_response.json['message'])) + + wait_to_complete_state_transition(api=api, lb=target_lb) + return changed, lb_patch_response.json + + +def absent_strategy(api, wished_lb): + response = api.get(path=api.api_path) + changed = False + + status_code = response.status_code + lbs_json = response.json + lbs_list = lbs_json["lbs"] + + if not response.ok: + api.module.fail_json(msg='Error getting load-balancers [{0}: {1}]'.format( + status_code, response.json['message'])) + + lb_lookup = dict((lb["name"], lb) + for lb in lbs_list) + if wished_lb["name"] not in lb_lookup.keys(): + return changed, {} + + target_lb = lb_lookup[wished_lb["name"]] + changed = True + if api.module.check_mode: + return changed, {"status": "Load-balancer would be destroyed"} + + wait_to_complete_state_transition(api=api, lb=target_lb, force_wait=True) + response = api.delete(path=api.api_path + "/%s" % target_lb["id"]) + if not response.ok: + api.module.fail_json(msg='Error deleting load-balancer [{0}: {1}]'.format( + response.status_code, response.json)) + + wait_to_complete_state_transition(api=api, lb=target_lb) + return changed, response.json + + +state_strategy = { + "present": present_strategy, + "absent": absent_strategy +} + + +def core(module): + region = module.params["region"] + wished_load_balancer = { + "state": module.params["state"], + "name": module.params["name"], + "description": module.params["description"], + "tags": module.params["tags"], + "organization_id": module.params["organization_id"] + } + module.params['api_url'] = SCALEWAY_ENDPOINT + api = Scaleway(module=module) + api.api_path = "lbaas/v1beta1/regions/%s/lbs" % region + + changed, summary = state_strategy[wished_load_balancer["state"]](api=api, + wished_lb=wished_load_balancer) + module.exit_json(changed=changed, scaleway_lb=summary) + + +def main(): + argument_spec = scaleway_argument_spec() + argument_spec.update(dict( + name=dict(required=True), + description=dict(required=True), + region=dict(required=True, choices=SCALEWAY_REGIONS), + state=dict(choices=state_strategy.keys(), default='present'), + tags=dict(type="list", default=[]), + organization_id=dict(required=True), + wait=dict(type="bool", default=False), + wait_timeout=dict(type="int", default=300), + wait_sleep_time=dict(type="int", default=3), + )) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + core(module) + + +if __name__ == '__main__': + main() diff --git a/test/legacy/roles/scaleway_lb/defaults/main.yml b/test/legacy/roles/scaleway_lb/defaults/main.yml new file mode 100644 index 00000000000..48d56e10899 --- /dev/null +++ b/test/legacy/roles/scaleway_lb/defaults/main.yml @@ -0,0 +1,8 @@ +--- +scaleway_region: fr-par +name: lb_ansible_test +description: Load-balancer used for testing scaleway_lb ansible module +updated_description: Load-balancer used for testing scaleway_lb ansible module (Updated description) +tags: + - first_tag + - second_tag diff --git a/test/legacy/roles/scaleway_lb/tasks/main.yml b/test/legacy/roles/scaleway_lb/tasks/main.yml new file mode 100644 index 00000000000..6bbbe0e703a --- /dev/null +++ b/test/legacy/roles/scaleway_lb/tasks/main.yml @@ -0,0 +1,216 @@ +# SCW_API_KEY='XXX' SCW_ORG='YYY' ansible-playbook ./test/legacy/scaleway.yml --tags test_scaleway_lb + +- name: Create a load-balancer (Check) + check_mode: yes + scaleway_lb: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + organization_id: '{{ scw_org }}' + description: '{{ description }}' + tags: '{{ tags }}' + register: lb_creation_check_task + +- debug: var=lb_creation_check_task + +- name: lb_creation_check_task is success + assert: + that: + - lb_creation_check_task is success + +- name: lb_creation_check_task is changed + assert: + that: + - lb_creation_check_task is changed + +- name: Create load-balancer + scaleway_lb: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + organization_id: '{{ scw_org }}' + description: '{{ description }}' + tags: '{{ tags }}' + wait: true + register: lb_creation_task + +- debug: var=lb_creation_task + +- name: lb_creation_task is success + assert: + that: + - lb_creation_task is success + +- name: lb_creation_task is changed + assert: + that: + - lb_creation_task is changed + +- name: Assert that the load-balancer is in a valid state + assert: + that: + - lb_creation_task.scaleway_lb.status == "ready" + +- name: Create load-balancer (Confirmation) + scaleway_lb: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + organization_id: '{{ scw_org }}' + tags: '{{ tags }}' + description: '{{ description }}' + register: lb_creation_confirmation_task + +- debug: var=lb_creation_confirmation_task + +- name: lb_creation_confirmation_task is success + assert: + that: + - lb_creation_confirmation_task is success + +- name: lb_creation_confirmation_task is not changed + assert: + that: + - lb_creation_confirmation_task is not changed + +- name: Assert that the load-balancer is in a valid state + assert: + that: + - lb_creation_confirmation_task.scaleway_lb.status == "ready" + +- name: Update load-balancer (Check) + check_mode: yes + scaleway_lb: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + organization_id: '{{ scw_org }}' + tags: '{{ tags }}' + description: '{{ updated_description }}' + register: lb_update_check_task + +- debug: var=lb_update_check_task + +- name: lb_update_check_task is success + assert: + that: + - lb_update_check_task is success + +- name: lb_update_check_task is changed + assert: + that: + - lb_update_check_task is changed + +- name: Update load-balancer + scaleway_lb: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + tags: '{{ tags }}' + organization_id: '{{ scw_org }}' + description: '{{ updated_description }}' + wait: true + register: lb_update_task + +- debug: var=lb_update_task + +- name: lb_update_task is success + assert: + that: + - lb_update_task is success + +- name: lb_update_task is changed + assert: + that: + - lb_update_task is changed + +- name: Assert that the load-balancer is in a valid state + assert: + that: + - lb_update_task.scaleway_lb.status == "ready" + +- name: Update load-balancer (Confirmation) + scaleway_lb: + state: present + name: '{{ name }}' + region: '{{ scaleway_region }}' + tags: '{{ tags }}' + organization_id: '{{ scw_org }}' + description: '{{ updated_description }}' + register: lb_update_confirmation_task + +- debug: var=lb_update_confirmation_task + +- name: lb_update_confirmation_task is success + assert: + that: + - lb_update_confirmation_task is success + +- name: lb_update_confirmation_task is not changed + assert: + that: + - lb_update_confirmation_task is not changed + +- name: Assert that the load-balancer is in a valid state + assert: + that: + - lb_update_confirmation_task.scaleway_lb.status == "ready" + +- name: Delete load-balancer (Check) + check_mode: yes + scaleway_lb: + state: absent + name: '{{ name }}' + region: '{{ scaleway_region }}' + description: '{{ description }}' + organization_id: '{{ scw_org }}' + register: lb_deletion_check_task + +- name: lb_deletion_check_task is success + assert: + that: + - lb_deletion_check_task is success + +- name: lb_deletion_check_task is changed + assert: + that: + - lb_deletion_check_task is changed + +- name: Delete load-balancer + scaleway_lb: + state: absent + name: '{{ name }}' + region: '{{ scaleway_region }}' + description: '{{ description }}' + organization_id: '{{ scw_org }}' + wait: true + register: lb_deletion_task + +- name: lb_deletion_task is success + assert: + that: + - lb_deletion_task is success + +- name: lb_deletion_task is changed + assert: + that: + - lb_deletion_task is changed + +- name: Delete load-balancer (Confirmation) + scaleway_lb: + state: absent + name: '{{ name }}' + region: '{{ scaleway_region }}' + description: '{{ description }}' + organization_id: '{{ scw_org }}' + register: lb_deletion_confirmation_task + +- name: lb_deletion_confirmation_task is success + assert: + that: + - lb_deletion_confirmation_task is success + +- name: lb_deletion_confirmation_task is not changed + assert: + that: + - lb_deletion_confirmation_task is not changed diff --git a/test/legacy/scaleway.yml b/test/legacy/scaleway.yml index c8f19fd657a..f65a059048c 100644 --- a/test/legacy/scaleway.yml +++ b/test/legacy/scaleway.yml @@ -10,6 +10,7 @@ - { role: scaleway_image_facts, tags: test_scaleway_image_facts } - { role: scaleway_ip, tags: test_scaleway_ip } - { role: scaleway_ip_facts, tags: test_scaleway_ip_facts } + - { role: scaleway_lb, tags: test_scaleway_lb } - { role: scaleway_organization_facts, tags: test_scaleway_organization_facts } - { role: scaleway_s3, tags: test_scaleway_s3 } - { role: scaleway_security_group_facts, tags: test_scaleway_security_group_facts }