New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Scaleway lb backend #56386
WIP: Scaleway lb backend #56386
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -179,3 +179,9 @@ def warn(self, x): | |
"fr-par-1", | ||
"nl-ams-1", | ||
] | ||
|
||
# New convention about region naming | ||
SCALEWAY_NEW_LOCATION = { | ||
'fr-par': {'name': 'Paris 1', 'country': 'FR', "api_endpoint": 'https://api-world.scaleway.com'}, | ||
'nl-ams': {'name': 'Amsterdam 1', 'country': 'NL', "api_endpoint": 'https://api-world.scaleway.com'} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,6 +148,7 @@ | |
import time | ||
from ansible.module_utils.basic import AnsibleModule | ||
from ansible.module_utils.scaleway import SCALEWAY_REGIONS, SCALEWAY_ENDPOINT, scaleway_argument_spec, Scaleway | ||
from ansible.module_utils.scaleway import SCALEWAY_NEW_LOCATION, scaleway_argument_spec, Scaleway | ||
|
||
STABLE_STATES = ( | ||
"ready", | ||
|
@@ -319,6 +320,7 @@ def core(module): | |
"organization_id": module.params["organization_id"] | ||
} | ||
module.params['api_url'] = SCALEWAY_ENDPOINT | ||
module.params['api_url'] = SCALEWAY_NEW_LOCATION[region]["api_endpoint"] | ||
api = Scaleway(module=module) | ||
api.api_path = "lbaas/v1beta1/regions/%s/lbs" % region | ||
|
||
|
@@ -332,9 +334,9 @@ def main(): | |
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=[]), | ||
region=dict(required=True, choices=SCALEWAY_NEW_LOCATION.keys()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove |
||
organization_id=dict(required=True), | ||
wait=dict(type="bool", default=False), | ||
wait_timeout=dict(type="int", default=300), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,361 @@ | ||
#!/usr/bin/python | ||
# | ||
# Scaleway Load-balancer backend 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_back | ||
short_description: Scaleway load-balancer backend management module | ||
version_added: "2.8" | ||
author: Remy Leone (@sieben) | ||
description: | ||
- "This module manages load-balancers backend on Scaleway." | ||
extends_documentation_fragment: scaleway | ||
|
||
options: | ||
|
||
name: | ||
description: | ||
- Name of the load-balancer backend | ||
|
||
organization: | ||
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 | ||
|
||
forward_port: | ||
description: | ||
- User sessions will be forwarded to this port of backend servers | ||
|
||
forward_port_algorithm: | ||
description: | ||
- Load balancing algorithm: | ||
- Round-Robin: Equal share | ||
- leastconn: Least amount of TCP connection | ||
choices: | ||
- leastconn | ||
- roundrobin | ||
|
||
forward_protocol: | ||
description: | ||
- Backend protocol | ||
choices: | ||
- tcp | ||
- http | ||
default: | ||
- tcp | ||
|
||
on_marked_down_action: | ||
description: | ||
- Modify what occurs when a backend server is marked down | ||
choices: | ||
- on_marked_down_action_none | ||
- shutdown_sessions | ||
|
||
send_proxy_v2: | ||
description: | ||
- Enables PROXY protocol version 2 (must be supported by backend servers) | ||
type: bool | ||
default: no | ||
|
||
server_ip: | ||
description: | ||
- Backend server IP addresses list (IPv4 or IPv6) | ||
|
||
sticky_sessions: | ||
description: | ||
- Enables cookie-based session persistence | ||
default: none | ||
choices: | ||
- cookie | ||
- none | ||
- table (Will match on IP) | ||
|
||
sticky_sessions_cookie_name: | ||
description: | ||
- Cookie name for sticky sessions | ||
|
||
timeout_connect: | ||
description: | ||
- Maximum initial server connection establishment time in milliseconds | ||
|
||
timeout_server: | ||
description: | ||
- Maximum server connection inactivity time in milliseconds | ||
|
||
timeout_tunnel: | ||
description: | ||
- Maximum tunnel inactivity time in milliseconds | ||
|
||
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 = ''' | ||
''' | ||
|
||
RETURNS = ''' | ||
''' | ||
|
||
import datetime | ||
import time | ||
from ansible.module_utils.basic import AnsibleModule | ||
from ansible.module_utils.scaleway import SCALEWAY_NEW_LOCATION, 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"], | ||
} | ||
|
||
|
||
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_backend, 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_backend) | ||
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_backend)) | ||
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_backend, wished_lb_backend): | ||
diff = { | ||
attr: wished_lb_backend[attr] for attr in MUTABLE_ATTRIBUTES if target_lb_backend[attr] != wished_lb_backend[attr] | ||
} | ||
if diff: | ||
return {attr: wished_lb_backend[attr] for attr in MUTABLE_ATTRIBUTES} | ||
else: | ||
return diff | ||
|
||
|
||
def present_strategy(api, wished_lb_backend): | ||
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["backends"] | ||
lb_lookup = dict((lb["name"], lb) | ||
for lb in lbs_list) | ||
|
||
if wished_lb_backend["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_backend)) | ||
creation_response = api.post(path=api.api_path, | ||
data=payload_from_wished_lb(wished_lb_backend)) | ||
|
||
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_backend=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_backend["name"]] | ||
patch_payload = lb_attributes_should_be_changed(target_lb_backend=target_lb, | ||
wished_lb_backend=wished_lb_backend) | ||
|
||
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_backend=target_lb) | ||
return changed, lb_patch_response.json | ||
|
||
|
||
def absent_strategy(api, wished_lb_backend): | ||
response = api.get(path=api.api_path) | ||
changed = False | ||
|
||
status_code = response.status_code | ||
lbs_json = response.json | ||
lbs_list = lbs_json["backends"] | ||
|
||
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_backend["name"] not in lb_lookup.keys(): | ||
return changed, {} | ||
|
||
target_lb = lb_lookup[wished_lb_backend["name"]] | ||
changed = True | ||
if api.module.check_mode: | ||
return changed, {"status": "Load-balancer would be destroyed"} | ||
|
||
wait_to_complete_state_transition(api=api, lb_backend=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_backend=target_lb) | ||
return changed, response.json | ||
|
||
|
||
state_strategy = { | ||
"present": present_strategy, | ||
"absent": absent_strategy | ||
} | ||
|
||
|
||
def core(module): | ||
region = module.params["region"] | ||
lb_id = module.params["lb_id"] | ||
wished_load_balancer_back = { | ||
"name": module.params["name"], | ||
"state": module.params["state"], | ||
"lb_id": module.params["lb_id"], | ||
"forward_port": module.params["forward_port"], | ||
"forward_protocol": module.params["forward_protocol"], | ||
"forward_port_algorithm": module.params["forward_port_algorithm"], | ||
"sticky_sessions": module.params["sticky_sessions"], | ||
"timeout_connect": module.params["timeout_connect"], | ||
"timeout_server": module.params["timeout_server"], | ||
"timeout_tunnel": module.params["timeout_tunnel"] | ||
} | ||
module.params['api_url'] = SCALEWAY_NEW_LOCATION[region]["api_endpoint"] | ||
api = Scaleway(module=module) | ||
api.api_path = "lbaas/v1beta1/regions/%s/lbs/%s/backends" % (region, lb_id) | ||
|
||
changed, summary = state_strategy[wished_load_balancer_back["state"]](api=api, | ||
wished_lb_backend=wished_load_balancer_back) | ||
module.exit_json(changed=changed, scaleway_lb=summary) | ||
|
||
|
||
def main(): | ||
argument_spec = scaleway_argument_spec() | ||
argument_spec.update(dict( | ||
name=dict(required=True), | ||
state=dict(choices=state_strategy.keys(), default='present'), | ||
lb_id=dict(required=True), | ||
forward_port=dict(required=True, type='int'), | ||
forward_protocol=dict(choices=["http", "tcp"], default="tcp"), | ||
forward_port_algorithm=dict(choices=["roundrobin", "leastconn"], default="roundrobin"), | ||
region=dict(required=True, choices=SCALEWAY_NEW_LOCATION.keys()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove |
||
sticky_sessions=dict(choices=["cookie", "none", "table"], default="none"), | ||
timeout_connect=dict(type="int"), | ||
timeout_server=dict(type="int"), | ||
timeout_tunnel=dict(type="int"), | ||
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://api.scaleway.com
(withoutworld
)