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