From 5cb9a9e4f0ff68670e5aa285263c01227b03d8af Mon Sep 17 00:00:00 2001
From: Simon-TheUser <35318753+Simon-TheUser@users.noreply.github.com>
Date: Wed, 2 Nov 2022 15:13:50 -0400
Subject: [PATCH] nsupdate: issues/4657 (#5377)

* Insert new entries before deleting old ones.
resolves #4657

* Slight wording changes.

* lint fix

* Address lint

* Added changelog
Fixed lint

* More linting

* Update changelogs/fragments/5377-nsupdate-ns-records-with-bind.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
---
 .../5377-nsupdate-ns-records-with-bind.yml    |  2 ++
 plugins/modules/net_tools/nsupdate.py         | 31 ++++++++++++++++++-
 2 files changed, 32 insertions(+), 1 deletion(-)
 create mode 100644 changelogs/fragments/5377-nsupdate-ns-records-with-bind.yml

diff --git a/changelogs/fragments/5377-nsupdate-ns-records-with-bind.yml b/changelogs/fragments/5377-nsupdate-ns-records-with-bind.yml
new file mode 100644
index 0000000000..c414ddc4bf
--- /dev/null
+++ b/changelogs/fragments/5377-nsupdate-ns-records-with-bind.yml
@@ -0,0 +1,2 @@
+bugfixes:
+  - nsupdate - fix silent failures when updating ``NS`` entries from Bind9 managed DNS zones (https://github.com/ansible-collections/community.general/issues/4657).
diff --git a/plugins/modules/net_tools/nsupdate.py b/plugins/modules/net_tools/nsupdate.py
index 512e44da2f..43b951fe61 100644
--- a/plugins/modules/net_tools/nsupdate.py
+++ b/plugins/modules/net_tools/nsupdate.py
@@ -339,7 +339,31 @@ class RecordManager(object):
 
     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 @@ class RecordManager(object):
                 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)