1#!/usr/bin/python
2#
3# Copyright: Ansible Project
4#
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {
12    'metadata_version': '1.1',
13    'status': ['preview'],
14    'supported_by': 'community'
15}
16
17
18DOCUMENTATION = '''
19---
20module: dnsimple
21version_added: "1.6"
22short_description: Interface with dnsimple.com (a DNS hosting service)
23description:
24   - "Manages domains and records via the DNSimple API, see the docs: U(http://developer.dnsimple.com/)."
25notes:
26  - DNSimple API v1 is deprecated. Please install dnsimple-python>=1.0.0 which uses v2 API.
27options:
28  account_email:
29    description:
30      - Account email. If omitted, the environment variables C(DNSIMPLE_EMAIL) and C(DNSIMPLE_API_TOKEN) will be looked for.
31      - "If those aren't found, a C(.dnsimple) file will be looked for, see: U(https://github.com/mikemaccana/dnsimple-python#getting-started)."
32    type: str
33  account_api_token:
34    description:
35      - Account API token. See I(account_email) for more information.
36    type: str
37  domain:
38    description:
39      - Domain to work with. Can be the domain name (e.g. "mydomain.com") or the numeric ID of the domain in DNSimple.
40      - If omitted, a list of domains will be returned.
41      - If domain is present but the domain doesn't exist, it will be created.
42    type: str
43  record:
44    description:
45      - Record to add, if blank a record for the domain will be created, supports the wildcard (*).
46    type: str
47  record_ids:
48    description:
49      - List of records to ensure they either exist or do not exist.
50    type: list
51  type:
52    description:
53      - The type of DNS record to create.
54    choices: [ 'A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO', 'POOL' ]
55    type: str
56  ttl:
57    description:
58      - The TTL to give the new record in seconds.
59    default: 3600
60    type: int
61  value:
62    description:
63      - Record value.
64      - Must be specified when trying to ensure a record exists.
65    type: str
66  priority:
67    description:
68      - Record priority.
69    type: int
70  state:
71    description:
72      - whether the record should exist or not.
73    choices: [ 'present', 'absent' ]
74    default: present
75    type: str
76  solo:
77    description:
78      - Whether the record should be the only one for that record type and record name.
79      - Only use with C(state) is set to C(present) on a record.
80    type: 'bool'
81    default: no
82requirements:
83  - "dnsimple >= 1.0.0"
84author: "Alex Coomans (@drcapulet)"
85'''
86
87EXAMPLES = '''
88- name: Authenticate using email and API token and fetch all domains
89  dnsimple:
90    account_email: test@example.com
91    account_api_token: dummyapitoken
92  delegate_to: localhost
93
94- name: Fetch my.com domain records
95  dnsimple:
96    domain: my.com
97    state: present
98  delegate_to: localhost
99  register: records
100
101- name: Delete a domain
102  dnsimple:
103    domain: my.com
104    state: absent
105  delegate_to: localhost
106
107- name: Create a test.my.com A record to point to 127.0.0.1
108  dnsimple:
109    domain: my.com
110    record: test
111    type: A
112    value: 127.0.0.1
113  delegate_to: localhost
114  register: record
115
116- name: Delete record using record_ids
117  dnsimple:
118    domain: my.com
119    record_ids: '{{ record["id"] }}'
120    state: absent
121  delegate_to: localhost
122
123- name: Create a my.com CNAME record to example.com
124  dnsimple:
125    domain: my.com
126    record: ''
127    type: CNAME
128    value: example.com
129    state: present
130  delegate_to: localhost
131
132- name: change TTL value for a record
133  dnsimple:
134    domain: my.com
135    record: ''
136    type: CNAME
137    value: example.com
138    ttl: 600
139    state: present
140  delegate_to: localhost
141
142- name: Delete the record
143  dnsimple:
144    domain: my.com
145    record: ''
146    type: CNAME
147    value: example.com
148    state: absent
149  delegate_to: localhost
150'''
151
152RETURN = r"""# """
153
154import os
155import traceback
156from distutils.version import LooseVersion
157
158DNSIMPLE_IMP_ERR = None
159try:
160    from dnsimple import DNSimple
161    from dnsimple.dnsimple import __version__ as dnsimple_version
162    from dnsimple.dnsimple import DNSimpleException
163    HAS_DNSIMPLE = True
164except ImportError:
165    DNSIMPLE_IMP_ERR = traceback.format_exc()
166    HAS_DNSIMPLE = False
167
168from ansible.module_utils.basic import AnsibleModule, missing_required_lib
169
170
171def main():
172    module = AnsibleModule(
173        argument_spec=dict(
174            account_email=dict(type='str'),
175            account_api_token=dict(type='str', no_log=True),
176            domain=dict(type='str'),
177            record=dict(type='str'),
178            record_ids=dict(type='list'),
179            type=dict(type='str', choices=['A', 'ALIAS', 'CNAME', 'MX', 'SPF', 'URL', 'TXT', 'NS', 'SRV', 'NAPTR', 'PTR', 'AAAA', 'SSHFP', 'HINFO',
180                                           'POOL']),
181            ttl=dict(type='int', default=3600),
182            value=dict(type='str'),
183            priority=dict(type='int'),
184            state=dict(type='str', choices=['present', 'absent'], default='present'),
185            solo=dict(type='bool', default=False),
186        ),
187        required_together=[
188            ['record', 'value']
189        ],
190        supports_check_mode=True,
191    )
192
193    if not HAS_DNSIMPLE:
194        module.fail_json(msg=missing_required_lib('dnsimple'), exception=DNSIMPLE_IMP_ERR)
195
196    if LooseVersion(dnsimple_version) < LooseVersion('1.0.0'):
197        module.fail_json(msg="Current version of dnsimple Python module [%s] uses 'v1' API which is deprecated."
198                             " Please upgrade to version 1.0.0 and above to use dnsimple 'v2' API." % dnsimple_version)
199
200    account_email = module.params.get('account_email')
201    account_api_token = module.params.get('account_api_token')
202    domain = module.params.get('domain')
203    record = module.params.get('record')
204    record_ids = module.params.get('record_ids')
205    record_type = module.params.get('type')
206    ttl = module.params.get('ttl')
207    value = module.params.get('value')
208    priority = module.params.get('priority')
209    state = module.params.get('state')
210    is_solo = module.params.get('solo')
211
212    if account_email and account_api_token:
213        client = DNSimple(email=account_email, api_token=account_api_token)
214    elif os.environ.get('DNSIMPLE_EMAIL') and os.environ.get('DNSIMPLE_API_TOKEN'):
215        client = DNSimple(email=os.environ.get('DNSIMPLE_EMAIL'), api_token=os.environ.get('DNSIMPLE_API_TOKEN'))
216    else:
217        client = DNSimple()
218
219    try:
220        # Let's figure out what operation we want to do
221
222        # No domain, return a list
223        if not domain:
224            domains = client.domains()
225            module.exit_json(changed=False, result=[d['domain'] for d in domains])
226
227        # Domain & No record
228        if domain and record is None and not record_ids:
229            domains = [d['domain'] for d in client.domains()]
230            if domain.isdigit():
231                dr = next((d for d in domains if d['id'] == int(domain)), None)
232            else:
233                dr = next((d for d in domains if d['name'] == domain), None)
234            if state == 'present':
235                if dr:
236                    module.exit_json(changed=False, result=dr)
237                else:
238                    if module.check_mode:
239                        module.exit_json(changed=True)
240                    else:
241                        module.exit_json(changed=True, result=client.add_domain(domain)['domain'])
242
243            # state is absent
244            else:
245                if dr:
246                    if not module.check_mode:
247                        client.delete(domain)
248                    module.exit_json(changed=True)
249                else:
250                    module.exit_json(changed=False)
251
252        # need the not none check since record could be an empty string
253        if domain and record is not None:
254            records = [r['record'] for r in client.records(str(domain), params={'name': record})]
255
256            if not record_type:
257                module.fail_json(msg="Missing the record type")
258
259            if not value:
260                module.fail_json(msg="Missing the record value")
261
262            rr = next((r for r in records if r['name'] == record and r['type'] == record_type and r['content'] == value), None)
263
264            if state == 'present':
265                changed = False
266                if is_solo:
267                    # delete any records that have the same name and record type
268                    same_type = [r['id'] for r in records if r['name'] == record and r['type'] == record_type]
269                    if rr:
270                        same_type = [rid for rid in same_type if rid != rr['id']]
271                    if same_type:
272                        if not module.check_mode:
273                            for rid in same_type:
274                                client.delete_record(str(domain), rid)
275                        changed = True
276                if rr:
277                    # check if we need to update
278                    if rr['ttl'] != ttl or rr['priority'] != priority:
279                        data = {}
280                        if ttl:
281                            data['ttl'] = ttl
282                        if priority:
283                            data['priority'] = priority
284                        if module.check_mode:
285                            module.exit_json(changed=True)
286                        else:
287                            module.exit_json(changed=True, result=client.update_record(str(domain), str(rr['id']), data)['record'])
288                    else:
289                        module.exit_json(changed=changed, result=rr)
290                else:
291                    # create it
292                    data = {
293                        'name': record,
294                        'type': record_type,
295                        'content': value,
296                    }
297                    if ttl:
298                        data['ttl'] = ttl
299                    if priority:
300                        data['priority'] = priority
301                    if module.check_mode:
302                        module.exit_json(changed=True)
303                    else:
304                        module.exit_json(changed=True, result=client.add_record(str(domain), data)['record'])
305
306            # state is absent
307            else:
308                if rr:
309                    if not module.check_mode:
310                        client.delete_record(str(domain), rr['id'])
311                    module.exit_json(changed=True)
312                else:
313                    module.exit_json(changed=False)
314
315        # Make sure these record_ids either all exist or none
316        if domain and record_ids:
317            current_records = [str(r['record']['id']) for r in client.records(str(domain))]
318            wanted_records = [str(r) for r in record_ids]
319            if state == 'present':
320                difference = list(set(wanted_records) - set(current_records))
321                if difference:
322                    module.fail_json(msg="Missing the following records: %s" % difference)
323                else:
324                    module.exit_json(changed=False)
325
326            # state is absent
327            else:
328                difference = list(set(wanted_records) & set(current_records))
329                if difference:
330                    if not module.check_mode:
331                        for rid in difference:
332                            client.delete_record(str(domain), rid)
333                    module.exit_json(changed=True)
334                else:
335                    module.exit_json(changed=False)
336
337    except DNSimpleException as e:
338        module.fail_json(msg="Unable to contact DNSimple: %s" % e.message)
339
340    module.fail_json(msg="Unknown what you wanted me to do")
341
342
343if __name__ == '__main__':
344    main()
345