Repository: ansible-collections/community.general

Issue:
Cannot update "NS" types entries from Bind9 managed DNS zone
### Summary

When running a nsupdate task where the type: "NS" is used, Ansible reports a successful change.

Unfortunately, Bind9 will silently ignore the deletes statement for NS type for a zone and the Ansible change does not work.



### Issue Type

Bug Report

### Component Name

nsupdate

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.5]
  config file = None
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/user/github.Simon-TheUser.community.general/venv/lib/python3.8/site-packages/ansible
  ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/user/github.Simon-TheUser.community.general/venv/bin/ansible
  python version = 3.8.10 (default, Mar 15 2022, 12:22:08) [GCC 9.4.0]
  jinja version = 3.1.2
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
Collection        Version
----------------- -------
community.general 4.8.0  
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

The changes are made against a Bind9.18 DNS Server

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
Before running the test, gather the list of NS entries for a DNS zone.
```console (paste below)
$ dig NS lab @192.168.1.2 +short
ns6.lab.
ns3.lab.
ns2.lab.
ns4.lab.
```

Create a task that does not include the ns3.lab. entry and execute it:

```yaml (paste below)
  tasks:
    - name: New NS entries
      set_fact:
        ns_list:
          - 'ns2.lab.'
          - 'ns4.lab.'
          - 'ns6.lab.'

    - name: Update NS entries
      community.general.nsupdate:
        key_name: '{{ rndc_key_name }}'
        key_secret: '{{ rndc_key }}'
        key_algorithm: 'hmac-sha256'
        server: "192.168.1.2"
        zone: "lab."
        record: "lab."
        value: "{{ ns_list }}"
        type: "NS"
```

```console
$ ansible-playbook test_NS_nsupdate.yml 

PLAY [Test for Dynamic DNS updates of NS entries with Bind9] **********************************************************************************************************************************************************************

TASK [New NS entries] *************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Update NS entries] **********************************************************************************************************************************************************************************************************
changed: [localhost]

PLAY RECAP ************************************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```


### Expected Results

After executing the task, I would expect the new list of NS records to only contain 3 items:

```console (paste below)
$ dig NS lab @192.168.1.2 +short
ns6.lab.
ns2.lab.
ns4.lab.
```

### Actual Results

Running the domain query shows that ns3.lab. is still present as a NS entry:

```console (paste below)
$ dig NS lab @192.168.1.2 +short
ns6.lab.
ns3.lab.
ns2.lab.
ns4.lab.
```

Inside the Bind9 logs, you can see that the delete request for NS records is ignored:
```
09-May-2022 18:00:50.352 client @0x7fe7dd1f9568 192.168.1.3#45458/key rndc_ddns_ansible: updating zone 'lab/IN': attempt to delete all SOA or NS records ignored
```
Bind9 source code reference: https://gitlab.isc.org/isc-projects/bind9/-/blob/v9_18/lib/ns/update.c#L3304


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct
Cannot update "NS" types entries from Bind9 managed DNS zone
### Summary

When running a nsupdate task where the type: "NS" is used, Ansible reports a successful change.

Unfortunately, Bind9 will silently ignore the deletes statement for NS type for a zone and the Ansible change does not work.



### Issue Type

Bug Report

### Component Name

nsupdate

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.5]
  config file = None
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/user/github.Simon-TheUser.community.general/venv/lib/python3.8/site-packages/ansible
  ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/user/github.Simon-TheUser.community.general/venv/bin/ansible
  python version = 3.8.10 (default, Mar 15 2022, 12:22:08) [GCC 9.4.0]
  jinja version = 3.1.2
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
Collection        Version
----------------- -------
community.general 4.8.0  
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

The changes are made against a Bind9.18 DNS Server

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
Before running the test, gather the list of NS entries for a DNS zone.
```console (paste below)
$ dig NS lab @192.168.1.2 +short
ns6.lab.
ns3.lab.
ns2.lab.
ns4.lab.
```

Create a task that does not include the ns3.lab. entry and execute it:

```yaml (paste below)
  tasks:
    - name: New NS entries
      set_fact:
        ns_list:
          - 'ns2.lab.'
          - 'ns4.lab.'
          - 'ns6.lab.'

    - name: Update NS entries
      community.general.nsupdate:
        key_name: '{{ rndc_key_name }}'
        key_secret: '{{ rndc_key }}'
        key_algorithm: 'hmac-sha256'
        server: "192.168.1.2"
        zone: "lab."
        record: "lab."
        value: "{{ ns_list }}"
        type: "NS"
```

