From fb32c19feacc386aeb6ec1b8c6e01cb0d5793771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Blot?= Date: Mon, 13 Feb 2017 12:13:23 +0100 Subject: [PATCH] New module: nsupdate (#21099) Add nsupdate module to manage DNS records on a DNS server This uses dnspython library It's greatly inspired by https://github.com/mskarbek/ansible-nsupdate with some rework, better feedbacks and documentation addition Signed-off-by: nerzhul --- CHANGELOG.md | 1 + lib/ansible/modules/network/nsupdate.py | 357 ++++++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 lib/ansible/modules/network/nsupdate.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dde4e26c0..93caae80f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -148,6 +148,7 @@ Ansible Changes By Release * sf_snapshot_schedule_manager * sf_volume_access_group_manager - nginx_status_facts +- nsupdate - omapi_host - openssl: * openssl_privatekey diff --git a/lib/ansible/modules/network/nsupdate.py b/lib/ansible/modules/network/nsupdate.py new file mode 100644 index 0000000000..9a759b7330 --- /dev/null +++ b/lib/ansible/modules/network/nsupdate.py @@ -0,0 +1,357 @@ +#!/usr/bin/python + +""" +Ansible module to manage DNS records using dnspython +(c) 2016, Marcin Skarbek +(c) 2016, Andreas Olsson +(c) 2017, Loic Blot + +This module was ported from https://github.com/mskarbek/ansible-nsupdate + +This file is part of Ansible + +Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with Ansible. If not, see . +""" + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + +DOCUMENTATION = ''' +--- +module: nsupdate + +short_description: Manage DNS records. +description: + - Create, update and remove DNS records using DDNS updates + - DDNS works well with both bind and Microsoft DNS (see https://technet.microsoft.com/en-us/library/cc961412.aspx) +version_added: "2.3" +requirements: + - dnspython +author: "Loic Blot (@nerzhul)" +options: + state: + description: + - Manage DNS record. + choices: ['present', 'absent'] + server: + description: + - Apply DNS modification on this server. + required: true + key_name: + description: + - Use TSIG key name to authenticate against DNS C(server) + key_secret: + description: + - Use TSIG key secret, associated with C(key_name), to authenticate against C(server) + default: 7911 + key_algorithm: + description: + - Specify key algorithm used by C(key_secret). + choices: ['HMAC-MD5.SIG-ALG.REG.INT', 'hmac-md5', 'hmac-sha1', 'hmac-sha224', 'hmac-sha256', 'hamc-sha384', + 'hmac-sha512'] + zone: + description: + - DNS record will be modified on this C(zone). + required: true + record: + description: + - Sets the DNS record to modify. + required: true + type: + description: + - Sets the record type. + default: 'A' + ttl: + description: + - Sets the record TTL. + default: 3600 + value: + description: + - Sets the record value. + default: None + +''' + +EXAMPLES = ''' +- name: Add or modify ansible.example.org A to 192.168.1.1" + nsupdate: + key_name: "nsupdate" + key_secret: "+bFQtBCta7j2vWkjPkAFtgA==" + server: "10.1.1.1" + zone: "example.org" + record: "ansible" + value: "192.168.1.1" + +- name: Remove puppet.example.org CNAME + nsupdate: + key_name: "nsupdate" + key_secret: "+bFQtBCta7j2vWkjPkAFtgA==" + server: "10.1.1.1" + zone: "example.org" + record: "puppet" + type: "CNAME" +''' + +RETURN = ''' +changed: + description: If module has modified record + returned: success + type: string +record: + description: DNS record + returned: success + type: string + sample: 'ansible' +ttl: + description: DNS record TTL + returned: success + type: int + sample: 86400 +type: + description: DNS record type + returned: success + type: string + sample: 'CNAME' +value: + description: DNS record value + returned: success + type: string + sample: '192.168.1.1' +zone: + description: DNS record zone + returned: success + type: string + sample: 'example.org.' +rc: + description: dnspython return code + returned: always + type: int + sample: 4 +rc_str: + description: dnspython return code (string representation) + returned: always + type: string + sample: 'REFUSED' +''' + +from binascii import Error as binascii_error +from socket import error as socket_error + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + +try: + import dns.update + import dns.query + import dns.tsigkeyring + import dns.message + import dns.resolver + + HAVE_DNSPYTHON = True +except ImportError: + HAVE_DNSPYTHON = False + + +class RecordManager(object): + def __init__(self, module): + self.module = module + + if module.params['zone'][-1] != '.': + self.zone = module.params['zone'] + '.' + else: + self.zone = module.params['zone'] + + if module.params['key_name']: + try: + self.keyring = dns.tsigkeyring.from_text({ + module.params['key_name']: module.params['key_secret'] + }) + except TypeError: + module.fail_json(msg='Missing key_secret') + except binascii_error: + e = get_exception() + module.fail_json(msg='TSIG key error: %s' % str(e)) + else: + self.keyring = None + + if module.params['key_algorithm'] == 'hmac-md5': + self.algorithm = 'HMAC-MD5.SIG-ALG.REG.INT' + else: + self.algorithm = module.params['key_algorithm'] + + self.dns_rc = 0 + + def __do_update(self, update): + response = None + try: + response = dns.query.tcp(update, self.module.params['server'], timeout=10) + except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature): + e = get_exception() + self.module.fail_json(msg='TSIG update error (%s): %s' % (e.__class__.__name__, str(e))) + except (socket_error, dns.exception.Timeout): + e = get_exception() + self.module.fail_json(msg='DNS server error: (%s): %s' % (e.__class__.__name__, str(e))) + return response + + def create_or_update_record(self): + result = {'changed': False, 'failed': False} + + exists = self.record_exists() + if exists in [0, 2]: + if self.module.check_mode: + self.module.exit_json(changed=True) + + if exists == 0: + self.dns_rc = self.create_record() + if self.dns_rc != 0: + result['msg'] = "Failed to create DNS record (rc: %d)" % self.dns_rc + + elif exists == 2: + self.dns_rc = self.modify_record() + if self.dns_rc != 0: + result['msg'] = "Failed to update DNS record (rc: %d)" % self.dns_rc + else: + result['changed'] = False + + if self.dns_rc != 0: + result['failed'] = True + else: + result['changed'] = True + + return result + + def create_record(self): + update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm) + try: + update.add(self.module.params['record'], + self.module.params['ttl'], + self.module.params['type'], + self.module.params['value']) + except AttributeError: + self.module.fail_json(msg='value needed when state=present') + except dns.exception.SyntaxError: + self.module.fail_json(msg='Invalid/malformed value') + + response = self.__do_update(update) + return dns.message.Message.rcode(response) + + def modify_record(self): + update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm) + update.replace(self.module.params['record'], + self.module.params['ttl'], + self.module.params['type'], + self.module.params['value']) + + response = self.__do_update(update) + return dns.message.Message.rcode(response) + + def remove_record(self): + result = {'changed': False, 'failed': False} + + if self.record_exists() == 0: + return result + + # Check mode and record exists, declared fake change. + if self.module.check_mode: + self.module.exit_json(changed=True) + + update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm) + update.delete(self.module.params['record'], self.module.params['type']) + + response = self.__do_update(update) + self.dns_rc = dns.message.Message.rcode(response) + + if self.dns_rc != 0: + result['failed'] = True + result['msg'] = "Failed to delete record (rc: %d)" % self.dns_rc + else: + result['changed'] = True + + return result + + def record_exists(self): + update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm) + try: + update.present(self.module.params['record'], self.module.params['type']) + except dns.rdatatype.UnknownRdatatype: + e = get_exception() + self.module.fail_json(msg='Record error: {}'.format(str(e))) + + response = self.__do_update(update) + self.dns_rc = dns.message.Message.rcode(response) + if self.dns_rc == 0: + if self.module.params['state'] == 'absent': + return 1 + try: + update.present(self.module.params['record'], self.module.params['type'], self.module.params['value']) + except AttributeError: + self.module.fail_json(msg='value needed when state=present') + except dns.exception.SyntaxError: + self.module.fail_json(msg='Invalid/malformed value') + response = self.__do_update(update) + self.dns_rc = dns.message.Message.rcode(response) + if self.dns_rc == 0: + return 1 + else: + return 2 + else: + return 0 + + +def main(): + tsig_algs = ['HMAC-MD5.SIG-ALG.REG.INT', 'hmac-md5', 'hmac-sha1', 'hmac-sha224', + 'hmac-sha256', 'hamc-sha384', 'hmac-sha512'] + + module = AnsibleModule( + argument_spec=dict( + state=dict(required=False, default='present', choices=['present', 'absent'], type='str'), + server=dict(required=True, type='str'), + key_name=dict(required=False, type='str'), + key_secret=dict(required=False, type='str', no_log=True), + key_algorithm=dict(required=False, default='hmac-md5', choices=tsig_algs, type='str'), + zone=dict(required=True, type='str'), + record=dict(required=True, type='str'), + type=dict(required=False, default='A', type='str'), + ttl=dict(required=False, default=3600, type='int'), + value=dict(required=False, default=None, type='str') + ), + supports_check_mode=True + ) + + if not HAVE_DNSPYTHON: + module.fail_json(msg='python library dnspython required: pip install dnspython') + + if len(module.params["record"]) == 0: + module.fail_json(msg='record cannot be empty.') + + record = RecordManager(module) + result = {} + if module.params["state"] == 'absent': + result = record.remove_record() + elif module.params["state"] == 'present': + result = record.create_or_update_record() + + result['rc'] = record.dns_rc + result['rc_str'] = dns.rcode.to_text(record.dns_rc) + if result['failed']: + module.fail_json(**result) + else: + result['record'] = {'zone': record.zone, 'record': module.params['record'], 'type': module.params['type'], + 'ttl': module.params['ttl'], 'value': module.params['value']} + + module.exit_json(**result) + + +if __name__ == '__main__': + main()