mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-24 19:31:26 -07:00
Cloudflare's DNS API deals with the case insensitivy of DNS names and IPv6 addresses by forcing them into lower case. To properly be able to detect changes the Ansible module needs to behave accordingly. To what extent the API expects sent DNS names to be lower case varies between record types. Yet, since sending lower case always works, and since we always get lower case back, it feels cleanest to use lower case for all DNS names.
822 lines
33 KiB
Python
822 lines
33 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
__metaclass__ = type
|
|
|
|
|
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
'status': ['preview'],
|
|
'supported_by': 'community'}
|
|
|
|
|
|
DOCUMENTATION = '''
|
|
---
|
|
module: cloudflare_dns
|
|
author: "Michael Gruener (@mgruener)"
|
|
requirements:
|
|
- "python >= 2.6"
|
|
version_added: "2.1"
|
|
short_description: manage Cloudflare DNS records
|
|
description:
|
|
- "Manages dns records via the Cloudflare API, see the docs: U(https://api.cloudflare.com/)"
|
|
options:
|
|
account_api_token:
|
|
description:
|
|
- >
|
|
Account API token. You can obtain your API key from the bottom of the Cloudflare 'My Account' page, found here: U(https://dash.cloudflare.com/)
|
|
required: true
|
|
account_email:
|
|
description:
|
|
- "Account email."
|
|
required: true
|
|
algorithm:
|
|
description:
|
|
- Algorithm number. Required for C(type=DS) and C(type=SSHFP) when C(state=present).
|
|
type: int
|
|
version_added: 2.7
|
|
cert_usage:
|
|
description:
|
|
- Certificate usage number. Required for C(type=TLSA) when C(state=present).
|
|
choices: [ 0, 1, 2, 3 ]
|
|
type: int
|
|
version_added: 2.7
|
|
hash_type:
|
|
description:
|
|
- Hash type number. Required for C(type=DS), C(type=SSHFP) and C(type=TLSA) when C(state=present).
|
|
choices: [ 1, 2 ]
|
|
type: int
|
|
version_added: 2.7
|
|
key_tag:
|
|
description:
|
|
- DNSSEC key tag. Needed for C(type=DS) when C(state=present).
|
|
type: int
|
|
version_added: 2.7
|
|
port:
|
|
description: Service port. Required for C(type=SRV) and C(type=TLSA).
|
|
priority:
|
|
description: Record priority. Required for C(type=MX) and C(type=SRV)
|
|
default: "1"
|
|
proto:
|
|
description:
|
|
- Service protocol. Required for C(type=SRV) and C(type=TLSA).
|
|
- Common values are tcp and udp.
|
|
- Before Ansible 2.6 only tcp and udp were available.
|
|
proxied:
|
|
description: Proxy through cloudflare network or just use DNS
|
|
type: bool
|
|
default: 'no'
|
|
version_added: "2.3"
|
|
record:
|
|
description:
|
|
- Record to add. Required if C(state=present). Default is C(@) (e.g. the zone name)
|
|
default: "@"
|
|
aliases: [ "name" ]
|
|
selector:
|
|
description:
|
|
- Selector number. Required for C(type=TLSA) when C(state=present).
|
|
choices: [ 0, 1 ]
|
|
type: int
|
|
version_added: 2.7
|
|
service:
|
|
description: Record service. Required for C(type=SRV)
|
|
solo:
|
|
description:
|
|
- Whether the record should be the only one for that record type and record name. Only use with C(state=present)
|
|
- This will delete all other records with the same record name and type.
|
|
state:
|
|
description:
|
|
- Whether the record(s) should exist or not
|
|
choices: [ 'present', 'absent' ]
|
|
default: present
|
|
timeout:
|
|
description:
|
|
- Timeout for Cloudflare API calls
|
|
default: 30
|
|
ttl:
|
|
description:
|
|
- The TTL to give the new record. Must be between 120 and 2,147,483,647 seconds, or 1 for automatic.
|
|
default: 1 (automatic)
|
|
type:
|
|
description:
|
|
- The type of DNS record to create. Required if C(state=present)
|
|
- C(type=DS), C(type=SSHFP) and C(type=TLSA) added in Ansible 2.7.
|
|
choices: [ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'DS', 'SPF', 'SSHFP', 'TLSA' ]
|
|
value:
|
|
description:
|
|
- The record value. Required for C(state=present)
|
|
aliases: [ "content" ]
|
|
weight:
|
|
description: Service weight. Required for C(type=SRV)
|
|
default: "1"
|
|
zone:
|
|
description:
|
|
- The name of the Zone to work with (e.g. "example.com"). The Zone must already exist.
|
|
required: true
|
|
aliases: ["domain"]
|
|
'''
|
|
|
|
EXAMPLES = '''
|
|
# create a test.my.com A record to point to 127.0.0.1
|
|
- cloudflare_dns:
|
|
zone: my.com
|
|
record: test
|
|
type: A
|
|
value: 127.0.0.1
|
|
account_email: test@example.com
|
|
account_api_token: dummyapitoken
|
|
register: record
|
|
|
|
# create a my.com CNAME record to example.com
|
|
- cloudflare_dns:
|
|
zone: my.com
|
|
type: CNAME
|
|
value: example.com
|
|
state: present
|
|
account_email: test@example.com
|
|
account_api_token: dummyapitoken
|
|
|
|
# change it's ttl
|
|
- cloudflare_dns:
|
|
zone: my.com
|
|
type: CNAME
|
|
value: example.com
|
|
ttl: 600
|
|
state: present
|
|
account_email: test@example.com
|
|
account_api_token: dummyapitoken
|
|
|
|
# and delete the record
|
|
- cloudflare_dns:
|
|
zone: my.com
|
|
type: CNAME
|
|
value: example.com
|
|
state: absent
|
|
account_email: test@example.com
|
|
account_api_token: dummyapitoken
|
|
|
|
# create a my.com CNAME record to example.com and proxy through cloudflare's network
|
|
- cloudflare_dns:
|
|
zone: my.com
|
|
type: CNAME
|
|
value: example.com
|
|
state: present
|
|
proxied: yes
|
|
account_email: test@example.com
|
|
account_api_token: dummyapitoken
|
|
|
|
# create TXT record "test.my.com" with value "unique value"
|
|
# delete all other TXT records named "test.my.com"
|
|
- cloudflare_dns:
|
|
domain: my.com
|
|
record: test
|
|
type: TXT
|
|
value: unique value
|
|
state: present
|
|
solo: true
|
|
account_email: test@example.com
|
|
account_api_token: dummyapitoken
|
|
|
|
# create a SRV record _foo._tcp.my.com
|
|
- cloudflare_dns:
|
|
domain: my.com
|
|
service: foo
|
|
proto: tcp
|
|
port: 3500
|
|
priority: 10
|
|
weight: 20
|
|
type: SRV
|
|
value: fooserver.my.com
|
|
|
|
# create a SSHFP record login.example.com
|
|
- cloudflare_dns:
|
|
zone: example.com
|
|
record: login
|
|
type: SSHFP
|
|
algorithm: 4
|
|
hash_type: 2
|
|
value: 9dc1d6742696d2f51ca1f1a78b3d16a840f7d111eb9454239e70db31363f33e1
|
|
|
|
# create a TLSA record _25._tcp.mail.example.com
|
|
- cloudflare_dns:
|
|
zone: example.com
|
|
record: mail
|
|
port: 25
|
|
proto: tcp
|
|
type: TLSA
|
|
cert_usage: 3
|
|
selector: 1
|
|
hash_type: 1
|
|
value: 6b76d034492b493e15a7376fccd08e63befdad0edab8e442562f532338364bf3
|
|
|
|
# Create a DS record for subdomain.example.com
|
|
- cloudflare_dns:
|
|
zone: example.com
|
|
record: subdomain
|
|
type: DS
|
|
key_tag: 5464
|
|
algorithm: 8
|
|
hash_type: 2
|
|
value: B4EB5AC4467D2DFB3BAF9FB9961DC1B6FED54A58CDFAA3E465081EC86F89BFAB
|
|
'''
|
|
|
|
RETURN = '''
|
|
record:
|
|
description: dictionary containing the record data
|
|
returned: success, except on record deletion
|
|
type: complex
|
|
contains:
|
|
content:
|
|
description: the record content (details depend on record type)
|
|
returned: success
|
|
type: string
|
|
sample: 192.0.2.91
|
|
created_on:
|
|
description: the record creation date
|
|
returned: success
|
|
type: string
|
|
sample: 2016-03-25T19:09:42.516553Z
|
|
data:
|
|
description: additional record data
|
|
returned: success, if type is SRV, DS, SSHFP or TLSA
|
|
type: dictionary
|
|
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: string
|
|
sample: f9efb0549e96abcb750de63b38c9576e
|
|
locked:
|
|
description: No documentation available
|
|
returned: success
|
|
type: boolean
|
|
sample: False
|
|
meta:
|
|
description: No documentation available
|
|
returned: success
|
|
type: dictionary
|
|
sample: { auto_added: false }
|
|
modified_on:
|
|
description: record modification date
|
|
returned: success
|
|
type: string
|
|
sample: 2016-03-25T19:09:42.516553Z
|
|
name:
|
|
description: the record name as FQDN (including _service and _proto for SRV)
|
|
returned: success
|
|
type: string
|
|
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: boolean
|
|
sample: False
|
|
proxied:
|
|
description: whether the record is proxied through cloudflare
|
|
returned: success
|
|
type: boolean
|
|
sample: False
|
|
ttl:
|
|
description: the time-to-live for the record
|
|
returned: success
|
|
type: int
|
|
sample: 300
|
|
type:
|
|
description: the record type
|
|
returned: success
|
|
type: string
|
|
sample: A
|
|
zone_id:
|
|
description: the id of the zone containing the record
|
|
returned: success
|
|
type: string
|
|
sample: abcede0bf9f0066f94029d2e6b73856a
|
|
zone_name:
|
|
description: the name of the zone containing the record
|
|
returned: success
|
|
type: string
|
|
sample: sample.com
|
|
'''
|
|
|
|
import json
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
|
from ansible.module_utils._text import to_native, to_text
|
|
from ansible.module_utils.urls import fetch_url
|
|
|
|
|
|
def lowercase_string(param):
|
|
if not isinstance(param, str):
|
|
return param
|
|
return param.lower()
|
|
|
|
|
|
class CloudflareAPI(object):
|
|
|
|
cf_api_endpoint = 'https://api.cloudflare.com/client/v4'
|
|
changed = False
|
|
|
|
def __init__(self, module):
|
|
self.module = module
|
|
self.account_api_token = module.params['account_api_token']
|
|
self.account_email = module.params['account_email']
|
|
self.algorithm = module.params['algorithm']
|
|
self.cert_usage = module.params['cert_usage']
|
|
self.hash_type = module.params['hash_type']
|
|
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 = '_' + self.proto
|
|
if (self.service is not None) and (not self.service.startswith('_')):
|
|
self.service = '_' + self.service
|
|
|
|
if (self.type == 'TLSA'):
|
|
if (self.proto is not None) and (not self.proto.startswith('_')):
|
|
self.proto = '_' + self.proto
|
|
if (self.port is not None):
|
|
self.port = '_' + str(self.port)
|
|
|
|
if not self.record.endswith(self.zone):
|
|
self.record = 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):
|
|
headers = {'X-Auth-Email': self.account_email,
|
|
'X-Auth-Key': self.account_api_token,
|
|
'Content-Type': 'application/json'}
|
|
data = None
|
|
if payload:
|
|
try:
|
|
data = json.dumps(payload)
|
|
except Exception as e:
|
|
self.module.fail_json(msg="Failed to encode payload as JSON: %s " % to_native(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="Failed API call {0}; got unexpected HTTP code {1}".format(api_call, info['status']))
|
|
|
|
error_msg = ''
|
|
if info['status'] == 401:
|
|
# Unauthorized
|
|
error_msg = "API user does not have permission; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
|
|
elif info['status'] == 403:
|
|
# Forbidden
|
|
error_msg = "API request not authenticated; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
|
|
elif info['status'] == 429:
|
|
# Too many requests
|
|
error_msg = "API client is rate limited; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
|
|
elif info['status'] == 405:
|
|
# Method not allowed
|
|
error_msg = "API incorrect HTTP method provided; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
|
|
elif info['status'] == 415:
|
|
# Unsupported Media Type
|
|
error_msg = "API request is not valid JSON; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
|
|
elif info['status'] == 400:
|
|
# Bad Request
|
|
error_msg = "API bad request; Status: {0}; Method: {1}: Call: {2}".format(info['status'], method, api_call)
|
|
|
|
result = None
|
|
try:
|
|
content = resp.read()
|
|
except AttributeError:
|
|
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 += "; Failed to parse API response with error {0}: {1}".format(to_native(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 not result['success']:
|
|
error_msg += "; Error details: "
|
|
for error in result['errors']:
|
|
error_msg += "code: {0}, error: {1}; ".format(error['code'], error['message'])
|
|
if 'error_chain' in error:
|
|
for chain_error in error['error_chain']:
|
|
error_msg += "code: {0}, error: {1}; ".format(chain_error['code'], 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 = ['page={0}'.format(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 += '?' + '&'.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="More than one zone matches {0}".format(zone))
|
|
|
|
if len(zones) < 1:
|
|
self.module.fail_json(msg="No zone found with name {0}".format(zone))
|
|
|
|
return zones[0]['id']
|
|
|
|
def get_zones(self, name=None):
|
|
if not name:
|
|
name = self.zone
|
|
param = ''
|
|
if name:
|
|
param = '?' + urlencode({'name': name})
|
|
zones, status = self._cf_api_call('/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 = '/zones/{0}/dns_records'.format(zone_id)
|
|
query = {}
|
|
if type:
|
|
query['type'] = type
|
|
if record:
|
|
query['name'] = record
|
|
if value:
|
|
query['content'] = value
|
|
if query:
|
|
api_call += '?' + urlencode(query)
|
|
|
|
records, status = self._cf_api_call(api_call)
|
|
return records
|
|
|
|
def delete_dns_records(self, **kwargs):
|
|
params = {}
|
|
for param in ['port', 'proto', 'service', 'solo', 'type', 'record', 'value', 'weight', 'zone',
|
|
'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']:
|
|
if param in kwargs:
|
|
params[param] = kwargs[param]
|
|
else:
|
|
params[param] = getattr(self, param)
|
|
|
|
records = []
|
|
content = params['value']
|
|
search_record = params['record']
|
|
if params['type'] == 'SRV':
|
|
if not (params['value'] is None or params['value'] == ''):
|
|
content = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
|
|
search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
|
|
elif params['type'] == 'DS':
|
|
if not (params['value'] is None or params['value'] == ''):
|
|
content = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
|
|
elif params['type'] == 'SSHFP':
|
|
if not (params['value'] is None or params['value'] == ''):
|
|
content = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
|
|
elif params['type'] == 'TLSA':
|
|
if not (params['value'] is None or params['value'] == ''):
|
|
content = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']
|
|
search_record = params['port'] + '.' + params['proto'] + '.' + params['record']
|
|
if params['solo']:
|
|
search_value = None
|
|
else:
|
|
search_value = content
|
|
|
|
records = self.get_dns_records(params['zone'], params['type'], search_record, search_value)
|
|
|
|
for rr in records:
|
|
if params['solo']:
|
|
if not ((rr['type'] == params['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('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
|
|
else:
|
|
self.changed = True
|
|
if not self.module.check_mode:
|
|
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(rr['zone_id'], rr['id']), 'DELETE')
|
|
return self.changed
|
|
|
|
def ensure_dns_record(self, **kwargs):
|
|
params = {}
|
|
for param in ['port', 'priority', 'proto', 'proxied', 'service', 'ttl', 'type', 'record', 'value', 'weight', 'zone',
|
|
'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']:
|
|
if param in kwargs:
|
|
params[param] = kwargs[param]
|
|
else:
|
|
params[param] = getattr(self, param)
|
|
|
|
search_value = params['value']
|
|
search_record = params['record']
|
|
new_record = None
|
|
if (params['type'] is None) or (params['record'] is None):
|
|
self.module.fail_json(msg="You must provide a type and a record to create a new record")
|
|
|
|
if (params['type'] in ['A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SPF']):
|
|
if not params['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 params['type'] == 'CNAME':
|
|
search_value = None
|
|
|
|
new_record = {
|
|
"type": params['type'],
|
|
"name": params['record'],
|
|
"content": params['value'],
|
|
"ttl": params['ttl']
|
|
}
|
|
|
|
if (params['type'] in ['A', 'AAAA', 'CNAME']):
|
|
new_record["proxied"] = params["proxied"]
|
|
|
|
if params['type'] == 'MX':
|
|
for attr in [params['priority'], params['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": params['type'],
|
|
"name": params['record'],
|
|
"content": params['value'],
|
|
"priority": params['priority'],
|
|
"ttl": params['ttl']
|
|
}
|
|
|
|
if params['type'] == 'SRV':
|
|
for attr in [params['port'], params['priority'], params['proto'], params['service'], params['weight'], params['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": params['value'],
|
|
"port": params['port'],
|
|
"weight": params['weight'],
|
|
"priority": params['priority'],
|
|
"name": params['record'][:-len('.' + params['zone'])],
|
|
"proto": params['proto'],
|
|
"service": params['service']
|
|
}
|
|
new_record = {"type": params['type'], "ttl": params['ttl'], 'data': srv_data}
|
|
search_value = str(params['weight']) + '\t' + str(params['port']) + '\t' + params['value']
|
|
search_record = params['service'] + '.' + params['proto'] + '.' + params['record']
|
|
|
|
if params['type'] == 'DS':
|
|
for attr in [params['key_tag'], params['algorithm'], params['hash_type'], params['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": params['key_tag'],
|
|
"algorithm": params['algorithm'],
|
|
"digest_type": params['hash_type'],
|
|
"digest": params['value'],
|
|
}
|
|
new_record = {
|
|
"type": params['type'],
|
|
"name": params['record'],
|
|
'data': ds_data,
|
|
"ttl": params['ttl'],
|
|
}
|
|
search_value = str(params['key_tag']) + '\t' + str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
|
|
|
|
if params['type'] == 'SSHFP':
|
|
for attr in [params['algorithm'], params['hash_type'], params['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": params['value'],
|
|
"type": params['hash_type'],
|
|
"algorithm": params['algorithm'],
|
|
}
|
|
new_record = {
|
|
"type": params['type'],
|
|
"name": params['record'],
|
|
'data': sshfp_data,
|
|
"ttl": params['ttl'],
|
|
}
|
|
search_value = str(params['algorithm']) + '\t' + str(params['hash_type']) + '\t' + params['value']
|
|
|
|
if params['type'] == 'TLSA':
|
|
for attr in [params['port'], params['proto'], params['cert_usage'], params['selector'], params['hash_type'], params['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 = params['port'] + '.' + params['proto'] + '.' + params['record']
|
|
tlsa_data = {
|
|
"usage": params['cert_usage'],
|
|
"selector": params['selector'],
|
|
"matching_type": params['hash_type'],
|
|
"certificate": params['value'],
|
|
}
|
|
new_record = {
|
|
"type": params['type'],
|
|
"name": search_record,
|
|
'data': tlsa_data,
|
|
"ttl": params['ttl'],
|
|
}
|
|
search_value = str(params['cert_usage']) + '\t' + str(params['selector']) + '\t' + str(params['hash_type']) + '\t' + params['value']
|
|
|
|
zone_id = self._get_zone_id(params['zone'])
|
|
records = self.get_dns_records(params['zone'], params['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:
|
|
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 (params['ttl'] is not None) and (cur_record['ttl'] != params['ttl']):
|
|
do_update = True
|
|
if (params['priority'] is not None) and ('priority' in cur_record) and (cur_record['priority'] != params['priority']):
|
|
do_update = True
|
|
if ('proxied' in new_record) and ('proxied' in cur_record) and (cur_record['proxied'] != params['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 (params['type'] == 'CNAME') and (cur_record['content'] != new_record['content']):
|
|
do_update = True
|
|
if do_update:
|
|
if self.module.check_mode:
|
|
result = new_record
|
|
else:
|
|
result, info = self._cf_api_call('/zones/{0}/dns_records/{1}'.format(zone_id, 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('/zones/{0}/dns_records'.format(zone_id), 'POST', new_record)
|
|
self.changed = True
|
|
return result, self.changed
|
|
|
|
|
|
def main():
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
account_api_token=dict(required=True, no_log=True, type='str'),
|
|
account_email=dict(required=True, type='str'),
|
|
algorithm=dict(required=False, default=None, type='int'),
|
|
cert_usage=dict(required=False, default=None, choices=[0, 1, 2, 3], type='int'),
|
|
hash_type=dict(required=False, default=None, choices=[1, 2], type='int'),
|
|
key_tag=dict(required=False, default=None, type='int'),
|
|
port=dict(required=False, default=None, type='int'),
|
|
priority=dict(required=False, default=1, type='int'),
|
|
proto=dict(required=False, default=None, type='str'),
|
|
proxied=dict(required=False, default=False, type='bool'),
|
|
record=dict(required=False, default='@', aliases=['name'], type='str'),
|
|
selector=dict(required=False, default=None, choices=[0, 1], type='int'),
|
|
service=dict(required=False, default=None, type='str'),
|
|
solo=dict(required=False, default=None, type='bool'),
|
|
state=dict(required=False, default='present', choices=['present', 'absent'], type='str'),
|
|
timeout=dict(required=False, default=30, type='int'),
|
|
ttl=dict(required=False, default=1, type='int'),
|
|
type=dict(required=False, default=None, choices=['A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'DS', 'SPF', 'SSHFP', 'TLSA'], type='str'),
|
|
value=dict(required=False, default=None, aliases=['content'], type='str'),
|
|
weight=dict(required=False, default=1, type='int'),
|
|
zone=dict(required=True, default=None, aliases=['domain'], type='str'),
|
|
),
|
|
supports_check_mode=True,
|
|
required_if=([
|
|
('state', 'present', ['record', 'type', 'value']),
|
|
('state', 'absent', ['record']),
|
|
('type', 'SRV', ['proto', 'service']),
|
|
('type', 'TLSA', ['proto', 'port']),
|
|
]
|
|
),
|
|
)
|
|
|
|
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'] == '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]})
|
|
else:
|
|
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()
|