1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 16:01:55 +00:00
community.general/plugins/modules/cloudflare_dns.py
2026-01-05 18:12:39 +01:00

1080 lines
37 KiB
Python

#!/usr/bin/python
# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
# 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.
- Should be set either to V(0) for not critical, or to V(128) for critical.
- Before community.general 12.3.0, only V(0) and V(1) were valid options.
The value V(1) is still available for backwards-compatibility reasons.
- Required for O(type=CAA) when O(state=present).
type: int
choices: [0, 1, 128]
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, 128]),
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()