From 8691c7ced795de343a4413f75eca002c073bf34f Mon Sep 17 00:00:00 2001 From: Andreas Olsson Date: Sat, 18 Aug 2018 21:56:02 +0200 Subject: [PATCH] Add DS record type to cloudflare_dns module (#44349) Cloudflare recently added support for DS records. They are used to delegate DNSSEC trust to a subdomain. --- .../modules/net_tools/cloudflare_dns.py | 69 +++++- .../test_cloudflare_dns/tasks/ds_record.yml | 223 ++++++++++++++++++ .../roles/test_cloudflare_dns/tasks/main.yml | 1 + 3 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 test/legacy/roles/test_cloudflare_dns/tasks/ds_record.yml diff --git a/lib/ansible/modules/net_tools/cloudflare_dns.py b/lib/ansible/modules/net_tools/cloudflare_dns.py index 4449c02f2d5..1c44d7f6888 100644 --- a/lib/ansible/modules/net_tools/cloudflare_dns.py +++ b/lib/ansible/modules/net_tools/cloudflare_dns.py @@ -35,8 +35,7 @@ options: required: true algorithm: description: - - Algorithm number. Required for C(type=SSHFP) when C(state=present). - choices: [ 1, 2, 3, 4 ] + - Algorithm number. Required for C(type=DS) and C(type=SSHFP) when C(state=present). type: int version_added: 2.7 cert_usage: @@ -47,10 +46,15 @@ options: version_added: 2.7 hash_type: description: - - Hash type number. Required for C(type=SSHFP) and C(type=TLSA) when C(state=present). + - 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: @@ -99,8 +103,8 @@ options: type: description: - The type of DNS record to create. Required if C(state=present) - - C(type=SSHFP) and C(type=TLSA) added in Ansible 2.7. - choices: [ 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'MX', 'NS', 'SPF', 'SSHFP', 'TLSA' ] + - 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) @@ -207,6 +211,16 @@ EXAMPLES = ''' 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 = ''' @@ -227,7 +241,7 @@ record: sample: 2016-03-25T19:09:42.516553Z data: description: additional record data - returned: success, if type is SRV, SSHFP or TLSA + returned: success, if type is SRV, DS, SSHFP or TLSA type: dictionary sample: { name: "jabber", @@ -320,6 +334,7 @@ class CloudflareAPI(object): 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 = module.params['proto'] @@ -357,6 +372,10 @@ class CloudflareAPI(object): 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, @@ -503,7 +522,8 @@ class CloudflareAPI(object): 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']: + 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: @@ -516,6 +536,9 @@ class CloudflareAPI(object): 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'] @@ -545,7 +568,7 @@ class CloudflareAPI(object): 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']: + 'algorithm', 'cert_usage', 'hash_type', 'selector', 'key_tag']: if param in kwargs: params[param] = kwargs[param] else: @@ -607,6 +630,24 @@ class CloudflareAPI(object): 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 == ''): @@ -686,9 +727,10 @@ def main(): 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, choices=[1, 2, 3, 4], type='int'), + 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'), @@ -700,7 +742,7 @@ def main(): 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', 'SPF', 'SSHFP', 'TLSA'], type='str'), + 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'), @@ -736,6 +778,13 @@ def main(): 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) diff --git a/test/legacy/roles/test_cloudflare_dns/tasks/ds_record.yml b/test/legacy/roles/test_cloudflare_dns/tasks/ds_record.yml new file mode 100644 index 00000000000..50aea9fe917 --- /dev/null +++ b/test/legacy/roles/test_cloudflare_dns/tasks/ds_record.yml @@ -0,0 +1,223 @@ +--- +######## DS record tests ################# + +- name: "Test: DS record creation" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 54592 + algorithm: 8 + hash_type: 2 + value: 6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F + ttl: 150 + register: cloudflare_dns + +- name: "Validate: DS record creation" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is changed + - cloudflare_dns.result.record.content == "54592\t8\t2\t6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F" + - cloudflare_dns.result.record.ttl == 150 + - cloudflare_dns.result.record.type == "DS" + - cloudflare_dns.result.record.name == "{{ cloudflare_dns_record }}.{{ cloudflare_zone }}" + - cloudflare_dns.result.record.zone_name == "{{ cloudflare_zone }}" + - cloudflare_dns.result.record.data.key_tag == 54592 + - cloudflare_dns.result.record.data.algorithm == 8 + - cloudflare_dns.result.record.data.digest_type == 2 + - cloudflare_dns.result.record.data.digest == "6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F" + +- name: "Test: DS record idempotency" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 54592 + algorithm: 8 + hash_type: 2 + value: 6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F + ttl: 150 + register: cloudflare_dns + +- name: "Validate: DS record idempotency" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is not changed + +- name: "Test: DS record update" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 54592 + algorithm: 8 + hash_type: 2 + value: 6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F + ttl: 300 + register: cloudflare_dns + +- name: "Validate: DS record update" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is changed + - cloudflare_dns.result.record.ttl == 300 + +- name: "Test: DS record duplicate (create new record)" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 24397 + algorithm: 8 + hash_type: 2 + value: 8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556 + ttl: 300 + register: cloudflare_dns + +- name: "Validate: DS record duplicate (create new record)" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is changed + - cloudflare_dns.result.record.content == "24397\t8\t2\t8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556" + - cloudflare_dns.result.record.ttl == 300 + - cloudflare_dns.result.record.type == "DS" + - cloudflare_dns.result.record.name == "{{ cloudflare_dns_record }}.{{ cloudflare_zone }}" + - cloudflare_dns.result.record.zone_name == "{{ cloudflare_zone }}" + - cloudflare_dns.result.record.data.key_tag == 24397 + - cloudflare_dns.result.record.data.algorithm == 8 + - cloudflare_dns.result.record.data.digest_type == 2 + - cloudflare_dns.result.record.data.digest == "8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556" + +- name: "Test: DS record duplicate (old record present)" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 54592 + algorithm: 8 + hash_type: 2 + value: 6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F + ttl: 300 + register: cloudflare_dns + +- name: "Validate: DS record duplicate (old record present)" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is not changed + - cloudflare_dns.result.record.content == "54592\t8\t2\t6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F" + - cloudflare_dns.result.record.ttl == 300 + - cloudflare_dns.result.record.type == "DS" + - cloudflare_dns.result.record.name == "{{ cloudflare_dns_record }}.{{ cloudflare_zone }}" + - cloudflare_dns.result.record.zone_name == "{{ cloudflare_zone }}" + - cloudflare_dns.result.record.data.key_tag == 54592 + - cloudflare_dns.result.record.data.algorithm == 8 + - cloudflare_dns.result.record.data.digest_type == 2 + - cloudflare_dns.result.record.data.digest == "6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F" + +- name: "Test: DS record duplicate (make new record solo)" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 24397 + algorithm: 8 + hash_type: 2 + value: 8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556 + ttl: 300 + solo: true + register: cloudflare_dns + +- name: "Validate: DS record duplicate (make new record solo)" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is changed + - cloudflare_dns.result.record.content == "24397\t8\t2\t8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556" + - cloudflare_dns.result.record.ttl == 300 + - cloudflare_dns.result.record.type == "DS" + - cloudflare_dns.result.record.name == "{{ cloudflare_dns_record }}.{{ cloudflare_zone }}" + - cloudflare_dns.result.record.zone_name == "{{ cloudflare_zone }}" + - cloudflare_dns.result.record.data.key_tag == 24397 + - cloudflare_dns.result.record.data.algorithm == 8 + - cloudflare_dns.result.record.data.digest_type == 2 + - cloudflare_dns.result.record.data.digest == "8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556" + +- name: "Test: DS record duplicate (old record absent)" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 54592 + algorithm: 8 + hash_type: 2 + value: 6A95688429A7796533165ACEFBE91BD115DAD747AA4E1D5DCA70DF040C68216F + ttl: 300 + state: absent + register: cloudflare_dns + +- name: "Validate: DS record duplicate (old record absent)" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is not changed + +- name: "Test: DS record deletion" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 24397 + algorithm: 8 + hash_type: 2 + value: 8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556 + ttl: 300 + state: absent + register: cloudflare_dns + +- name: "Validate: DS record deletion" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is changed + +- name: "Test: DS record deletion succeeded" + cloudflare_dns: + account_email: "{{ cloudflare_email }}" + account_api_token: "{{ cloudflare_api_token }}" + zone: "{{ cloudflare_zone }}" + record: "{{ cloudflare_dns_record }}" + type: DS + key_tag: 24397 + algorithm: 8 + hash_type: 2 + value: 8A5E62E004BFA4CF4B42BE9405C39EBA1AC5A91BDE181FB73E935ECC1F3F5556 + ttl: 300 + state: absent + register: cloudflare_dns + +- name: "Validate: DS record deletion succeeded" + assert: + that: + - cloudflare_dns is successful + - cloudflare_dns is not changed diff --git a/test/legacy/roles/test_cloudflare_dns/tasks/main.yml b/test/legacy/roles/test_cloudflare_dns/tasks/main.yml index cc463b0dfd7..6fb254001e0 100644 --- a/test/legacy/roles/test_cloudflare_dns/tasks/main.yml +++ b/test/legacy/roles/test_cloudflare_dns/tasks/main.yml @@ -59,6 +59,7 @@ - include: cname_record.yml - include: txt_record.yml - include: ns_record.yml +- include: ds_record.yml - include: spf_record.yml - include: mx_record.yml - include: srv_record.yml