From e22bf295290bff7d6f845bf76d354a9859296234 Mon Sep 17 00:00:00 2001
From: Rafael Guterres Jeffman <rjeffman@redhat.com>
Date: Mon, 2 Mar 2020 15:58:40 -0300
Subject: [PATCH] New DNSConfig management module

There is a new vaultcontainer management module placed in the plugins folder:

plugins/modules/ipadnsconfig.py

The dnsconfig module allows to modify global DNS configuration.

Here is the documentation for the module:

README-dnsconfig.md

New example playbooks have been added:

playbooks/dnsconfig/set_configuration.yml
playbooks/dnsconfig/disable-global-forwarders.yml
playbooks/dnsconfig/disallow-reverse-sync.yml

New tests for the module:

tests/dnsconfig/test_dnsconfig.yml
---
 README-dnsconfig.md                           | 140 ++++++++++
 README.md                                     |   1 +
 .../dnsconfig/disable-global-forwarders.yml   |   9 +
 playbooks/dnsconfig/disallow-reverse-sync.yml |   9 +
 playbooks/dnsconfig/forwarders-absent.yml     |  13 +
 playbooks/dnsconfig/set-configuration.yml     |  14 +
 plugins/modules/ipadnsconfig.py               | 257 ++++++++++++++++++
 tests/dnsconfig/test_dnsconfig.yml            | 141 ++++++++++
 8 files changed, 584 insertions(+)
 create mode 100644 README-dnsconfig.md
 create mode 100644 playbooks/dnsconfig/disable-global-forwarders.yml
 create mode 100644 playbooks/dnsconfig/disallow-reverse-sync.yml
 create mode 100644 playbooks/dnsconfig/forwarders-absent.yml
 create mode 100644 playbooks/dnsconfig/set-configuration.yml
 create mode 100644 plugins/modules/ipadnsconfig.py
 create mode 100644 tests/dnsconfig/test_dnsconfig.yml

diff --git a/README-dnsconfig.md b/README-dnsconfig.md
new file mode 100644
index 00000000..029ec515
--- /dev/null
+++ b/README-dnsconfig.md
@@ -0,0 +1,140 @@
+DNSConfig module
+============
+
+Description
+-----------
+
+The dnsconfig module allows to modify global DNS configuration.
+
+
+Features
+--------
+* Global DNS configuration
+
+
+Supported FreeIPA Versions
+--------------------------
+
+FreeIPA versions 4.4.0 and up are supported by the ipadnsconfig module.
+
+
+Requirements
+------------
+
+**Controller**
+* Ansible version: 2.8+
+
+**Node**
+* Supported FreeIPA version (see above)
+
+
+Usage
+=====
+
+Example inventory file
+
+```ini
+[ipaserver]
+ipaserver.test.local
+```
+
+Example playbook to set global DNS configuration:
+
+```yaml
+---
+- name: Playbook to handle global DNS configuration
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  # Set dnsconfig.
+  - ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      forward_policy: only
+      allow_sync_ptr: yes
+```
+
+Example playbook to ensure a global forwarder, with a custom port, is absent:
+
+```yaml
+---
+- name: Playbook to handle global DNS configuration
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  # Ensure global forwarder with a custom port is absent.
+  - ipadnsconfig:
+      forwarders:
+          - ip_address: 2001:4860:4860::8888
+            port: 53
+      state: absent
+```
+
+Example playbook to disable global forwarders:
+
+```yaml
+---
+- name: Playbook to disable global DNS forwarders
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  # Disable global forwarders.
+  - ipadnsconfig:
+      forward_policy: none
+```
+
+Example playbook to change global forward policy:
+
+```yaml
+---
+- name: Playbook to change global forward policy
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  # Disable global forwarders.
+  - ipadnsconfig:
+      forward_policy: first
+```
+
+Example playbook to disallow synchronization of forward (A, AAAA) and reverse (PTR) records:
+
+```yaml
+---
+- name: Playbook to disallow reverse synchronization.
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  # Disable global forwarders.
+  - ipadnsconfig:
+      allow_sync_ptr: no
+```
+
+Variables
+=========
+
+ipadnsconfig
+------------
+
+Variable | Description | Required
+-------- | ----------- | --------
+`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
+`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
+`forwarders` | The list of forwarders dicts. Each `forwarders` dict entry has:| no
+&nbsp; | `ip_address` - The IPv4 or IPv6 address of the DNS server. | yes
+&nbsp; | `port` - The custom port that should be used on this server. | no
+`forward_policy` | The global forwarding policy. It can be one of `only`, `first`, or `none`.  | no
+`allow_sync_ptr` | Allow synchronization of forward (A, AAAA) and reverse (PTR) records (bool). | yes
+`state` | The state to ensure. It can be one of `present` or `absent`, default: `present`. | yes
+
+
+Authors
+=======
+
+Rafael Guterres Jeffman
diff --git a/README.md b/README.md
index f5d4c257..ebc4e40f 100644
--- a/README.md
+++ b/README.md
@@ -407,6 +407,7 @@ Roles
 Modules in plugin/modules
 =========================
 
