1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
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
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14DOCUMENTATION = r'''
15---
16module: cloudflare_dns
17author:
18- Michael Gruener (@mgruener)
19requirements:
20   - python >= 2.6
21version_added: "2.1"
22short_description: Manage Cloudflare DNS records
23description:
24   - "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)"
25options:
26  account_api_token:
27    description:
28    - Account API token.
29    - "You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)"
30    type: str
31    required: true
32  account_email:
33    description:
34    - Account email.
35    type: str
36    required: true
37  algorithm:
38    description:
39    - Algorithm number.
40    - Required for C(type=DS) and C(type=SSHFP) when C(state=present).
41    type: int
42    version_added: '2.7'
43  cert_usage:
44    description:
45    - Certificate usage number.
46    - Required for C(type=TLSA) when C(state=present).
47    type: int
48    choices: [ 0, 1, 2, 3 ]
49    version_added: '2.7'
50  hash_type:
51    description:
52    - Hash type number.
53    - Required for C(type=DS), C(type=SSHFP) and C(type=TLSA) when C(state=present).
54    type: int
55    choices: [ 1, 2 ]
56    version_added: '2.7'
57  key_tag:
58    description:
59    - DNSSEC key tag.
60    - Needed for C(type=DS) when C(state=present).
61    type: int
62    version_added: '2.7'
63  port:
64    description:
65    - Service port.
66    - Required for C(type=SRV) and C(type=TLSA).
67    type: int
68  priority:
69    description:
70    - Record priority.
71    - Required for C(type=MX) and C(type=SRV)
72    default: 1
73  proto:
74    description:
75    - Service protocol. Required for C(type=SRV) and C(type=TLSA).
76    - Common values are TCP and UDP.
77    - Before Ansible 2.6 only TCP and UDP were available.
78    type: str
79  proxied:
80    description:
81    - Proxy through Cloudflare network or just use DNS.
82    type: bool
83    default: no
84    version_added: '2.3'
85  record:
86    description:
87    - Record to add.
88    - Required if C(state=present).
89    - Default is C(@) (e.g. the zone name).
90    type: str
91    default: '@'
92    aliases: [ name ]
93  selector:
94    description:
95    - Selector number.
96    - Required for C(type=TLSA) when C(state=present).
97    choices: [ 0, 1 ]
98    type: int
99    version_added: '2.7'
100  service:
101    description:
102    - Record service.
103    - Required for C(type=SRV)
104  solo:
105    description:
106    - Whether the record should be the only one for that record type and record name.
107    - Only use with C(state=present).
108    - This will delete all other records with the same record name and type.
109    type: bool
110  state:
111    description:
112    - Whether the record(s) should exist or not.
113    type: str
114    choices: [ absent, present ]
115    default: present
116  timeout:
117    description:
118    - Timeout for Cloudflare API calls.
119    type: int
120    default: 30
121  ttl:
122    description:
123    - The TTL to give the new record.
124    - Must be between 120 and 2,147,483,647 seconds, or 1 for automatic.
125    type: int
126    default: 1
127  type:
128    description:
129      - The type of DNS record to create. Required if C(state=present).
130      - C(type=DS), C(type=SSHFP) and C(type=TLSA) added in Ansible 2.7.
131    type: str
132    choices: [ A, AAAA, CNAME, DS, MX, NS, SPF, SRV, SSHFP, TLSA, TXT ]
133  value:
134    description:
135    - The record value.
136    - Required for C(state=present).
137    type: str
138    aliases: [ content ]
139  weight:
140    description:
141    - Service weight.
142    - Required for C(type=SRV).
143    type: int
144    default: 1
145  zone:
146    description:
147    - The name of the Zone to work with (e.g. "example.com").
148    - The Zone must already exist.
149    type: str
150    required: true
151    aliases: [ domain ]
152'''
153
154EXAMPLES = r'''
155- name: Create a test.my.com A record to point to 127.0.0.1
156  cloudflare_dns:
157    zone: my.com
158    record: test
159    type: A
160    value: 127.0.0.1
161    account_email: test@example.com
162    account_api_token: dummyapitoken
163  register: record
164
165- name: Create a my.com CNAME record to example.com
166  cloudflare_dns:
167    zone: my.com
168    type: CNAME
169    value: example.com
170    account_email: test@example.com
171    account_api_token: dummyapitoken
172    state: present
173
174- name: Change its TTL
175  cloudflare_dns:
176    zone: my.com
177    type: CNAME
178    value: example.com
179    ttl: 600
180    account_email: test@example.com
181    account_api_token: dummyapitoken
182    state: present
183
184- name: Delete the record
185  cloudflare_dns:
186    zone: my.com
187    type: CNAME
188    value: example.com
189    account_email: test@example.com
190    account_api_token: dummyapitoken
191    state: absent
192
193- name: create a my.com CNAME record to example.com and proxy through Cloudflare's network
194  cloudflare_dns:
195    zone: my.com
196    type: CNAME
197    value: example.com
198    proxied: yes
199    account_email: test@example.com
200    account_api_token: dummyapitoken
201    state: present
202
203# This deletes all other TXT records named "test.my.com"
204- name: Create TXT record "test.my.com" with value "unique value"
205  cloudflare_dns:
206    domain: my.com
207    record: test
208    type: TXT
209    value: unique value
210    solo: true
211    account_email: test@example.com
212    account_api_token: dummyapitoken
213    state: present
214
215- name: Create an SRV record _foo._tcp.my.com
216  cloudflare_dns:
217    domain: my.com
218    service: foo
219    proto: tcp
220    port: 3500
221    priority: 10
222    weight: 20
223    type: SRV
224    value: fooserver.my.com
225
226- name: Create a SSHFP record login.example.com
227  cloudflare_dns:
228    zone: example.com
229    record: login
230    type: SSHFP
231    algorithm: 4
232    hash_type: 2
233    value: 9dc1d6742696d2f51ca1f1a78b3d16a840f7d111eb9454239e70db31363f33e1
234
235- name: Create a TLSA record _25._tcp.mail.example.com
236  cloudflare_dns:
237    zone: example.com
238    record: mail
239    port: 25
240    proto: tcp
241    type: TLSA
242    cert_usage: 3
243    selector: 1
244    hash_type: 1
245    value: 6b76d034492b493e15a7376fccd08e63befdad0edab8e442562f532338364bf3
246
247- name: Create a DS record for subdomain.example.com
248  cloudflare_dns:
249    zone: example.com
250    record: subdomain
251    type: DS
252    key_tag: 5464
253    algorithm: 8
254    hash_type: 2
255    value: B4EB5AC4467D2DFB3BAF9FB9961DC1B6FED54A58CDFAA3E465081EC86F89BFAB
256'''
257
258RETURN = r'''
259record:
260    description: A dictionary containing the record data.
261    returned: success, except on record deletion
262    type: complex
263    contains:
264        content:
265            description: The record content (details depend on record type).
266            returned: success
267            type: str
268            sample: 192.0.2.91
269        created_on:
270            description: The record creation date.
271            returned: success
272            type: str
273            sample: "2016-03-25T19:09:42.516553Z"
274        data:
275            description: Additional record data.
276            returned: success, if type is SRV, DS, SSHFP or TLSA
277            type: dict
278            sample: {
279                name: "jabber",
280                port: 8080,
281                priority: 10,
282                proto: "_tcp",
283                service: "_xmpp",
284                target: "jabberhost.sample.com",
285                weight: 5,
286            }
287        id:
288            description: The record ID.
289            returned: success
290            type: str
291            sample: f9efb0549e96abcb750de63b38c9576e
292        locked:
293            description: No documentation available.
294            returned: success
295            type: bool
296            sample: False
297        meta:
298            description: No documentation available.
299            returned: success
300            type: dict
301            sample: { auto_added: false }
302        modified_on:
303            description: Record modification date.
304            returned: success
305            type: str
306            sample: "2016-03-25T19:09:42.516553Z"
307        name:
308            description: The record name as FQDN (including _service and _proto for SRV).
309            returned: success
310            type: str
311            sample: www.sample.com
312        priority:
313            description: Priority of the MX record.
314            returned: success, if type is MX
315            type: int
316            sample: 10
317        proxiable:
318            description: Whether this record can be proxied through Cloudflare.
319            returned: success
320            type: bool
321            sample: False
322        proxied:
323            description: Whether the record is proxied through Cloudflare.
324            returned: success
325            type: bool
326            sample: False
327        ttl:
328            description: The time-to-live for the record.
329            returned: success
330            type: int
331            sample: 300
332        type:
333            description: The record type.
334            returned: success
335            type: str
336            sample: A
337        zone_id:
338            description: The ID of the zone containing the record.
339            returned: success
340            type: str
341            sample: abcede0bf9f0066f94029d2e6b73856a
342        zone_name:
343            description: The name of the zone containing the record.
344            returned: success
345            type: str
346            sample: sample.com
347'''
348
349import json
350
351from ansible.module_utils.basic import AnsibleModule
352from ansible.module_utils.six.moves.urllib.parse import urlencode
353from ansible.module_utils._text import to_native, to_text
354from ansible.module_utils.urls import fetch_url
355
356
357def lowercase_string(param):
358    if not isinstance(param, str):
359        return param
360    return param.lower()
361
362
363class CloudflareAPI(object):
364
365    cf_api_endpoint = 'https://api.cloudflare.com/client/v4'
366    changed = False
367
368    def __init__(self, module):
369        self.module = module
370        self.account_api_token = module.params['account_api_token']
371        self.account_email = module.params['account_email']
372        self.algorithm = module.params['algorithm']
373        self.cert_usage = module.params['cert_usage']
374        self.hash_type = module.params['hash_type']
375        self.key_tag = module.params['key_tag']
376        self.port = module.params['port']
377        self.priority = module.params['priority']
378        self.proto = lowercase_string(module.params['proto'])
379        self.proxied = module.params['proxied']
380        self.selector = module.params['selector']
381        self.record = lowercase_string(module.params['record'])
382        self.service = lowercase_string(module.params['service'])
383        self.is_solo = module.params['solo']
384        self.state = module.params['state']
385        self.timeout = module.params['timeout']
386        self.ttl = module.params['ttl']
387        self.type = module.params['type']
388        self.value = module.params['value']
389        self.weight = module.params['weight']
390        self.zone = lowercase_string(module.params['zone'])
391
392        if self.record == '@':
393            self.record = self.zone
394
395        if (self.type in ['CNAME', 'NS', 'MX', 'SRV']) and (self.value is not None):
396            self.value = self.value.rstrip('.').lower()
397
398        if (self.type == 'AAAA') and (self.value is not None):
399            self.value = self.value.lower()
400
401        if (self.type == 'SRV'):
402            if (self.proto is not None) and (not self.proto.startswith('_')):
403                self.proto = '_' + self.proto
404            if (self.service is not None) and (not self.service.startswith('_')):
405                self.service = '_' + self.service
406
407        if (self.type == 'TLSA'):
408            if (self.proto is not None) and (not self.proto.startswith('_')):
409                self.proto = '_' + self.proto
410            if (self.port is not None):
411                self.port = '_' + str(self.port)
412
413        if not self.record.endswith(self.zone):
414            self.record = self.record + '.' + self.zone
415
416        if (self.type == 'DS'):
417            if self.record == self.zone:
418                self.module.fail_json(msg="DS records only apply to subdomains.")
419
420    def _cf_simple_api_call(self, api_call, method='GET', payload=None):
421        headers = {'X-Auth-Email': self.account_email,
422                   'X-Auth-Key': self.account_api_token,
423                   'Content-Type': 'application/json'}
424        data = None
425        if payload:
426            try:
427                data = json.dumps(payload)
428            except Exception as e:
429                self.module.fail_json(msg="Failed to encode payload as JSON: %s " % to_native(e))
430
431        resp, info = fetch_url(self.module,
432                               self.cf_api_endpoint + api_call,
433                               headers=headers,
434                               data=data,
435                               method=method,
436                               timeout=self.timeout)
437
438        if info['status'] not in [200, 304, 400, 401, 403, 429, 405, 415]:
439            self.module.fail_json(msg="Failed API call {0}; got unexpected HTTP code {1}".format(api_call, info['status']))
440
441        error_msg = ''
442        if info['status'] == 401:
443            # Unauthorized
444            error_msg = "API user does not have permission; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
445        elif info['status'] == 403:
446            # Forbidden
447            error_msg = "API request not authenticated; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
448        elif info['status'] == 429:
449            # Too many requests
450            error_msg = "API client is rate limited; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
451        elif info['status'] == 405:
452            # Method not allowed
453            error_msg = "API incorrect HTTP method provided; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
454        elif info['status'] == 415:
455            # Unsupported Media Type
456            error_msg = "API request is not valid JSON; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
457        elif info['status'] == 400:
458            # Bad Request
459            error_msg = "API bad request; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
460
461        result = None
462        try:
463            content = resp.read()
464        except AttributeError:
465            if info['body']:
466                content = info['body']
467            else:
468                error_msg += "; The API response was empty"
469
470        if content:
471            try:
472                result = json.loads(to_text(content, errors='surrogate_or_strict'))
473            except (getattr(json, 'JSONDecodeError', ValueError)) as e:
474                error_msg += "; Failed to parse API response with error {0}: {1}".format(to_native(e), content)
475
476        # Without a valid/parsed JSON response no more error processing can be done
477        if result is None:
478            self.module.fail_json(msg=error_msg)
479
480        if not result['success']:
481            error_msg += "; Error details: "
482            for error in result['errors']:
483                error_msg += "code: {0}, error: {1}; ".format(error['code'], error['message'])
484                if 'error_chain' in error:
485                    for chain_error in error['error_chain']:
486                        error_msg += "code: {0}, error: {1}; ".format(chain_error['code'], chain_error['message'])
487            self.module.fail_json(msg=error_msg)
488
489        return result, info['status']
490
491    def _cf_api_call(self, api_call, method='GET', payload=None):
492        result, status = self._cf_simple_api_call(api_call, method, payload)
493
494        data = result['result']
495
496        if 'result_info' in result:
497            pagination = result['result_info']
498            if pagination['total_pages'] > 1:
499                next_page = int(pagination['page']) + 1
500                parameters = ['page={0}'.format(next_page)]
501                # strip "page" parameter from call parameters (if there are any)
502                if '?' in api_call:
503                    raw_api_call, query = api_call.split('?', 1)
504                    parameters += [param for param in query.split('&') if not param.startswith('page')]
505                else:
506                    raw_api_call = api_call
507                while next_page <= pagination['total_pages']:
508                    raw_api_call += '?' + '&'.join(parameters)
509                    result, status = self._cf_simple_api_call(raw_api_call, method, payload)
510                    data += result['result']
511                    next_page += 1
512
513        return data, status
514
515    def _get_zone_id(self, zone=None):
516        if not zone:
517            zone = self.zone
518
519        zones = self.get_zones(zone)
520        if len(zones) > 1:
521            self.module.fail_json(msg="More than one zone matches {0}".format(zone))
522
523        if len(zones) < 1:
524            self.module.fail_json(msg="No zone found with name {0}".format(zone))
525
526        return zones[0]['id']
527
528    def get_zones(self, name=None):
529        if not name:
530            name = self.zone
531        param = ''
532        if name:
533            param = '?' + urlencode({'name': name})
534        zones, status = self._cf_api_call('/zones' + param)
535        return zones
536
537    def get_dns_records(self, zone_name=None, type=None, record=None, value=''):
538        if not zone_name:
539            zone_name = self.zone
540        if not type:
541            type = self.type
542        if not record:
543            record = self.record
544        # necessary because None as value means to override user
545        # set module value
546        if (not value) and (value is not None):
547            value = self.value
548
549        zone_id = self._get_zone_id()
550        api_call = '/zones/{0}/dns_records'.format(zone_id)
551        query = {}
552        if type:
553            query['type'] = type
554        if record:
555            query['name'] = record
556        if value:
557            query['content'] = value
558        if query:
559            api_call += '?' + urlencode(query)
560
561        records, status = self._cf_api_call(api_call)
562        return records
563
564    def delete_dns_records(self, **kwargs):
565        params = {}
566        for param in ['port', 'proto', 'service', 'solo', 'type', 'record', 'value', 'weight', 'zone',
567                      'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']:
568            if param in kwargs:
569                params[param] = kwargs[param]
570            else:
571                params[param] = getattr(self, param)
572
573        records = []
574        content = params['value']
575        search_record = params['record']
576        if params['type'] == 'SRV':
577            if not (params['value'] is None or params['value'] == ''):
578                content = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
579            search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
580        elif params['type'] == 'DS':
581            if not (params['value'] is None or params['value'] == ''):
582                content = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
583        elif params['type'] == 'SSHFP':
584            if not (params['value'] is None or params['value'] == ''):
585                content = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
586        elif params['type'] == 'TLSA':
587            if not (params['value'] is None or params['value'] == ''):
588                content = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']
589            search_record = params['port'] + '.' + params['proto'] + '.' + params['record']
590        if params['solo']:
591            search_value = None
592        else:
593            search_value = content
594
595        records = self.get_dns_records(params['zone'], params['type'], search_record, search_value)
596
597        for rr in records:
598            if params['solo']:
599                if not ((rr['type'] == params['type']) and (rr['name'] == search_record) and (rr['content'] == content)):
600                    self.changed = True
601                    if not self.module.check_mode:
602                        result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
603            else:
604                self.changed = True
605                if not self.module.check_mode:
606                    result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
607        return self.changed
608
609    def ensure_dns_record(self, **kwargs):
610        params = {}
611        for param in ['port', 'priority', 'proto', 'proxied', 'service', 'ttl', 'type', 'record', 'value', 'weight', 'zone',
612                      'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']:
613            if param in kwargs:
614                params[param] = kwargs[param]
615            else:
616                params[param] = getattr(self, param)
617
618        search_value = params['value']
619        search_record = params['record']
620        new_record = None
621        if (params['type'] is None) or (params['record'] is None):
622            self.module.fail_json(msg="You must provide a type and a record to create a new record")
623
624        if (params['type'] in ['A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SPF']):
625            if not params['value']:
626                self.module.fail_json(msg="You must provide a non-empty value to create this record type")
627
628            # there can only be one CNAME per record
629            # ignoring the value when searching for existing
630            # CNAME records allows us to update the value if it
631            # changes
632            if params['type'] == 'CNAME':
633                search_value = None
634
635            new_record = {
636                "type": params['type'],
637                "name": params['record'],
638                "content": params['value'],
639                "ttl": params['ttl']
640            }
641
642        if (params['type'] in ['A', 'AAAA', 'CNAME']):
643            new_record["proxied"] = params["proxied"]
644
645        if params['type'] == 'MX':
646            for attr in [params['priority'], params['value']]:
647                if (attr is None) or (attr == ''):
648                    self.module.fail_json(msg="You must provide priority and a value to create this record type")
649            new_record = {
650                "type": params['type'],
651                "name": params['record'],
652                "content": params['value'],
653                "priority": params['priority'],
654                "ttl": params['ttl']
655            }
656
657        if params['type'] == 'SRV':
658            for attr in [params['port'], params['priority'], params['proto'], params['service'], params['weight'], params['value']]:
659                if (attr is None) or (attr == ''):
660                    self.module.fail_json(msg="You must provide port, priority, proto, service, weight and a value to create this record type")
661            srv_data = {
662                "target": params['value'],
663                "port": params['port'],
664                "weight": params['weight'],
665                "priority": params['priority'],
666                "name": params['record'][:-len('.' + params['zone'])],
667                "proto": params['proto'],
668                "service": params['service']
669            }
670            new_record = {"type": params['type'], "ttl": params['ttl'], 'data': srv_data}
671            search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
672            search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
673
674        if params['type'] == 'DS':
675            for attr in [params['key_tag'], params['algorithm'], params['hash_type'], params['value']]:
676                if (attr is None) or (attr == ''):
677                    self.module.fail_json(msg="You must provide key_tag, algorithm, hash_type and a value to create this record type")
678            ds_data = {
679                "key_tag": params['key_tag'],
680                "algorithm": params['algorithm'],
681                "digest_type": params['hash_type'],
682                "digest": params['value'],
683            }
684            new_record = {
685                "type": params['type'],
686                "name": params['record'],
687                'data': ds_data,
688                "ttl": params['ttl'],
689            }
690            search_value = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
691
692        if params['type'] == 'SSHFP':
693            for attr in [params['algorithm'], params['hash_type'], params['value']]:
694                if (attr is None) or (attr == ''):
695                    self.module.fail_json(msg="You must provide algorithm, hash_type and a value to create this record type")
696            sshfp_data = {
697                "fingerprint": params['value'],
698                "type": params['hash_type'],
699                "algorithm": params['algorithm'],
700            }
701            new_record = {
702                "type": params['type'],
703                "name": params['record'],
704                'data': sshfp_data,
705                "ttl": params['ttl'],
706            }
707            search_value = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
708
709        if params['type'] == 'TLSA':
710            for attr in [params['port'], params['proto'], params['cert_usage'], params['selector'], params['hash_type'], params['value']]:
711                if (attr is None) or (attr == ''):
712                    self.module.fail_json(msg="You must provide port, proto, cert_usage, selector, hash_type and a value to create this record type")
713            search_record = params['port'] + '.' + params['proto'] + '.' + params['record']
714            tlsa_data = {
715                "usage": params['cert_usage'],
716                "selector": params['selector'],
717                "matching_type": params['hash_type'],
718                "certificate": params['value'],
719            }
720            new_record = {
721                "type": params['type'],
722                "name": search_record,
723                'data': tlsa_data,
724                "ttl": params['ttl'],
725            }
726            search_value = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']
727
728        zone_id = self._get_zone_id(params['zone'])
729        records = self.get_dns_records(params['zone'], params['type'], search_record, search_value)
730        # in theory this should be impossible as cloudflare does not allow
731        # the creation of duplicate records but lets cover it anyways
732        if len(records) > 1:
733            self.module.fail_json(msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!")
734        # record already exists, check if it must be updated
735        if len(records) == 1:
736            cur_record = records[0]
737            do_update = False
738            if (params['ttl'] is not None) and (cur_record['ttl'] != params['ttl']):
739                do_update = True
740            if (params['priority'] is not None) and ('priority' in cur_record) and (cur_record['priority'] != params['priority']):
741                do_update = True
742            if ('proxied' in new_record) and ('proxied' in cur_record) and (cur_record['proxied'] != params['proxied']):
743                do_update = True
744            if ('data' in new_record) and ('data' in cur_record):
745                if (cur_record['data'] != new_record['data']):
746                    do_update = True
747            if (params['type'] == 'CNAME') and (cur_record['content'] != new_record['content']):
748                do_update = True
749            if do_update:
750                if self.module.check_mode:
751                    result = new_record
752                else:
753                    result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(zone_id, records[0]['id']), 'PUT', new_record)
754                self.changed = True
755                return result, self.changed
756            else:
757                return records, self.changed
758        if self.module.check_mode:
759            result = new_record
760        else:
761            result, info = self._cf_api_call('/zones/{0}/dns_records'.format(zone_id), 'POST', new_record)
762        self.changed = True
763        return result, self.changed
764
765
766def main():
767    module = AnsibleModule(
768        argument_spec=dict(
769            account_api_token=dict(type='str', required=True, no_log=True),
770            account_email=dict(type='str', required=True),
771            algorithm=dict(type='int'),
772            cert_usage=dict(type='int', choices=[0, 1, 2, 3]),
773            hash_type=dict(type='int', choices=[1, 2]),
774            key_tag=dict(type='int'),
775            port=dict(type='int'),
776            priority=dict(type='int', default=1),
777            proto=dict(type='str'),
778            proxied=dict(type='bool', default=False),
779            record=dict(type='str', default='@', aliases=['name']),
780            selector=dict(type='int', choices=[0, 1]),
781            service=dict(type='str'),
782            solo=dict(type='bool'),
783            state=dict(type='str', default='present', choices=['absent', 'present']),
784            timeout=dict(type='int', default=30),
785            ttl=dict(type='int', default=1),
786            type=dict(type='str', choices=['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'SPF', 'SRV', 'SSHFP', 'TLSA', 'TXT']),
787            value=dict(type='str', aliases=['content']),
788            weight=dict(type='int', default=1),
789            zone=dict(type='str', required=True, aliases=['domain']),
790        ),
791        supports_check_mode=True,
792        required_if=[
793            ('state', 'present', ['record', 'type', 'value']),
794            ('state', 'absent', ['record']),
795            ('type', 'SRV', ['proto', 'service']),
796            ('type', 'TLSA', ['proto', 'port']),
797        ],
798    )
799
800    if module.params['type'] == 'SRV':
801        if not ((module.params['weight'] is not None and module.params['port'] is not None
802                 and not (module.params['value'] is None or module.params['value'] == ''))
803                or (module.params['weight'] is None and module.params['port'] is None
804                    and (module.params['value'] is None or module.params['value'] == ''))):
805            module.fail_json(msg="For SRV records the params weight, port and value all need to be defined, or not at all.")
806
807    if module.params['type'] == 'SSHFP':
808        if not ((module.params['algorithm'] is not None and module.params['hash_type'] is not None
809                 and not (module.params['value'] is None or module.params['value'] == ''))
810                or (module.params['algorithm'] is None and module.params['hash_type'] is None
811                    and (module.params['value'] is None or module.params['value'] == ''))):
812            module.fail_json(msg="For SSHFP records the params algorithm, hash_type and value all need to be defined, or not at all.")
813
814    if module.params['type'] == 'TLSA':
815        if not ((module.params['cert_usage'] is not None and module.params['selector'] is not None and module.params['hash_type'] is not None
816                 and not (module.params['value'] is None or module.params['value'] == ''))
817                or (module.params['cert_usage'] is None and module.params['selector'] is None and module.params['hash_type'] is None
818                    and (module.params['value'] is None or module.params['value'] == ''))):
819            module.fail_json(msg="For TLSA records the params cert_usage, selector, hash_type and value all need to be defined, or not at all.")
820
821    if module.params['type'] == 'DS':
822        if not ((module.params['key_tag'] is not None and module.params['algorithm'] is not None and module.params['hash_type'] is not None
823                 and not (module.params['value'] is None or module.params['value'] == ''))
824                or (module.params['key_tag'] is None and module.params['algorithm'] is None and module.params['hash_type'] is None
825                    and (module.params['value'] is None or module.params['value'] == ''))):
826            module.fail_json(msg="For DS records the params key_tag, algorithm, hash_type and value all need to be defined, or not at all.")
827
828    changed = False
829    cf_api = CloudflareAPI(module)
830
831    # sanity checks
832    if cf_api.is_solo and cf_api.state == 'absent':
833        module.fail_json(msg="solo=true can only be used with state=present")
834
835    # perform add, delete or update (only the TTL can be updated) of one or
836    # more records
837    if cf_api.state == 'present':
838        # delete all records matching record name + type
839        if cf_api.is_solo:
840            changed = cf_api.delete_dns_records(solo=cf_api.is_solo)
841        result, changed = cf_api.ensure_dns_record()
842        if isinstance(result, list):
843            module.exit_json(changed=changed, result={'record': result[0]})
844
845        module.exit_json(changed=changed, result={'record': result})
846    else:
847        # force solo to False, just to be sure
848        changed = cf_api.delete_dns_records(solo=False)
849        module.exit_json(changed=changed)
850
851
852if __name__ == '__main__':
853    main()
854