```console
$ ansible-playbook test_NS_nsupdate.yml 

PLAY [Test for Dynamic DNS updates of NS entries with Bind9] **********************************************************************************************************************************************************************

TASK [New NS entries] *************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [Update NS entries] **********************************************************************************************************************************************************************************************************
changed: [localhost]

PLAY RECAP ************************************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```


### Expected Results

After executing the task, I would expect the new list of NS records to only contain 3 items:

```console (paste below)
$ dig NS lab @192.168.1.2 +short
ns6.lab.
ns2.lab.
ns4.lab.
```

### Actual Results

Running the domain query shows that ns3.lab. is still present as a NS entry:

```console (paste below)
$ dig NS lab @192.168.1.2 +short
ns6.lab.
ns3.lab.
ns2.lab.
ns4.lab.
```

Inside the Bind9 logs, you can see that the delete request for NS records is ignored:
```
09-May-2022 18:00:50.352 client @0x7fe7dd1f9568 192.168.1.3#45458/key rndc_ddns_ansible: updating zone 'lab/IN': attempt to delete all SOA or NS records ignored
```
Bind9 source code reference: https://gitlab.isc.org/isc-projects/bind9/-/blob/v9_18/lib/ns/update.c#L3304


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/net_tools/nsupdate.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/net_tools/nsupdate.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @nerzhul
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Files identified in the description:
* [`plugins/modules/net_tools/nsupdate.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/net_tools/nsupdate.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @nerzhul
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/net_tools/nsupdate.py b/plugins/modules/net_tools/nsupdate.py
--- a/plugins/modules/net_tools/nsupdate.py
+++ b/plugins/modules/net_tools/nsupdate.py
@@ -339,7 +339,31 @@ def create_record(self):
 
     def modify_record(self):
         update = dns.update.Update(self.zone, keyring=self.keyring, keyalgorithm=self.algorithm)
-        update.delete(self.module.params['record'], self.module.params['type'])
+
+        if self.module.params['type'].upper() == 'NS':
+            # When modifying a NS record, Bind9 silently refuses to delete all the NS entries for a zone:
+            # > 09-May-2022 18:00:50.352 client @0x7fe7dd1f9568 192.168.1.3#45458/key rndc_ddns_ansible:
+            # > updating zone 'lab/IN': attempt to delete all SOA or NS records ignored
+            # https://gitlab.isc.org/isc-projects/bind9/-/blob/v9_18/lib/ns/update.c#L3304
+            # Let's perform dns inserts and updates first, deletes after.
+            query = dns.message.make_query(self.module.params['record'], self.module.params['type'])
+            if self.keyring:
+                query.use_tsig(keyring=self.keyring, algorithm=self.algorithm)
+
+            try:
+                if self.module.params['protocol'] == 'tcp':
+                    lookup = dns.query.tcp(query, self.module.params['server'], timeout=10, port=self.module.params['port'])
+                else:
+                    lookup = dns.query.udp(query, self.module.params['server'], timeout=10, port=self.module.params['port'])
+            except (dns.tsig.PeerBadKey, dns.tsig.PeerBadSignature) as e:
+                self.module.fail_json(msg='TSIG update error (%s): %s' % (e.__class__.__name__, to_native(e)))
+            except (socket_error, dns.exception.Timeout) as e:
+                self.module.fail_json(msg='DNS server error: (%s): %s' % (e.__class__.__name__, to_native(e)))
+
+            entries_to_remove = [n.to_text() for n in lookup.answer[0].items if n.to_text() not in self.value]
+        else:
+            update.delete(self.module.params['record'], self.module.params['type'])
+
         for entry in self.value:
             try:
                 update.add(self.module.params['record'],
@@ -350,6 +374,11 @@ def modify_record(self):
                 self.module.fail_json(msg='value needed when state=present')
             except dns.exception.SyntaxError:
                 self.module.fail_json(msg='Invalid/malformed value')
+
+        if self.module.params['type'].upper() == 'NS':
+            for entry in entries_to_remove:
+                update.delete(self.module.params['record'], self.module.params['type'], entry)
+
         response = self.__do_update(update)
 
         return dns.message.Message.rcode(response)


Test patch: