2021-08-08 10:40:22 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2022-08-07 14:03:49 +02:00
|
|
|
# Copyright (c) Ansible Project
|
|
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2020-06-24 21:50:36 +02:00
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
import json
|
|
|
|
import re
|
|
|
|
import sys
|
|
|
|
|
|
|
|
from ansible.module_utils.basic import env_fallback
|
|
|
|
from ansible.module_utils.urls import fetch_url
|
|
|
|
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
|
|
|
|
|
|
|
|
|
|
|
def scaleway_argument_spec():
|
|
|
|
return dict(
|
|
|
|
api_token=dict(required=True, fallback=(env_fallback, ['SCW_TOKEN', 'SCW_API_KEY', 'SCW_OAUTH_TOKEN', 'SCW_API_TOKEN']),
|
|
|
|
no_log=True, aliases=['oauth_token']),
|
|
|
|
api_url=dict(fallback=(env_fallback, ['SCW_API_URL']), default='https://api.scaleway.com', aliases=['base_url']),
|
|
|
|
api_timeout=dict(type='int', default=30, aliases=['timeout']),
|
|
|
|
query_parameters=dict(type='dict', default={}),
|
|
|
|
validate_certs=dict(default=True, type='bool'),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def payload_from_object(scw_object):
|
|
|
|
return dict(
|
|
|
|
(k, v)
|
|
|
|
for k, v in scw_object.items()
|
|
|
|
if k != 'id' and v is not None
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class ScalewayException(Exception):
|
|
|
|
|
|
|
|
def __init__(self, message):
|
|
|
|
self.message = message
|
|
|
|
|
|
|
|
|
|
|
|
# Specify a complete Link header, for validation purposes
|
|
|
|
R_LINK_HEADER = r'''<[^>]+>;\srel="(first|previous|next|last)"
|
|
|
|
(,<[^>]+>;\srel="(first|previous|next|last)")*'''
|
|
|
|
# Specify a single relation, for iteration and string extraction purposes
|
2021-03-18 23:06:56 +01:00
|
|
|
R_RELATION = r'</?(?P<target_IRI>[^>]+)>; rel="(?P<relation>first|previous|next|last)"'
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
def parse_pagination_link(header):
|
|
|
|
if not re.match(R_LINK_HEADER, header, re.VERBOSE):
|
|
|
|
raise ScalewayException('Scaleway API answered with an invalid Link pagination header')
|
|
|
|
else:
|
|
|
|
relations = header.split(',')
|
|
|
|
parsed_relations = {}
|
|
|
|
rc_relation = re.compile(R_RELATION)
|
|
|
|
for relation in relations:
|
|
|
|
match = rc_relation.match(relation)
|
|
|
|
if not match:
|
|
|
|
raise ScalewayException('Scaleway API answered with an invalid relation in the Link pagination header')
|
|
|
|
data = match.groupdict()
|
|
|
|
parsed_relations[data['relation']] = data['target_IRI']
|
|
|
|
return parsed_relations
|
|
|
|
|
|
|
|
|
|
|
|
class Response(object):
|
|
|
|
|
|
|
|
def __init__(self, resp, info):
|
|
|
|
self.body = None
|
|
|
|
if resp:
|
|
|
|
self.body = resp.read()
|
|
|
|
self.info = info
|
|
|
|
|
|
|
|
@property
|
|
|
|
def json(self):
|
|
|
|
if not self.body:
|
|
|
|
if "body" in self.info:
|
|
|
|
return json.loads(self.info["body"])
|
|
|
|
return None
|
|
|
|
try:
|
|
|
|
return json.loads(self.body)
|
|
|
|
except ValueError:
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def status_code(self):
|
|
|
|
return self.info["status"]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def ok(self):
|
|
|
|
return self.status_code in (200, 201, 202, 204)
|
|
|
|
|
|
|
|
|
|
|
|
class Scaleway(object):
|
|
|
|
|
|
|
|
def __init__(self, module):
|
|
|
|
self.module = module
|
|
|
|
self.headers = {
|
|
|
|
'X-Auth-Token': self.module.params.get('api_token'),
|
|
|
|
'User-Agent': self.get_user_agent_string(module),
|
2020-06-02 15:09:58 +02:00
|
|
|
'Content-Type': 'application/json',
|
2020-03-09 10:11:07 +01:00
|
|
|
}
|
|
|
|
self.name = None
|
|
|
|
|
|
|
|
def get_resources(self):
|
|
|
|
results = self.get('/%s' % self.name)
|
|
|
|
|
|
|
|
if not results.ok:
|
|
|
|
raise ScalewayException('Error fetching {0} ({1}) [{2}: {3}]'.format(
|
|
|
|
self.name, '%s/%s' % (self.module.params.get('api_url'), self.name),
|
|
|
|
results.status_code, results.json['message']
|
|
|
|
))
|
|
|
|
|
|
|
|
return results.json.get(self.name)
|
|
|
|
|
|
|
|
def _url_builder(self, path, params):
|
|
|
|
d = self.module.params.get('query_parameters')
|
|
|
|
if params is not None:
|
|
|
|
d.update(params)
|
|
|
|
query_string = urlencode(d, doseq=True)
|
|
|
|
|
|
|
|
if path[0] == '/':
|
|
|
|
path = path[1:]
|
|
|
|
return '%s/%s?%s' % (self.module.params.get('api_url'), path, query_string)
|
|
|
|
|
|
|
|
def send(self, method, path, data=None, headers=None, params=None):
|
|
|
|
url = self._url_builder(path=path, params=params)
|
|
|
|
self.warn(url)
|
|
|
|
|
|
|
|
if headers is not None:
|
|
|
|
self.headers.update(headers)
|
|
|
|
|
|
|
|
if self.headers['Content-Type'] == "application/json":
|
|
|
|
data = self.module.jsonify(data)
|
|
|
|
|
|
|
|
resp, info = fetch_url(
|
|
|
|
self.module, url, data=data, headers=self.headers, method=method,
|
|
|
|
timeout=self.module.params.get('api_timeout')
|
|
|
|
)
|
|
|
|
|
|
|
|
# Exceptions in fetch_url may result in a status -1, the ensures a proper error to the user in all cases
|
|
|
|
if info['status'] == -1:
|
|
|
|
self.module.fail_json(msg=info['msg'])
|
|
|
|
|
|
|
|
return Response(resp, info)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_user_agent_string(module):
|
2021-08-12 12:07:50 +02:00
|
|
|
return "ansible %s Python %s" % (module.ansible_version, sys.version.split(' ', 1)[0])
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
def get(self, path, data=None, headers=None, params=None):
|
|
|
|
return self.send(method='GET', path=path, data=data, headers=headers, params=params)
|
|
|
|
|
|
|
|
def put(self, path, data=None, headers=None, params=None):
|
|
|
|
return self.send(method='PUT', path=path, data=data, headers=headers, params=params)
|
|
|
|
|
|
|
|
def post(self, path, data=None, headers=None, params=None):
|
|
|
|
return self.send(method='POST', path=path, data=data, headers=headers, params=params)
|
|
|
|
|
|
|
|
def delete(self, path, data=None, headers=None, params=None):
|
|
|
|
return self.send(method='DELETE', path=path, data=data, headers=headers, params=params)
|
|
|
|
|
|
|
|
def patch(self, path, data=None, headers=None, params=None):
|
|
|
|
return self.send(method="PATCH", path=path, data=data, headers=headers, params=params)
|
|
|
|
|
|
|
|
def update(self, path, data=None, headers=None, params=None):
|
|
|
|
return self.send(method="UPDATE", path=path, data=data, headers=headers, params=params)
|
|
|
|
|
|
|
|
def warn(self, x):
|
|
|
|
self.module.warn(str(x))
|
|
|
|
|
|
|
|
|
|
|
|
SCALEWAY_LOCATION = {
|
2022-02-14 18:13:55 +01:00
|
|
|
'par1': {
|
|
|
|
'name': 'Paris 1',
|
|
|
|
'country': 'FR',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-1',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-1'
|
|
|
|
},
|
|
|
|
|
|
|
|
'EMEA-FR-PAR1': {
|
|
|
|
'name': 'Paris 1',
|
|
|
|
'country': 'FR',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-1',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-1'
|
|
|
|
},
|
|
|
|
|
|
|
|
'par2': {
|
|
|
|
'name': 'Paris 2',
|
|
|
|
'country': 'FR',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-2',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-2'
|
|
|
|
},
|
|
|
|
|
|
|
|
'EMEA-FR-PAR2': {
|
|
|
|
'name': 'Paris 2',
|
|
|
|
'country': 'FR',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/fr-par-2',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/fr-par-2'
|
|
|
|
},
|
|
|
|
|
|
|
|
'ams1': {
|
|
|
|
'name': 'Amsterdam 1',
|
|
|
|
'country': 'NL',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/nl-ams-1',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/nl-ams-10'
|
|
|
|
},
|
|
|
|
|
|
|
|
'EMEA-NL-EVS': {
|
|
|
|
'name': 'Amsterdam 1',
|
|
|
|
'country': 'NL',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/nl-ams-1',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/nl-ams-1'
|
|
|
|
},
|
|
|
|
|
|
|
|
'waw1': {
|
|
|
|
'name': 'Warsaw 1',
|
|
|
|
'country': 'PL',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/pl-waw-1',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/pl-waw-1'
|
|
|
|
},
|
|
|
|
|
|
|
|
'EMEA-PL-WAW1': {
|
|
|
|
'name': 'Warsaw 1',
|
|
|
|
'country': 'PL',
|
|
|
|
'api_endpoint': 'https://api.scaleway.com/instance/v1/zones/pl-waw-1',
|
|
|
|
'api_endpoint_vpc': 'https://api.scaleway.com/vpc/v1/zones/pl-waw-1'
|
|
|
|
},
|
2020-03-09 10:11:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
SCALEWAY_ENDPOINT = "https://api.scaleway.com"
|
|
|
|
|
|
|
|
SCALEWAY_REGIONS = [
|
|
|
|
"fr-par",
|
|
|
|
"nl-ams",
|
2021-01-28 12:51:07 +01:00
|
|
|
"pl-waw",
|
2020-03-09 10:11:07 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
SCALEWAY_ZONES = [
|
|
|
|
"fr-par-1",
|
2021-01-28 12:51:07 +01:00
|
|
|
"fr-par-2",
|
2020-03-09 10:11:07 +01:00
|
|
|
"nl-ams-1",
|
2021-01-28 12:51:07 +01:00
|
|
|
"pl-waw-1",
|
2020-03-09 10:11:07 +01:00
|
|
|
]
|