+* [ipadnsconfig](README-dnsconfig.md)
 * [ipagroup](README-group.md)
 * [ipahbacrule](README-hbacrule.md)
 * [ipahbacsvc](README-hbacsvc.md)
diff --git a/playbooks/dnsconfig/disable-global-forwarders.yml b/playbooks/dnsconfig/disable-global-forwarders.yml
new file mode 100644
index 00000000..3b4f638c
--- /dev/null
+++ b/playbooks/dnsconfig/disable-global-forwarders.yml
@@ -0,0 +1,9 @@
+---
+- name: Playbook to disable global DNS forwarders
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Disable global forwarders.
+    ipadnsconfig:
+      forward_policy: none
diff --git a/playbooks/dnsconfig/disallow-reverse-sync.yml b/playbooks/dnsconfig/disallow-reverse-sync.yml
new file mode 100644
index 00000000..e99996ef
--- /dev/null
+++ b/playbooks/dnsconfig/disallow-reverse-sync.yml
@@ -0,0 +1,9 @@
+---
+- name: Playbook to disallow reverse record synchronization.
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Disallow reverse record synchronization.
+    ipadnsconfig:
+      allow_sync_ptr: no
diff --git a/playbooks/dnsconfig/forwarders-absent.yml b/playbooks/dnsconfig/forwarders-absent.yml
new file mode 100644
index 00000000..21a393dd
--- /dev/null
+++ b/playbooks/dnsconfig/forwarders-absent.yml
@@ -0,0 +1,13 @@
+---
+- name: Playbook to handle global DNS configuration
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Set dnsconfig.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      state: absent
diff --git a/playbooks/dnsconfig/set-configuration.yml b/playbooks/dnsconfig/set-configuration.yml
new file mode 100644
index 00000000..17880aaf
--- /dev/null
+++ b/playbooks/dnsconfig/set-configuration.yml
@@ -0,0 +1,14 @@
+---
+- name: Playbook to handle global DNS configuration
+  hosts: ipaserver
+  become: true
+
+  tasks:
+  - name: Set dnsconfig.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      forward_policy: only
+      allow_sync_ptr: yes
diff --git a/plugins/modules/ipadnsconfig.py b/plugins/modules/ipadnsconfig.py
new file mode 100644
index 00000000..4c9cf2d7
--- /dev/null
+++ b/plugins/modules/ipadnsconfig.py
@@ -0,0 +1,257 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Rafael Guterres Jeffman <rjeffman@redhat.com>
+#
+# Copyright (C) 2019 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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.
+#
+# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+ANSIBLE_METADATA = {
+    "metadata_version": "1.0",
+    "supported_by": "community",
+    "status": ["preview"],
+}
+
+DOCUMENTATION = """
+---
+module: ipadnsconfig
+short description: Manage FreeIPA dnsconfig
+description: Manage FreeIPA dnsconfig
+options:
+  ipaadmin_principal:
+    description: The admin principal
+    default: admin
+  ipaadmin_password:
+    description: The admin password
+    required: false
+
+  forwarders:
+    description: The list of global DNS forwarders.
+    required: false
+    options:
+      ip_address:
+        description: The forwarder nameserver IP address list (IPv4 and IPv6).
+        required: true
+      port:
+        description: The port to forward requests to.
+        required: false
+  forward_policy:
+    description:
+      Global forwarding policy. Set to "none" to disable any configured
+      global forwarders.
+    required: false
+    choices: ['only', 'first', 'none']
+  allow_sync_ptr:
+    description:
+      Allow synchronization of forward (A, AAAA) and reverse (PTR) records.
+    required: false
+    type: bool
+  state:
+    description: State to ensure
+    default: present
+    choices: ["present", "absent"]
+"""
+
+EXAMPLES = """
+# Ensure global DNS forward configuration, allowing PTR record synchronization.
+- ipadnsconfig:
+    forwarders:
+      - ip_address: 8.8.4.4
+      - ip_address: 2001:4860:4860::8888
+        port: 53
+    forward_policy: only
+    allow_sync_ptr: yes
+
+# Ensure forwarder is absent.
+- ipadnsconfig:
+    forwarders:
+      - ip_address: 2001:4860:4860::8888
+        port: 53
+    state: absent
+
+# Disable PTR record synchronization.
+- ipadnsconfig:
+    allow_sync_ptr: no
+
+# Disable global forwarders.
+- ipadnsconfig:
+    forward_policy: none
+"""
+
+RETURN = """
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils._text import to_text
+from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
+    temp_kdestroy, valid_creds, api_connect, api_command, \
+    api_command_no_name, compare_args_ipa, module_params_get, \
+    gen_add_del_lists, is_ipv4_addr, is_ipv6_addr, ipalib_errors
+
+
+def find_dnsconfig(module):
+    _args = {
+        "all": True,
+    }
+
+    _result = api_command_no_name(module, "dnsconfig_show", _args)
+
+    if "result" in _result:
+        if _result["result"].get('idnsforwarders', None) is None:
+            _result["result"]['idnsforwarders'] = ['']
+        return _result["result"]
+    else:
+        module.fail("Could not retrieve current DNS configuration.")
+    return None
+
+
+def gen_args(module, state, dnsconfig, forwarders, forward_policy,
+             allow_sync_ptr):
+    _args = {}
+
+    if forwarders:
+        _forwarders = []
+        for forwarder in forwarders:
+            ip_address = forwarder.get('ip_address')
+            port = forwarder.get('port')
+            if not (is_ipv4_addr(ip_address) or is_ipv6_addr(ip_address)):
+                module.fail(
+                    msg="Invalid IP for DNS forwarder: %s" % ip_address)
+            if port is None:
+                _forwarders.append(ip_address)
+            else:
+                _forwarders.append('%s port %d' % (ip_address, port))
+
+        global_forwarders = dnsconfig.get('idnsforwarders', [])
+        if state == 'absent':
+            _args['idnsforwarders'] = [
+                fwd for fwd in global_forwarders if fwd not in _forwarders]
+            # When all forwarders should be excluded, use an empty string ('').
+            if not _args['idnsforwarders']:
+                _args['idnsforwarders'] = ['']
+
+        elif state == 'present':
+            _args['idnsforwarders'] = [
+                fwd for fwd in _forwarders if fwd not in global_forwarders]
+            # If no forwarders should be added, remove argument.
+            if not _args['idnsforwarders']:
+                del _args['idnsforwarders']
+
+        else:
+            # shouldn't happen, but let's be paranoid.
+            module.fail(msg="Invalid state: %s" % state)
+
+    if forward_policy is not None:
+        _args['idnsforwardpolicy'] = forward_policy
+
+    if allow_sync_ptr is not None:
+        _args['idnsallowsyncptr'] = 'TRUE' if allow_sync_ptr else 'FALSE'
+
+    return _args
+
+
+def main():
+    forwarder_spec = dict(
+       ip_address=dict(type=str, required=True),
+       port=dict(type=int, required=False, default=None)
+    )
+
+    ansible_module = AnsibleModule(
+       argument_spec=dict(
+           # general
+           ipaadmin_principal=dict(type='str', default='admin'),
+           ipaadmin_password=dict(type='str', no_log=True),
+
+           # dnsconfig
+           forwarders=dict(type='list', default=None, required=False,
+                           options=dict(**forwarder_spec)),
+           forward_policy=dict(type='str', required=False, default=None,
+                               choices=['only', 'first', 'none']),
+           allow_sync_ptr=dict(type='bool', required=False, default=None),
+
+           # general
+           state=dict(type="str", default="present",
+                      choices=["present", "absent"]),
+
+       )
+    )
+
+    ansible_module._ansible_debug = True
+
+    # general
+    ipaadmin_principal = module_params_get(ansible_module,
+                                           "ipaadmin_principal")
+    ipaadmin_password = module_params_get(ansible_module,
+                                          "ipaadmin_password")
+
+    forwarders = module_params_get(ansible_module, 'forwarders') or []
+    forward_policy = module_params_get(ansible_module, 'forward_policy')
+    allow_sync_ptr = module_params_get(ansible_module, 'allow_sync_ptr')
+
+    state = module_params_get(ansible_module, 'state')
+
+    # Check parameters.
+    invalid = []
+    if state == 'absent':
+        invalid = ['forward_policy', 'allow_sync_ptr']
+
+    for x in invalid:
+        if vars()[x] is not None:
+            ansible_module.fail_json(
+                msg="Argument '%s' can not be used with state '%s'" %
+                (x, state))
+
+    # Init
+
+    changed = False
+    ccache_dir = None
+    ccache_name = None
+    try:
+        if not valid_creds(ansible_module, ipaadmin_principal):
+            ccache_dir, ccache_name = temp_kinit(ipaadmin_principal,
+                                                 ipaadmin_password)
+        api_connect()
+
+        res_find = find_dnsconfig(ansible_module)
+        args = gen_args(ansible_module, state, res_find, forwarders,
+                        forward_policy, allow_sync_ptr)
+
+        # Execute command only if configuration changes.
+        if not compare_args_ipa(ansible_module, args, res_find):
+            try:
+                api_command_no_name(ansible_module, 'dnsconfig_mod', args)
+                # If command did not fail, something changed.
+                changed = True
+
+            except Exception as e:
+                msg = str(e)
+                ansible_module.fail_json(msg="dnsconfig_mod: %s" % msg)
+
+    except Exception as e:
+        ansible_module.fail_json(msg=str(e))
+
+    finally:
+        temp_kdestroy(ccache_dir, ccache_name)
+
+    # Done
+
+    ansible_module.exit_json(changed=changed)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/dnsconfig/test_dnsconfig.yml b/tests/dnsconfig/test_dnsconfig.yml
new file mode 100644
index 00000000..1e1b1094
--- /dev/null
+++ b/tests/dnsconfig/test_dnsconfig.yml
@@ -0,0 +1,141 @@
+---
+- name: Test dnsconfig
+  hosts: ipaserver
+  become: true
+  gather_facts: true
+
+  tasks:
+  # Setup.
+  - name: Ensure forwarders are absent.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      state: absent
+
+  # Tests.
+
+  - name: Set dnsconfig.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      forward_policy: only
+      allow_sync_ptr: yes
+    register: result
+    failed_when: not result.changed
+
+  - name: Set dnsconfig, with the same values.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      forward_policy: only
+      allow_sync_ptr: yes
+    register: result
+    failed_when: result.changed
+
+  - name: Ensure forwarder is absent.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+      state: absent
+    register: result
+    failed_when: not result.changed
+
+  - name: Ensure forwarder is absent, again.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+      state: absent
+    register: result
+    failed_when: result.changed
+
+  - name: Disable global forwarders.
+    ipadnsconfig:
+        forward_policy: none
+    register: result
+    failed_when: not result.changed
+
+  - name: Disable global forwarders, again.
+    ipadnsconfig:
+        forward_policy: none
+    register: result
+    failed_when: result.changed
+
+  - name: Re-enable global forwarders.
+    ipadnsconfig:
+        forward_policy: first
+    register: result
+    failed_when: not result.changed
+
+  - name: Re-enable global forwarders, again.
+    ipadnsconfig:
+        forward_policy: first
+    register: result
+    failed_when: result.changed
+
+  - name: Disable PTR record synchronization.
+    ipadnsconfig:
+      allow_sync_ptr: no
+    register: result
+    failed_when: not result.changed
+
+  - name: Disable PTR record synchronization, again.
+    ipadnsconfig:
+      allow_sync_ptr: no
+    register: result
+    failed_when: result.changed
+
+  - name: Re-enable PTR record synchronization.
+    ipadnsconfig:
+      allow_sync_ptr: yes
+    register: result
+    failed_when: not result.changed
+
+  - name: Re-enable PTR record synchronization, again.
+    ipadnsconfig:
+      allow_sync_ptr: yes
+    register: result
+    failed_when: result.changed
+
+  - name: Ensure all forwarders are absent.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      state: absent
+    register: result
+    failed_when: not result.changed
+
+
+  - name: Ensure all forwarders are absent, again.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      state: absent
+    register: result
+    failed_when: result.changed
+
+  # Cleanup.
+  - name: Ensure forwarders are absent.
+    ipadnsconfig:
+      forwarders:
+        - ip_address: 8.8.8.8
+        - ip_address: 8.8.4.4
+        - ip_address: 2001:4860:4860::8888
+        - ip_address: 2001:4860:4860::8888
+          port: 53
+      state: absent
-- 
GitLab