1# -*- coding: utf-8 -*- 2from __future__ import (absolute_import, division, print_function) 3__metaclass__ = type 4 5import json 6import re 7import sys 8 9from ansible.module_utils.basic import env_fallback 10from ansible.module_utils.urls import fetch_url 11from ansible.module_utils.six.moves.urllib.parse import urlencode 12 13 14def scaleway_argument_spec(): 15 return dict( 16 api_token=dict(required=True, fallback=(env_fallback, ['SCW_TOKEN', 'SCW_API_KEY', 'SCW_OAUTH_TOKEN', 'SCW_API_TOKEN']), 17 no_log=True, aliases=['oauth_token']), 18 api_url=dict(fallback=(env_fallback, ['SCW_API_URL']), default='https://api.scaleway.com', aliases=['base_url']), 19 api_timeout=dict(type='int', default=30, aliases=['timeout']), 20 query_parameters=dict(type='dict', default={}), 21 validate_certs=dict(default=True, type='bool'), 22 ) 23 24 25def payload_from_object(scw_object): 26 return dict( 27 (k, v) 28 for k, v in scw_object.items() 29 if k != 'id' and v is not None 30 ) 31 32 33class ScalewayException(Exception): 34 35 def __init__(self, message): 36 self.message = message 37 38 39# Specify a complete Link header, for validation purposes 40R_LINK_HEADER = r'''<[^>]+>;\srel="(first|previous|next|last)" 41 (,<[^>]+>;\srel="(first|previous|next|last)")*''' 42# Specify a single relation, for iteration and string extraction purposes 43R_RELATION = r'</?(?P<target_IRI>[^>]+)>; rel="(?P<relation>first|previous|next|last)"' 44 45 46def parse_pagination_link(header): 47 if not re.match(R_LINK_HEADER, header, re.VERBOSE): 48 raise ScalewayException('Scaleway API answered with an invalid Link pagination header') 49 else: 50 relations = header.split(',') 51 parsed_relations = {} 52 rc_relation = re.compile(R_RELATION) 53 for relation in relations: 54 match = rc_relation.match(relation) 55 if not match: 56 raise ScalewayException('Scaleway API answered with an invalid relation in the Link pagination header') 57 data = match.groupdict() 58 parsed_relations[data['relation']] = data['target_IRI'] 59 return parsed_relations 60 61 62class Response(object): 63 64 def __init__(self, resp, info): 65 self.body = None 66 if resp: 67 self.body = resp.read() 68 self.info = info 69 70 @property 71 def json(self): 72 if not self.body: 73 if "body" in self.info: 74 return json.loads(self.info["body"]) 75 return None 76 try: 77 return json.loads(self.body) 78 except ValueError: 79 return None 80 81 @property 82 def status_code(self): 83 return self.info["status"] 84 85 @property 86 def ok(self): 87 return self.status_code in (200, 201, 202, 204) 88 89 90class Scaleway(object): 91 92 def __init__(self, module): 93 self.module = module 94 self.headers = { 95 'X-Auth-Token': self.module.params.get('api_token'), 96 'User-Agent': self.get_user_agent_string(module), 97 'Content-Type': 'application/json', 98 } 99 self.name = None 100 101 def get_resources(self): 102 results = self.get('/%s' % self.name) 103 104 if not results.ok: 105 raise ScalewayException('Error fetching {0} ({1}) [{2}: {3}]'.format( 106 self.name, '%s/%s' % (self.module.params.get('api_url'), self.name), 107 results.status_code, results.json['message'] 108 )) 109 110 return results.json.get(self.name) 111 112 def _url_builder(self, path, params): 113 d = self.module.params.get('query_parameters') 114 if params is not None: 115 d.update(params) 116 query_string = urlencode(d, doseq=True) 117 118 if path[0] == '/': 119 path = path[1:] 120 return '%s/%s?%s' % (self.module.params.get('api_url'), path, query_string) 121 122 def send(self, method, path, data=None, headers=None, params=None): 123 url = self._url_builder(path=path, params=params) 124 self.warn(url) 125 126 if headers is not None: 127 self.headers.update(headers) 128 129 if self.headers['Content-Type'] == "application/json": 130 data = self.module.jsonify(data) 131 132 resp, info = fetch_url( 133 self.module, url, data=data, headers=self.headers, method=method, 134 timeout=self.module.params.get('api_timeout') 135 ) 136 137 # Exceptions in fetch_url may result in a status -1, the ensures a proper error to the user in all cases 138 if info['status'] == -1: 139 self.module.fail_json(msg=info['msg']) 140 141 return Response(resp, info) 142 143 @staticmethod 144 def get_user_agent_string(module): 145 return "ansible %s Python %s" % (module.ansible_version, sys.version.split(' ', 1)[0]) 146 147 def get(self, path, data=None, headers=None, params=None): 148 return self.send(method='GET', path=path, data=data, headers=headers, params=params) 149 150 def put(self, path, data=None, headers=None, params=None): 151 return self.send(method='PUT', path=path, data=data, headers=headers, params=params) 152 153 def post(self, path, data=None, headers=None, params=None): 154 return self.send(method='POST', path=path, data=data, headers=headers, params=params) 155 156 def delete(self, path, data=None, headers=None, params=None): 157 return self.send(method='DELETE', path=path, data=data, headers=headers, params=params) 158 159 def patch(self, path, data=None, headers=None, params=None): 160 return self.send(method="PATCH", path=path, data=data, headers=headers, params=params) 161 162 def update(self, path, data=None, headers=None, params=None): 163 return self.send(method="UPDATE", path=path, data=data, headers=headers, params=params) 164 165 def warn(self, x): 166 self.module.warn(str(x)) 167 168 169SCALEWAY_LOCATION = { 170 'par1': {'name': 'Paris 1', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-1'}, 171 'EMEA-FR-PAR1': {'name': 'Paris 1', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-1'}, 172 173 'par2': {'name': 'Paris 2', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-2'}, 174 'EMEA-FR-PAR2': {'name': 'Paris 2', 'country': 'FR', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/fr-par-2'}, 175 176 'ams1': {'name': 'Amsterdam 1', 'country': 'NL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/nl-ams-1'}, 177 'EMEA-NL-EVS': {'name': 'Amsterdam 1', 'country': 'NL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/nl-ams-1'}, 178 179 'waw1': {'name': 'Warsaw 1', 'country': 'PL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/pl-waw-1'}, 180 'EMEA-PL-WAW1': {'name': 'Warsaw 1', 'country': 'PL', "api_endpoint": 'https://api.scaleway.com/instance/v1/zones/pl-waw-1'}, 181} 182 183SCALEWAY_ENDPOINT = "https://api.scaleway.com" 184 185SCALEWAY_REGIONS = [ 186 "fr-par", 187 "nl-ams", 188 "pl-waw", 189] 190 191SCALEWAY_ZONES = [ 192 "fr-par-1", 193 "fr-par-2", 194 "nl-ams-1", 195 "pl-waw-1", 196] 197