From 1a8bbcf14674c3c0efb49e0d276233b2dfa33bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 19 Jan 2018 09:34:23 +0100 Subject: [PATCH] vultr: new module vr_dns_record (#34423) --- .../modules/cloud/vultr/vr_dns_record.py | 356 ++++++++++++++++++ .../roles/vr_dns_record/defaults/main.yml | 36 ++ .../vr_dns_record/tasks/create_record.yml | 65 ++++ .../legacy/roles/vr_dns_record/tasks/main.yml | 15 + .../roles/vr_dns_record/tasks/record.yml | 4 + .../vr_dns_record/tasks/remove_record.yml | 112 ++++++ .../tasks/test_fail_multiple.yml | 76 ++++ .../vr_dns_record/tasks/update_record.yml | 68 ++++ 8 files changed, 732 insertions(+) create mode 100644 lib/ansible/modules/cloud/vultr/vr_dns_record.py create mode 100644 test/legacy/roles/vr_dns_record/defaults/main.yml create mode 100644 test/legacy/roles/vr_dns_record/tasks/create_record.yml create mode 100644 test/legacy/roles/vr_dns_record/tasks/main.yml create mode 100644 test/legacy/roles/vr_dns_record/tasks/record.yml create mode 100644 test/legacy/roles/vr_dns_record/tasks/remove_record.yml create mode 100644 test/legacy/roles/vr_dns_record/tasks/test_fail_multiple.yml create mode 100644 test/legacy/roles/vr_dns_record/tasks/update_record.yml diff --git a/lib/ansible/modules/cloud/vultr/vr_dns_record.py b/lib/ansible/modules/cloud/vultr/vr_dns_record.py new file mode 100644 index 00000000000..9e1b0fceaad --- /dev/null +++ b/lib/ansible/modules/cloud/vultr/vr_dns_record.py @@ -0,0 +1,356 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2017, René Moser +# 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: vr_dns_record +short_description: Manages DNS records on Vultr. +description: + - Create, update and remove DNS records. +version_added: "2.5" +author: "René Moser (@resmo)" +options: + name: + description: + - The record name (subrecord). + default: "" + aliases: [ subrecord ] + domain: + description: + - The domain the record is related to. + required: true + record_type: + description: + - Type of the record. + default: A + choices: + - A + - AAAA + - CNAME + - MX + - SRV + - ALIAS + - SPF + - TXT + - NS + aliases: [ type ] + data: + description: + - Data of the record. + - Required if C(state=present) or C(multiple=yes). + ttl: + description: + - TTL of the record. + default: 300 + multiple: + description: + - Whether to use more than one record with similar C(name) including no name and C(record_type). + - Only allowed for a few record types, e.g. C(record_type=A), C(record_type=NS) or C(record_type=MX). + - C(data) will not be updated, instead it is used as a key to find existing records. + default: no + type: bool + priority: + description: + - Priority of the record. + default: 0 + state: + description: + - State of the DNS record. + default: present + choices: [ present, absent ] +extends_documentation_fragment: vultr +''' + +EXAMPLES = ''' +- name: Ensure an A record exists + vr_dns_record: + name: www + domain: example.com + data: 10.10.10.10 + ttl: 3600 + +- name: Ensure a second A record exists for round robin LB + vr_dns_record: + name: www + domain: example.com + data: 10.10.10.11 + ttl: 60 + multiple: yes + +- name: Ensure a CNAME record exists + vr_dns_record: + name: web + record_type: CNAME + domain: example.com + data: www.example.com + +- name: Ensure MX record exists + vr_dns_record: + record_type: MX + domain: example.com + data: "{{ item.data }}" + priority: "{{ item.priority }}" + multiple: yes + with_items: + - { data: mx1.example.com, priority: 10 } + - { data: mx2.example.com, priority: 10 } + - { data: mx3.example.com, priority: 20 } + +- name: Ensure a record is absent + local_action: + module: vr_dns_record + name: www + domain: example.com + state: absent + +- name: Ensure MX record is absent in case multiple exists + vr_dns_record: + record_type: MX + domain: example.com + data: mx1.example.com + multiple: yes + state: absent +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: string + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 +vultr_dns_record: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: The ID of the DNS record. + returned: success + type: int + sample: 1265277 + name: + description: The name of the DNS record. + returned: success + type: string + sample: web + record_type: + description: The name of the DNS record. + returned: success + type: string + sample: web + data: + description: Data of the DNS record. + returned: success + type: string + sample: 10.10.10.10 + domain: + description: Domain the DNS record is related to. + returned: success + type: string + sample: example.com + priority: + description: Priority of the DNS record. + returned: success + type: int + sample: 10 + ttl: + description: Time to live of the DNS record. + returned: success + type: int + sample: 300 +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + +RECORD_TYPES = [ + 'A', + 'AAAA', + 'CNAME', + 'MX', + 'TXT', + 'NS', + 'SRV', + 'CAA', + 'SSHFP' +] + + +class AnsibleVultrDnsRecord(Vultr): + + def __init__(self, module): + super(AnsibleVultrDnsRecord, self).__init__(module, "vultr_dns_record") + + self.returns = { + 'RECORDID': dict(key='id'), + 'name': dict(), + 'record': dict(), + 'priority': dict(), + 'data': dict(), + 'type': dict(key='record_type'), + 'ttl': dict(), + } + + def get_record(self): + records = self.api_query(path="/v1/dns/records?domain=%s" % self.module.params.get('domain')) + + multiple = self.module.params.get('multiple') + data = self.module.params.get('data') + name = self.module.params.get('name') + record_type = self.module.params.get('record_type') + + result = {} + for record in records or []: + if record.get('type') != record_type: + continue + + if record.get('name') == name: + if not multiple: + if result: + self.module.fail_json(msg="More than one record with record_type=%s and name=%s params. " + "Use multiple=yes for more than one record." % (record_type, name)) + else: + result = record + elif record.get('data') == data: + return record + + return result + + def present_record(self): + record = self.get_record() + if not record: + record = self._create_record(record) + else: + record = self._update_record(record) + return record + + def _create_record(self, record): + self.result['changed'] = True + data = { + 'name': self.module.params.get('name'), + 'domain': self.module.params.get('domain'), + 'data': self.module.params.get('data'), + 'type': self.module.params.get('record_type'), + 'priority': self.module.params.get('priority'), + 'ttl': self.module.params.get('ttl'), + } + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/create_record", + method="POST", + data=data + ) + record = self.get_record() + return record + + def _update_record(self, record): + data = { + 'RECORDID': record['RECORDID'], + 'name': self.module.params.get('name'), + 'domain': self.module.params.get('domain'), + 'data': self.module.params.get('data'), + 'type': self.module.params.get('record_type'), + 'priority': self.module.params.get('priority'), + 'ttl': self.module.params.get('ttl'), + } + has_changed = [k for k in data if k in record and data[k] != record[k]] + if has_changed: + self.result['changed'] = True + + self.result['diff']['before'] = record + self.result['diff']['after'] = record.copy() + self.result['diff']['after'].update(data) + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/update_record", + method="POST", + data=data + ) + record = self.get_record() + return record + + def absent_record(self): + record = self.get_record() + if record: + self.result['changed'] = True + + data = { + 'RECORDID': record['RECORDID'], + 'domain': self.module.params.get('domain'), + } + + self.result['diff']['before'] = record + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/delete_record", + method="POST", + data=data + ) + return record + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + domain=dict(required=True), + name=dict(default="", aliases=['subrecord']), + state=dict(choices=['present', 'absent'], default='present'), + ttl=dict(type='int', default=300), + record_type=dict(choices=RECORD_TYPES, default='A', aliases=['type']), + multiple=dict(type='bool', default=False), + priority=dict(type='int', default=0), + data=dict() + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['data']), + ('multiple', True, ['data']), + ], + + supports_check_mode=True, + ) + + vr_record = AnsibleVultrDnsRecord(module) + if module.params.get('state') == "absent": + record = vr_record.absent_record() + else: + record = vr_record.present_record() + + result = vr_record.get_result(record) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/legacy/roles/vr_dns_record/defaults/main.yml b/test/legacy/roles/vr_dns_record/defaults/main.yml new file mode 100644 index 00000000000..cef5bab1d17 --- /dev/null +++ b/test/legacy/roles/vr_dns_record/defaults/main.yml @@ -0,0 +1,36 @@ +--- +vr_dns_domain_name: example-ansible.com +vr_dns_record_items: +# Single A record +- name: test-www + data: 10.10.10.10 + ttl: 400 + update_data: 10.10.10.11 + update_ttl: 200 + +# Multiple A records +- name: test-www-multiple + data: 10.10.11.10 + update_data: 10.10.11.11 + multiple: true + update_ttl: 600 + +# CNAME +- name: test-cname + data: www.ansible.com + update_data: www.ansible.ch + record_type: CNAME + +# Single Multiple MX record +- data: mx1.example-ansible.com + priority: 10 + update_priority: 20 + record_type: MX + +# Multiple MX records +- data: mx2.example-ansible.com + priority: 10 + update_data: mx1.example-ansible.com + update_priority: 20 + record_type: MX + multiple: true diff --git a/test/legacy/roles/vr_dns_record/tasks/create_record.yml b/test/legacy/roles/vr_dns_record/tasks/create_record.yml new file mode 100644 index 00000000000..f1c2eb81207 --- /dev/null +++ b/test/legacy/roles/vr_dns_record/tasks/create_record.yml @@ -0,0 +1,65 @@ +--- +- name: test setup dns record + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + record_type: "{{ item.record_type | default(omit) }}" + state: absent + register: result +- name: verify test setup dns record + assert: + that: + - result is successful + +- name: test create a dns record in check mode + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.data }}" + ttl: "{{ item.ttl | default(omit) }}" + record_type: "{{ item.record_type | default(omit) }}" + priority: "{{ item.priority | default(omit) }}" + check_mode: yes + register: result +- name: verify test create a dns record in check mode + assert: + that: + - result is changed + +- name: test create a dns record + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.data }}" + ttl: "{{ item.ttl | default(omit) }}" + record_type: "{{ item.record_type | default(omit) }}" + priority: "{{ item.priority | default(omit) }}" + register: result +- name: verify test create a dns record + assert: + that: + - result is changed + - result.vultr_dns_record.data == "{{ item.data }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.ttl == {{ item.ttl | default(300) }} + - result.vultr_dns_record.priority == {{ item.priority | default(0) }} + +- name: test create a dns record idempotence + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.data }}" + ttl: "{{ item.ttl | default(omit) }}" + record_type: "{{ item.record_type | default(omit) }}" + priority: "{{ item.priority | default(omit) }}" + register: result +- name: verify test create a dns record idempotence + assert: + that: + - result is not changed + - result.vultr_dns_record.data == "{{ item.data }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.ttl == {{ item.ttl | default(300) }} + - result.vultr_dns_record.priority == {{ item.priority | default(0) }} diff --git a/test/legacy/roles/vr_dns_record/tasks/main.yml b/test/legacy/roles/vr_dns_record/tasks/main.yml new file mode 100644 index 00000000000..e9449e66727 --- /dev/null +++ b/test/legacy/roles/vr_dns_record/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- name: setup dns domain + vr_dns_domain: + name: "{{ vr_dns_domain_name }}" + server_ip: 10.10.10.10 + register: result +- name: verify setup dns domain + assert: + that: + - result is successful + +- include_tasks: test_fail_multiple.yml + +- include_tasks: record.yml + with_items: "{{ vr_dns_record_items }}" diff --git a/test/legacy/roles/vr_dns_record/tasks/record.yml b/test/legacy/roles/vr_dns_record/tasks/record.yml new file mode 100644 index 00000000000..620a4872203 --- /dev/null +++ b/test/legacy/roles/vr_dns_record/tasks/record.yml @@ -0,0 +1,4 @@ +--- +- include_tasks: create_record.yml +- include_tasks: update_record.yml +- include_tasks: remove_record.yml diff --git a/test/legacy/roles/vr_dns_record/tasks/remove_record.yml b/test/legacy/roles/vr_dns_record/tasks/remove_record.yml new file mode 100644 index 00000000000..0e2b85663a1 --- /dev/null +++ b/test/legacy/roles/vr_dns_record/tasks/remove_record.yml @@ -0,0 +1,112 @@ +--- +- name: test remove a dns record in check mode + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.update_data | default(item.data) }}" + record_type: "{{ item.record_type | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + state: absent + check_mode: yes + register: result +- name: verify test remove a dns record in check mode + assert: + that: + - result is changed + - result.vultr_dns_record.data == "{{ item.update_data | default(item.data) }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.ttl == {{ item.update_ttl | default(300) }} + - result.vultr_dns_record.priority == {{ item.update_priority | default(item.priority | default(0)) }} + +- name: test remove second dns record in check mode + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.data | default(item.data) }}" + record_type: "{{ item.record_type | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + state: absent + check_mode: yes + register: result + when: item.multiple is defined and item.multiple == true +- name: verify test remove a dns record in check mode + assert: + that: + - result is changed + - result.vultr_dns_record.data == "{{ item.data | default(item.data) }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.ttl == {{ item.ttl | default(300) }} + - result.vultr_dns_record.priority == {{ item.priority | default(0) }} + when: item.multiple is defined and item.multiple == true + +- name: test remove a dns record + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.update_data | default(item.data) }}" + record_type: "{{ item.record_type | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + state: absent + register: result +- name: verify test remove a dns record + assert: + that: + - result is changed + - result.vultr_dns_record.data == "{{ item.update_data | default(item.data) }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.ttl == {{ item.update_ttl | default(300) }} + - result.vultr_dns_record.priority == {{ item.update_priority | default(item.priority | default(0)) }} + +- name: test remove second dns record + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.data }}" + record_type: "{{ item.record_type | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + state: absent + register: result + when: item.multiple is defined and item.multiple == true +- name: verify test remove a dns record + assert: + that: + - result is changed + - result.vultr_dns_record.data == "{{ item.data }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.ttl == {{ item.ttl | default(300) }} + - result.vultr_dns_record.priority == {{ item.priority | default(0) }} + when: item.multiple is defined and item.multiple == true + +- name: test remove a dns record idempotence + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.update_data | default(item.data) }}" + record_type: "{{ item.record_type | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + state: absent + register: result +- name: verify test remove a dns record idempotence + assert: + that: + - result is not changed + +- name: test remove second dns record idempotence + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.data }}" + record_type: "{{ item.record_type | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + state: absent + register: result + when: item.multiple is defined and item.multiple == true +- name: verify test remove a dns record idempotence + assert: + that: + - result is not changed + when: item.multiple is defined and item.multiple == true diff --git a/test/legacy/roles/vr_dns_record/tasks/test_fail_multiple.yml b/test/legacy/roles/vr_dns_record/tasks/test_fail_multiple.yml new file mode 100644 index 00000000000..58bec1ce1b1 --- /dev/null +++ b/test/legacy/roles/vr_dns_record/tasks/test_fail_multiple.yml @@ -0,0 +1,76 @@ +--- +- name: setup first dns record + vr_dns_record: + name: test-multiple + domain: "{{ vr_dns_domain_name }}" + data: 1.2.3.4 + multiple: yes + register: result +- name: verify setup a dns record + assert: + that: + - result is successful + +- name: setup second dns record + vr_dns_record: + name: test-multiple + domain: "{{ vr_dns_domain_name }}" + data: 1.2.3.5 + multiple: yes + register: result +- name: verify setup second dns record + assert: + that: + - result is successful + +- name: test-multiple fail multiple identical records found + vr_dns_record: + name: test-multiple + domain: "{{ vr_dns_domain_name }}" + state: absent + register: result + ignore_errors: yes +- name: verify test fail multiple identical records found + assert: + that: + - result is failed + +- name: test-multiple fail absent multiple identical records but not data + vr_dns_record: + name: test-multiple + domain: "{{ vr_dns_domain_name }}" + state: absent + multiple: yes + register: result + ignore_errors: yes +- name: verify test-multiple success absent multiple identical records found + assert: + that: + - result is failed + - "result.msg == 'multiple is True but all of the following are missing: data'" + +- name: test-multiple success absent multiple identical records second found + vr_dns_record: + name: test-multiple + domain: "{{ vr_dns_domain_name }}" + data: 1.2.3.5 + state: absent + multiple: yes + register: result +- name: verify test-multiple success absent multiple identical records second found + assert: + that: + - result is changed + +- name: test-multiple success absent multiple identical records first found + vr_dns_record: + name: test-multiple + domain: "{{ vr_dns_domain_name }}" + data: 1.2.3.4 + state: absent + multiple: yes + register: result +- name: verify test-multiple success absent multiple identical records firstfound + assert: + that: + - result is changed diff --git a/test/legacy/roles/vr_dns_record/tasks/update_record.yml b/test/legacy/roles/vr_dns_record/tasks/update_record.yml new file mode 100644 index 00000000000..eb8e3c8e0a3 --- /dev/null +++ b/test/legacy/roles/vr_dns_record/tasks/update_record.yml @@ -0,0 +1,68 @@ +--- +- name: test update or add another dns record in check mode + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.update_data | default(item.data) }}" + ttl: "{{ item.update_ttl | default(omit) }}" + record_type: "{{ item.record_type | default(omit) }}" + priority: "{{ item.update_priority | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + check_mode: yes + register: result +- name: verify test updatein check mode + assert: + that: + - result is changed + - result.vultr_dns_record.data == "{{ item.data }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.ttl == {{ item.ttl | default(300) }} + - result.vultr_dns_record.priority == {{ item.priority | default(0) }} + when: item.multiple is undefined or item.multiple == false +- name: verify test add another dns record in check mode + assert: + that: + - result is changed + - not result.vultr_dns_record + when: item.multiple is defined and item.multiple == true + +- name: test update or add another dns record + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.update_data | default(item.data) }}" + ttl: "{{ item.update_ttl | default(omit) }}" + record_type: "{{ item.record_type | default(omit) }}" + priority: "{{ item.update_priority | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + register: result +- name: verify test update a dns record + assert: + that: + - result is changed + - result.vultr_dns_record.data == "{{ item.update_data | default(item.data) }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.ttl == {{ item.update_ttl | default(300) }} + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.priority == {{ item.update_priority | default(0) }} + +- name: test update or add another dns record idempotence + vr_dns_record: + name: "{{ item.name | default(omit) }}" + domain: "{{ vr_dns_domain_name }}" + data: "{{ item.update_data | default(item.data) }}" + ttl: "{{ item.update_ttl | default(omit) }}" + record_type: "{{ item.record_type | default(omit) }}" + priority: "{{ item.update_priority | default(omit) }}" + multiple: "{{ item.multiple | default(omit) }}" + register: result +- name: verify test update a dns record idempotence + assert: + that: + - result is not changed + - result.vultr_dns_record.data == "{{ item.update_data | default(item.data) }}" + - result.vultr_dns_record.name == "{{ item.name | default("") }}" + - result.vultr_dns_record.ttl == {{ item.update_ttl | default(300) }} + - result.vultr_dns_record.record_type == "{{ item.record_type | default('A') }}" + - result.vultr_dns_record.priority == {{ item.update_priority | default(0) }}