1# -*- coding: utf-8 -*- 2# Copyright: (c) 2019 Gregory Thiemonge <gregory.thiemonge@gmail.com> 3# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 4 5from __future__ import absolute_import, division, print_function 6__metaclass__ = type 7 8import json 9 10from ansible.module_utils.common.text.converters import to_native, to_text 11from ansible.module_utils.urls import fetch_url 12 13 14class GandiLiveDNSAPI(object): 15 16 api_endpoint = 'https://api.gandi.net/v5/livedns' 17 changed = False 18 19 error_strings = { 20 400: 'Bad request', 21 401: 'Permission denied', 22 404: 'Resource not found', 23 } 24 25 attribute_map = { 26 'record': 'rrset_name', 27 'type': 'rrset_type', 28 'ttl': 'rrset_ttl', 29 'values': 'rrset_values' 30 } 31 32 def __init__(self, module): 33 self.module = module 34 self.api_key = module.params['api_key'] 35 36 def _build_error_message(self, module, info): 37 s = '' 38 body = info.get('body') 39 if body: 40 errors = module.from_json(body).get('errors') 41 if errors: 42 error = errors[0] 43 name = error.get('name') 44 if name: 45 s += '{0} :'.format(name) 46 description = error.get('description') 47 if description: 48 s += description 49 return s 50 51 def _gandi_api_call(self, api_call, method='GET', payload=None, error_on_404=True): 52 headers = {'Authorization': 'Apikey {0}'.format(self.api_key), 53 'Content-Type': 'application/json'} 54 data = None 55 if payload: 56 try: 57 data = json.dumps(payload) 58 except Exception as e: 59 self.module.fail_json(msg="Failed to encode payload as JSON: %s " % to_native(e)) 60 61 resp, info = fetch_url(self.module, 62 self.api_endpoint + api_call, 63 headers=headers, 64 data=data, 65 method=method) 66 67 error_msg = '' 68 if info['status'] >= 400 and (info['status'] != 404 or error_on_404): 69 err_s = self.error_strings.get(info['status'], '') 70 71 error_msg = "API Error {0}: {1}".format(err_s, self._build_error_message(self.module, info)) 72 73 result = None 74 try: 75 content = resp.read() 76 except AttributeError: 77 content = None 78 79 if content: 80 try: 81 result = json.loads(to_text(content, errors='surrogate_or_strict')) 82 except (getattr(json, 'JSONDecodeError', ValueError)) as e: 83 error_msg += "; Failed to parse API response with error {0}: {1}".format(to_native(e), content) 84 85 if error_msg: 86 self.module.fail_json(msg=error_msg) 87 88 return result, info['status'] 89 90 def build_result(self, result, domain): 91 if result is None: 92 return None 93 94 res = {} 95 for k in self.attribute_map: 96 v = result.get(self.attribute_map[k], None) 97 if v is not None: 98 if k == 'record' and v == '@': 99 v = '' 100 res[k] = v 101 102 res['domain'] = domain 103 104 return res 105 106 def build_results(self, results, domain): 107 if results is None: 108 return [] 109 return [self.build_result(r, domain) for r in results] 110 111 def get_records(self, record, type, domain): 112 url = '/domains/%s/records' % (domain) 113 if record: 114 url += '/%s' % (record) 115 if type: 116 url += '/%s' % (type) 117 118 records, status = self._gandi_api_call(url, error_on_404=False) 119 120 if status == 404: 121 return [] 122 123 if not isinstance(records, list): 124 records = [records] 125 126 # filter by type if record is not set 127 if not record and type: 128 records = [r 129 for r in records 130 if r['rrset_type'] == type] 131 132 return records 133 134 def create_record(self, record, type, values, ttl, domain): 135 url = '/domains/%s/records' % (domain) 136 new_record = { 137 'rrset_name': record, 138 'rrset_type': type, 139 'rrset_values': values, 140 'rrset_ttl': ttl, 141 } 142 record, status = self._gandi_api_call(url, method='POST', payload=new_record) 143 144 if status in (200, 201,): 145 return new_record 146 147 return None 148 149 def update_record(self, record, type, values, ttl, domain): 150 url = '/domains/%s/records/%s/%s' % (domain, record, type) 151 new_record = { 152 'rrset_values': values, 153 'rrset_ttl': ttl, 154 } 155 record = self._gandi_api_call(url, method='PUT', payload=new_record)[0] 156 return record 157 158 def delete_record(self, record, type, domain): 159 url = '/domains/%s/records/%s/%s' % (domain, record, type) 160 161 self._gandi_api_call(url, method='DELETE') 162 163 def delete_dns_record(self, record, type, values, domain): 164 if record == '': 165 record = '@' 166 167 records = self.get_records(record, type, domain) 168 169 if records: 170 cur_record = records[0] 171 172 self.changed = True 173 174 if values is not None and set(cur_record['rrset_values']) != set(values): 175 new_values = set(cur_record['rrset_values']) - set(values) 176 if new_values: 177 # Removing one or more values from a record, we update the record with the remaining values 178 self.update_record(record, type, list(new_values), cur_record['rrset_ttl'], domain) 179 records = self.get_records(record, type, domain) 180 return records[0], self.changed 181 182 if not self.module.check_mode: 183 self.delete_record(record, type, domain) 184 else: 185 cur_record = None 186 187 return None, self.changed 188 189 def ensure_dns_record(self, record, type, ttl, values, domain): 190 if record == '': 191 record = '@' 192 193 records = self.get_records(record, type, domain) 194 195 if records: 196 cur_record = records[0] 197 198 do_update = False 199 if ttl is not None and cur_record['rrset_ttl'] != ttl: 200 do_update = True 201 if values is not None and set(cur_record['rrset_values']) != set(values): 202 do_update = True 203 204 if do_update: 205 if self.module.check_mode: 206 result = dict( 207 rrset_type=type, 208 rrset_name=record, 209 rrset_values=values, 210 rrset_ttl=ttl 211 ) 212 else: 213 self.update_record(record, type, values, ttl, domain) 214 215 records = self.get_records(record, type, domain) 216 result = records[0] 217 self.changed = True 218 return result, self.changed 219 else: 220 return cur_record, self.changed 221 222 if self.module.check_mode: 223 new_record = dict( 224 rrset_type=type, 225 rrset_name=record, 226 rrset_values=values, 227 rrset_ttl=ttl 228 ) 229 result = new_record 230 else: 231 result = self.create_record(record, type, values, ttl, domain) 232 233 self.changed = True 234 return result, self.changed 235