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