#!/usr/bin/python # Copyright (c) 2016 Michael Gruener # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" module: cloudflare_dns author: - Michael Gruener (@mgruener) short_description: Manage Cloudflare DNS records description: - 'Manages DNS records using the Cloudflare API, see the docs: U(https://api.cloudflare.com/).' extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: full diff_mode: support: none options: api_token: description: - API token. - Required for API token authentication. - "You can obtain your API token from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)." - Can be specified in E(CLOUDFLARE_TOKEN) environment variable since community.general 2.0.0. type: str version_added: '0.2.0' account_api_key: description: - Account API key. - Required for API keys authentication. - "You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)." type: str aliases: [account_api_token] account_email: description: - Account email. Required for API keys authentication. type: str algorithm: description: - Algorithm number. - Required for O(type=DS) and O(type=SSHFP) when O(state=present). type: int cert_usage: description: - Certificate usage number. - Required for O(type=TLSA) when O(state=present). type: int choices: [0, 1, 2, 3] comment: description: - Comments or notes about the DNS record. type: str version_added: 10.1.0 flag: description: - Issuer Critical Flag. - Required for O(type=CAA) when O(state=present). type: int choices: [0, 1] version_added: 8.0.0 tag: description: - CAA issue restriction. - Required for O(type=CAA) when O(state=present). type: str choices: [issue, issuewild, iodef] version_added: 8.0.0 hash_type: description: - Hash type number. - Required for O(type=DS), O(type=SSHFP) and O(type=TLSA) when O(state=present). type: int choices: [1, 2] key_tag: description: - DNSSEC key tag. - Needed for O(type=DS) when O(state=present). type: int port: description: - Service port. - Required for O(type=SRV) and O(type=TLSA). type: int priority: description: - Record priority. - Required for O(type=MX) and O(type=SRV). default: 1 type: int proto: description: - Service protocol. Required for O(type=SRV) and O(type=TLSA). - Common values are TCP and UDP. type: str proxied: description: - Proxy through Cloudflare network or just use DNS. type: bool default: false record: description: - Record to add. - Required if O(state=present). - Default is V(@) (that is, the zone name). type: str default: '@' aliases: [name] selector: description: - Selector number. - Required for O(type=TLSA) when O(state=present). choices: [0, 1] type: int service: description: - Record service. - Required for O(type=SRV). type: str solo: description: - Whether the record should be the only one for that record type and record name. - Only use with O(state=present). - This deletes all other records with the same record name and type. type: bool state: description: - Whether the record(s) should exist or not. type: str choices: [absent, present] default: present tags: description: - Custom tags for the DNS record. type: list elements: str version_added: 10.1.0 timeout: description: - Timeout for Cloudflare API calls. type: int default: 30 ttl: description: - The TTL to give the new record. - Must be between V(120) and V(2,147,483,647) seconds, or V(1) for automatic. type: int default: 1 type: description: - The type of DNS record to create. Required if O(state=present). - Support for V(SPF) has been removed from community.general 9.0.0 since that record type is no longer supported by CloudFlare. - Support for V(PTR) has been added in community.general 11.1.0. type: str choices: [A, AAAA, CNAME, DS, MX, NS, SRV, SSHFP, TLSA, CAA, TXT, PTR] value: description: - The record value. - Required for O(state=present). type: str aliases: [content] weight: description: - Service weight. - Required for O(type=SRV). type: int default: 1 zone: description: - The name of the Zone to work with (for example V(example.com)). - The Zone must already exist. type: str required: true aliases: [domain] """ EXAMPLES = r""" - name: Create a test.example.net A record to point to 127.0.0.1 community.general.cloudflare_dns: zone: example.net record: test type: A value: 127.0.0.1 account_email: test@example.com account_api_key: dummyapitoken register: record - name: Create a record using api token community.general.cloudflare_dns: zone: example.net record: test type: A value: 127.0.0.1 api_token: dummyapitoken - name: Create a record with comment and tags community.general.cloudflare_dns: zone: example.net record: test type: A value: 127.0.0.1 comment: Local test website tags: - test - local api_token: dummyapitoken - name: Create a example.net CNAME record to example.com community.general.cloudflare_dns: zone: example.net type: CNAME value: example.com account_email: test@example.com account_api_key: dummyapitoken state: present - name: Change its TTL community.general.cloudflare_dns: zone: example.net type: CNAME value: example.com ttl: 600 account_email: test@example.com account_api_key: dummyapitoken state: present - name: Delete the record community.general.cloudflare_dns: zone: example.net type: CNAME value: example.com account_email: test@example.com account_api_key: dummyapitoken state: absent - name: Create a example.net CNAME record to example.com and proxy through Cloudflare's network community.general.cloudflare_dns: zone: example.net type: CNAME value: example.com proxied: true account_email: test@example.com account_api_key: dummyapitoken state: present # This deletes all other TXT records named "test.example.net" - name: Create TXT record "test.example.net" with value "unique value" community.general.cloudflare_dns: domain: example.net record: test type: TXT value: unique value solo: true account_email: test@example.com account_api_key: dummyapitoken state: present - name: Create an SRV record _foo._tcp.example.net community.general.cloudflare_dns: domain: example.net service: foo proto: tcp port: 3500 priority: 10 weight: 20 type: SRV value: fooserver.example.net - name: Create a SSHFP record login.example.com community.general.cloudflare_dns: zone: example.com record: login type: SSHFP algorithm: 4 hash_type: 2 value: 9dc1d6742696d2f51ca1f1a78b3d16a840f7d111eb9454239e70db31363f33e1 - name: Create a TLSA record _25._tcp.mail.example.com community.general.cloudflare_dns: zone: example.com record: mail port: 25 proto: tcp type: TLSA cert_usage: 3 selector: 1 hash_type: 1 value: 6b76d034492b493e15a7376fccd08e63befdad0edab8e442562f532338364bf3 - name: Create a CAA record subdomain.example.com community.general.cloudflare_dns: zone: example.com record: subdomain type: CAA flag: 0 tag: issue value: ca.example.com - name: Create a DS record for subdomain.example.com community.general.cloudflare_dns: zone: example.com record: subdomain type: DS key_tag: 5464 algorithm: 8 hash_type: 2 value: B4EB5AC4467D2DFB3BAF9FB9961DC1B6FED54A58CDFAA3E465081EC86F89BFAB - name: Create PTR record "1.2.0.192.in-addr.arpa" with value "test.example.com" community.general.cloudflare_dns: zone: 2.0.192.in-addr.arpa record: 1 type: PTR value: test.example.com state: present """ RETURN = r""" record: description: A dictionary containing the record data. returned: success, except on record deletion type: complex contains: comment: description: Comments or notes about the DNS record. returned: success type: str sample: Domain verification record version_added: 10.1.0 comment_modified_on: description: When the record comment was last modified. Omitted if there is no comment. returned: success type: str sample: "2024-01-01T05:20:00.12345Z" version_added: 10.1.0 content: description: The record content (details depend on record type). returned: success type: str sample: 192.0.2.91 created_on: description: The record creation date. returned: success type: str sample: "2016-03-25T19:09:42.516553Z" data: description: Additional record data. returned: success, if type is SRV, DS, SSHFP TLSA or CAA type: dict sample: { "name": "jabber", "port": 8080, "priority": 10, "proto": "_tcp", "service": "_xmpp", "target": "jabberhost.sample.com", "weight": 5 } id: description: The record ID. returned: success type: str sample: f9efb0549e96abcb750de63b38c9576e locked: description: No documentation available. returned: success type: bool sample: false meta: description: Extra Cloudflare-specific information about the record. returned: success type: dict sample: {"auto_added": false} modified_on: description: Record modification date. returned: success type: str sample: "2016-03-25T19:09:42.516553Z" name: description: The record name as FQDN (including _service and _proto for SRV). returned: success type: str sample: www.sample.com priority: description: Priority of the MX record. returned: success, if type is MX type: int sample: 10 proxiable: description: Whether this record can be proxied through Cloudflare. returned: success type: bool sample: false proxied: description: Whether the record is proxied through Cloudflare. returned: success type: bool sample: false tags: description: Custom tags for the DNS record. returned: success type: list elements: str sample: ["production", "app"] version_added: 10.1.0 tags_modified_on: description: When the record tags were last modified. Omitted if there are no tags. returned: success type: str sample: "2025-01-01T05:20:00.12345Z" version_added: 10.1.0 ttl: description: The time-to-live for the record. returned: success type: int sample: 300 type: description: The record type. returned: success type: str sample: A zone_id: description: The ID of the zone containing the record. returned: success type: str sample: abcede0bf9f0066f94029d2e6b73856a zone_name: description: The name of the zone containing the record. returned: success type: str sample: sample.com """ import json from urllib.parse import urlencode from ansible.module_utils.basic import AnsibleModule, env_fallback from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.urls import fetch_url def lowercase_string(param): return param.lower() if isinstance(param, str) else param def join_str(sep, *args): return sep.join([str(arg) for arg in args]) class CloudflareAPI: cf_api_endpoint = "https://api.cloudflare.com/client/v4" changed = False def __init__(self, module): self.module = module self.api_token = module.params["api_token"] self.account_api_key = module.params["account_api_key"] self.account_email = module.params["account_email"] self.algorithm = module.params["algorithm"] self.cert_usage = module.params["cert_usage"] self.comment = module.params["comment"] self.hash_type = module.params["hash_type"] self.flag = module.params["flag"] self.tag = module.params["tag"] self.tags = module.params["tags"] self.key_tag = module.params["key_tag"] self.port = module.params["port"] self.priority = module.params["priority"] self.proto = lowercase_string(module.params["proto"]) self.proxied = module.params["proxied"] self.selector = module.params["selector"] self.record = lowercase_string(module.params["record"]) self.service = lowercase_string(module.params["service"]) self.is_solo = module.params["solo"] self.state = module.params["state"] self.timeout = module.params["timeout"] self.ttl = module.params["ttl"] self.type = module.params["type"] self.value = module.params["value"] self.weight = module.params["weight"] self.zone = lowercase_string(module.params["zone"]) if self.record == "@": self.record = self.zone if (self.type in ["CNAME", "NS", "MX", "SRV"]) and (self.value is not None): self.value = self.value.rstrip(".").lower() if (self.type == "AAAA") and (self.value is not None): self.value = self.value.lower() if self.type == "SRV": if (self.proto is not None) and (not self.proto.startswith("_")): self.proto = f"_{self.proto}" if (self.service is not None) and (not self.service.startswith("_")): self.service = f"_{self.service}" if self.type == "TLSA": if (self.proto is not None) and (not self.proto.startswith("_")): self.proto = f"_{self.proto}" if self.port is not None: self.port = f"_{self.port}" if not self.record.endswith(self.zone): self.record = join_str(".", self.record, self.zone) if self.type == "DS": if self.record == self.zone: self.module.fail_json(msg="DS records only apply to subdomains.") def _cf_simple_api_call(self, api_call, method="GET", payload=None): if self.api_token: headers = { "Authorization": f"Bearer {self.api_token}", "Content-Type": "application/json", } else: headers = { "X-Auth-Email": self.account_email, "X-Auth-Key": self.account_api_key, "Content-Type": "application/json", } data = None if payload: try: data = json.dumps(payload) except Exception as e: self.module.fail_json(msg=f"Failed to encode payload as JSON: {e} ") resp, info = fetch_url( self.module, self.cf_api_endpoint + api_call, headers=headers, data=data, method=method, timeout=self.timeout, ) if info["status"] not in [200, 304, 400, 401, 403, 429, 405, 415]: self.module.fail_json( msg=f"Failed API call {api_call}; got unexpected HTTP code {info['status']}: {info.get('msg')}" ) error_msg = "" if info["status"] == 401: # Unauthorized error_msg = ( f"API user does not have permission; Status: {info['status']}; Method: {method}: Call: {api_call}" ) elif info["status"] == 403: # Forbidden error_msg = f"API request not authenticated; Status: {info['status']}; Method: {method}: Call: {api_call}" elif info["status"] == 429: # Too many requests error_msg = f"API client is rate limited; Status: {info['status']}; Method: {method}: Call: {api_call}" elif info["status"] == 405: # Method not allowed error_msg = ( f"API incorrect HTTP method provided; Status: {info['status']}; Method: {method}: Call: {api_call}" ) elif info["status"] == 415: # Unsupported Media Type error_msg = f"API request is not valid JSON; Status: {info['status']}; Method: {method}: Call: {api_call}" elif info["status"] == 400: # Bad Request error_msg = f"API bad request; Status: {info['status']}; Method: {method}: Call: {api_call}" result = None try: content = resp.read() except AttributeError: content = None if not content: if info["body"]: content = info["body"] else: error_msg += "; The API response was empty" if content: try: result = json.loads(to_text(content, errors="surrogate_or_strict")) except getattr(json, "JSONDecodeError", ValueError) as e: error_msg += f"; Failed to parse API response with error {e}: {content}" # Without a valid/parsed JSON response no more error processing can be done if result is None: self.module.fail_json(msg=error_msg) if "success" not in result: error_msg += f"; Unexpected error details: {result.get('error')}" self.module.fail_json(msg=error_msg) if not result["success"]: error_msg += "; Error details: " for error in result["errors"]: error_msg += f"code: {error['code']}, error: {error['message']}; " if "error_chain" in error: for chain_error in error["error_chain"]: error_msg += f"code: {chain_error['code']}, error: {chain_error['message']}; " self.module.fail_json(msg=error_msg) return result, info["status"] def _cf_api_call(self, api_call, method="GET", payload=None): result, status = self._cf_simple_api_call(api_call, method, payload) data = result["result"] if "result_info" in result: pagination = result["result_info"] if pagination["total_pages"] > 1: next_page = int(pagination["page"]) + 1 parameters = [f"page={next_page}"] # strip "page" parameter from call parameters (if there are any) if "?" in api_call: raw_api_call, query = api_call.split("?", 1) parameters += [param for param in query.split("&") if not param.startswith("page")] else: raw_api_call = api_call while next_page <= pagination["total_pages"]: raw_api_call += f"?{'&'.join(parameters)}" result, status = self._cf_simple_api_call(raw_api_call, method, payload) data += result["result"] next_page += 1 return data, status def _get_zone_id(self, zone=None): if not zone: zone = self.zone zones = self.get_zones(zone) if len(zones) > 1: self.module.fail_json(msg=f"More than one zone matches {zone}") if len(zones) < 1: self.module.fail_json(msg=f"No zone found with name {zone}") return zones[0]["id"] def get_zones(self, name=None): if not name: name = self.zone param = "" if name: param = f"?{urlencode({'name': name})}" zones, status = self._cf_api_call(f"/zones{param}") return zones def get_dns_records(self, zone_name=None, type=None, record=None, value=""): if not zone_name: zone_name = self.zone if not type: type = self.type if not record: record = self.record # necessary because None as value means to override user # set module value if (not value) and (value is not None): value = self.value zone_id = self._get_zone_id() api_call = f"/zones/{zone_id}/dns_records" query = {} if type: query["type"] = type if record: query["name"] = record if value: query["content"] = value if query: api_call += f"?{urlencode(query)}" records, status = self._cf_api_call(api_call) return records def delete_dns_records(self, solo): records = [] content = self.value search_record = self.record if self.type == "SRV": if not (self.value is None or self.value == ""): content = join_str("\t", self.weight, self.port, self.value) search_record = join_str(".", self.service, self.proto, self.record) elif self.type == "DS": if not (self.value is None or self.value == ""): content = join_str("\t", self.key_tag, self.algorithm, self.hash_type, self.value) elif self.type == "SSHFP": if not (self.value is None or self.value == ""): content = join_str(" ", self.algorithm, self.hash_type, self.value.upper()) elif self.type == "TLSA": if not (self.value is None or self.value == ""): content = join_str("\t", self.cert_usage, self.selector, self.hash_type, self.value) search_record = join_str(".", self.port, self.proto, self.record) if solo: search_value = None else: search_value = content zone_id = self._get_zone_id(self.zone) records = self.get_dns_records(self.zone, self.type, search_record, search_value) for rr in records: if solo: if not ((rr["type"] == self.type) and (rr["name"] == search_record) and (rr["content"] == content)): self.changed = True if not self.module.check_mode: result, info = self._cf_api_call(f"/zones/{zone_id}/dns_records/{rr['id']}", "DELETE") else: self.changed = True if not self.module.check_mode: result, info = self._cf_api_call(f"/zones/{zone_id}/dns_records/{rr['id']}", "DELETE") return self.changed def ensure_dns_record(self): search_value = self.value search_record = self.record new_record = None if self.type in ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "PTR"]: if not self.value: self.module.fail_json(msg="You must provide a non-empty value to create this record type") # there can only be one CNAME per record # ignoring the value when searching for existing # CNAME records allows us to update the value if it # changes if self.type == "CNAME": search_value = None new_record = {"type": self.type, "name": self.record, "content": self.value, "ttl": self.ttl} if self.type in ["A", "AAAA", "CNAME"]: new_record["proxied"] = self.proxied if self.type == "MX": for attr in [self.priority, self.value]: if (attr is None) or (attr == ""): self.module.fail_json(msg="You must provide priority and a value to create this record type") new_record = { "type": self.type, "name": self.record, "content": self.value, "priority": self.priority, "ttl": self.ttl, } if self.type == "SRV": for attr in [self.port, self.priority, self.proto, self.service, self.weight, self.value]: if (attr is None) or (attr == ""): self.module.fail_json( msg="You must provide port, priority, proto, service, weight and a value to create this record type" ) srv_data = { "target": self.value, "port": self.port, "weight": self.weight, "priority": self.priority, } new_record = { "type": self.type, "name": join_str(".", self.service, self.proto, self.record), "ttl": self.ttl, "data": srv_data, } search_value = join_str("\t", self.weight, self.port, self.value) search_record = join_str(".", self.service, self.proto, self.record) if self.type == "DS": for attr in [self.key_tag, self.algorithm, self.hash_type, self.value]: if (attr is None) or (attr == ""): self.module.fail_json( msg="You must provide key_tag, algorithm, hash_type and a value to create this record type" ) ds_data = { "key_tag": self.key_tag, "algorithm": self.algorithm, "digest_type": self.hash_type, "digest": self.value, } new_record = { "type": self.type, "name": self.record, "data": ds_data, "ttl": self.ttl, } search_value = join_str("\t", self.key_tag, self.algorithm, self.hash_type, self.value) if self.type == "SSHFP": for attr in [self.algorithm, self.hash_type, self.value]: if (attr is None) or (attr == ""): self.module.fail_json( msg="You must provide algorithm, hash_type and a value to create this record type" ) sshfp_data = { "fingerprint": self.value.upper(), "type": self.hash_type, "algorithm": self.algorithm, } new_record = { "type": self.type, "name": self.record, "data": sshfp_data, "ttl": self.ttl, } search_value = join_str(" ", self.algorithm, self.hash_type, self.value) if self.type == "TLSA": for attr in [self.port, self.proto, self.cert_usage, self.selector, self.hash_type, self.value]: if (attr is None) or (attr == ""): self.module.fail_json( msg="You must provide port, proto, cert_usage, selector, hash_type and a value to create this record type" ) search_record = join_str(".", self.port, self.proto, self.record) tlsa_data = { "usage": self.cert_usage, "selector": self.selector, "matching_type": self.hash_type, "certificate": self.value, } new_record = { "type": self.type, "name": search_record, "data": tlsa_data, "ttl": self.ttl, } search_value = join_str("\t", self.cert_usage, self.selector, self.hash_type, self.value) if self.type == "CAA": for attr in [self.flag, self.tag, self.value]: if attr == "": self.module.fail_json(msg="You must provide flag, tag and a value to create this record type") caa_data = { "flags": self.flag, "tag": self.tag, "value": self.value, } new_record = { "type": self.type, "name": self.record, "data": caa_data, "ttl": self.ttl, } search_value = None new_record["comment"] = self.comment or None new_record["tags"] = self.tags or [] zone_id = self._get_zone_id(self.zone) records = self.get_dns_records(self.zone, self.type, search_record, search_value) # in theory this should be impossible as cloudflare does not allow # the creation of duplicate records but lets cover it anyways if len(records) > 1: # As Cloudflare API cannot filter record containing quotes # CAA records must be compared locally if self.type == "CAA": for rr in records: if ( rr["data"]["flags"] == caa_data["flags"] and rr["data"]["tag"] == caa_data["tag"] and rr["data"]["value"] == caa_data["value"] ): return rr, self.changed else: self.module.fail_json( msg="More than one record already exists for the given attributes. That should be impossible, please open an issue!" ) # record already exists, check if it must be updated if len(records) == 1: cur_record = records[0] do_update = False if (self.ttl is not None) and (cur_record["ttl"] != self.ttl): do_update = True if (self.priority is not None) and ("priority" in cur_record) and (cur_record["priority"] != self.priority): do_update = True if ("proxied" in new_record) and ("proxied" in cur_record) and (cur_record["proxied"] != self.proxied): do_update = True if ("data" in new_record) and ("data" in cur_record): if cur_record["data"] != new_record["data"]: do_update = True if (self.type == "CNAME") and (cur_record["content"] != new_record["content"]): do_update = True if cur_record["comment"] != new_record["comment"]: do_update = True if sorted(cur_record["tags"]) != sorted(new_record["tags"]): do_update = True if do_update: if self.module.check_mode: result = new_record else: result, info = self._cf_api_call( f"/zones/{zone_id}/dns_records/{records[0]['id']}", "PUT", new_record ) self.changed = True return result, self.changed else: return records, self.changed if self.module.check_mode: result = new_record else: result, info = self._cf_api_call(f"/zones/{zone_id}/dns_records", "POST", new_record) self.changed = True return result, self.changed def main(): module = AnsibleModule( argument_spec=dict( api_token=dict(type="str", no_log=True, fallback=(env_fallback, ["CLOUDFLARE_TOKEN"])), account_api_key=dict(type="str", no_log=True, aliases=["account_api_token"]), account_email=dict(type="str"), algorithm=dict(type="int"), cert_usage=dict(type="int", choices=[0, 1, 2, 3]), comment=dict(type="str"), hash_type=dict(type="int", choices=[1, 2]), key_tag=dict(type="int", no_log=False), port=dict(type="int"), flag=dict(type="int", choices=[0, 1]), tag=dict(type="str", choices=["issue", "issuewild", "iodef"]), tags=dict(type="list", elements="str"), priority=dict(type="int", default=1), proto=dict(type="str"), proxied=dict(type="bool", default=False), record=dict(type="str", default="@", aliases=["name"]), selector=dict(type="int", choices=[0, 1]), service=dict(type="str"), solo=dict(type="bool"), state=dict(type="str", default="present", choices=["absent", "present"]), timeout=dict(type="int", default=30), ttl=dict(type="int", default=1), type=dict( type="str", choices=["A", "AAAA", "CNAME", "DS", "MX", "NS", "SRV", "SSHFP", "TLSA", "CAA", "TXT", "PTR"], ), value=dict(type="str", aliases=["content"]), weight=dict(type="int", default=1), zone=dict(type="str", required=True, aliases=["domain"]), ), supports_check_mode=True, required_if=[ ("state", "present", ["record", "type", "value"]), ("state", "absent", ["record"]), ("type", "SRV", ["proto", "service"]), ("type", "TLSA", ["proto", "port"]), ("type", "CAA", ["flag", "tag"]), ], required_together=[ ("account_api_key", "account_email"), ], required_one_of=[ ["api_token", "account_api_key"], ], ) if module.params["type"] == "SRV": if not ( ( module.params["weight"] is not None and module.params["port"] is not None and not (module.params["value"] is None or module.params["value"] == "") ) or ( module.params["weight"] is None and module.params["port"] is None and (module.params["value"] is None or module.params["value"] == "") ) ): module.fail_json( msg="For SRV records the params weight, port and value all need to be defined, or not at all." ) if module.params["type"] == "SSHFP": if not ( ( module.params["algorithm"] is not None and module.params["hash_type"] is not None and not (module.params["value"] is None or module.params["value"] == "") ) or ( module.params["algorithm"] is None and module.params["hash_type"] is None and (module.params["value"] is None or module.params["value"] == "") ) ): module.fail_json( msg="For SSHFP records the params algorithm, hash_type and value all need to be defined, or not at all." ) if module.params["type"] == "TLSA": if not ( ( module.params["cert_usage"] is not None and module.params["selector"] is not None and module.params["hash_type"] is not None and not (module.params["value"] is None or module.params["value"] == "") ) or ( module.params["cert_usage"] is None and module.params["selector"] is None and module.params["hash_type"] is None and (module.params["value"] is None or module.params["value"] == "") ) ): 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." ) if module.params["type"] == "CAA": if not ( ( module.params["flag"] is not None and module.params["tag"] is not None and not (module.params["value"] is None or module.params["value"] == "") ) or ( module.params["flag"] is None and module.params["tag"] is None and (module.params["value"] is None or module.params["value"] == "") ) ): module.fail_json( msg="For CAA records the params flag, tag and value all need to be defined, or not at all." ) if module.params["type"] == "DS": if not ( ( module.params["key_tag"] is not None and module.params["algorithm"] is not None and module.params["hash_type"] is not None and not (module.params["value"] is None or module.params["value"] == "") ) or ( module.params["key_tag"] is None and module.params["algorithm"] is None and module.params["hash_type"] is None and (module.params["value"] is None or module.params["value"] == "") ) ): 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." ) changed = False cf_api = CloudflareAPI(module) # sanity checks if cf_api.is_solo and cf_api.state == "absent": module.fail_json(msg="solo=true can only be used with state=present") # perform add, delete or update (only the TTL can be updated) of one or # more records if cf_api.state == "present": # delete all records matching record name + type if cf_api.is_solo: changed = cf_api.delete_dns_records(solo=cf_api.is_solo) result, changed = cf_api.ensure_dns_record() if isinstance(result, list): module.exit_json(changed=changed, result={"record": result[0]}) module.exit_json(changed=changed, result={"record": result}) else: # force solo to False, just to be sure changed = cf_api.delete_dns_records(solo=False) module.exit_json(changed=changed) if __name__ == "__main__": main()