Skip to content

Commit 76725f0

Browse files
committed
chore(api): clean knot.py: add docs
1 parent fd4a163 commit 76725f0

1 file changed

Lines changed: 25 additions & 18 deletions

File tree

api/desecapi/knot.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
"""Knot DNS backend helpers for catalog updates, DNSSEC, and transfers."""
2+
13
from functools import lru_cache
24
from hashlib import sha1
35
import logging
46
import os
57
import socket
68
import select
7-
import threading
89
import time
910

1011
import dns.dnssec
@@ -43,6 +44,7 @@
4344

4445

4546
def _tsig_algorithm(name):
47+
"""Return a dnspython TSIG algorithm constant for the configured name."""
4648
if not name:
4749
return None
4850
algorithm = _TSIG_ALGORITHM_MAP.get(name.lower())
@@ -53,6 +55,7 @@ def _tsig_algorithm(name):
5355

5456
@lru_cache(maxsize=1)
5557
def _knot_host_ip():
58+
"""Resolve NSLORD_KNOT_HOST to a concrete IP address (IPv4/IPv6)."""
5659
host = settings.NSLORD_KNOT_HOST
5760
try:
5861
dns.inet.af_for_address(host)
@@ -75,6 +78,7 @@ def _knot_host_ip():
7578

7679

7780
def _update_keyring():
81+
"""Return TSIG keyring/name/algorithm tuple for dynamic updates."""
7882
key_name = settings.NSLORD_KNOT_UPDATE_KEY_NAME
7983
key_secret = settings.NSLORD_KNOT_UPDATE_KEY_SECRET
8084
if not key_name or not key_secret:
@@ -88,6 +92,7 @@ def _update_keyring():
8892

8993

9094
def _transfer_keyring():
95+
"""Return TSIG keyring/name/algorithm tuple for AXFR/IXFR transfers."""
9196
key_name = settings.NSLORD_KNOT_TRANSFER_KEY_NAME
9297
key_secret = settings.NSLORD_KNOT_TRANSFER_KEY_SECRET
9398
if not key_name or not key_secret:
@@ -132,15 +137,18 @@ def _send_update(update: dns.update.Update) -> None:
132137

133138

134139
def _catalog_member_subname(zone):
140+
"""Return catalog member label for a zone name (stable hash)."""
135141
zone = zone.rstrip(".") + "."
136142
return f"{sha1(zone.encode()).hexdigest()}.zones"
137143

138144

139145
def _catalog_record_name(zone):
146+
"""Return the FQDN of the catalog member PTR record for a zone."""
140147
return f"{_catalog_member_subname(zone)}.{settings.CATALOG_ZONE}".strip(".") + "."
141148

142149

143150
def _new_update(zone):
151+
"""Create a dnspython Update with configured TSIG for a zone."""
144152
keyring, keyname, keyalgorithm = _update_keyring()
145153
return dns.update.Update(
146154
zone,
@@ -178,6 +186,7 @@ def ensure_default_ns(name):
178186

179187

180188
def _write_bind_keypair(name, dnskey, private_key):
189+
"""Write BIND-style DNSKEY + private key files for Knot import."""
181190
import_dir = settings.NSLORD_KNOT_IMPORT_DIR
182191
if not import_dir or not private_key:
183192
return None
@@ -201,6 +210,7 @@ def _write_bind_keypair(name, dnskey, private_key):
201210

202211

203212
def _key_ready_path(name):
213+
"""Return the path of the Knot CSK import readiness marker file."""
204214
import_dir = settings.NSLORD_KNOT_IMPORT_DIR
205215
if not import_dir:
206216
return None
@@ -209,12 +219,14 @@ def _key_ready_path(name):
209219

210220

211221
def prepare_csk_key(name, *, dnskey, private_key=None):
222+
"""Prepare a CSK keypair for Knot import without triggering any update."""
212223
if not private_key:
213224
return
214225
_write_bind_keypair(name, dnskey, private_key)
215226

216227

217228
def wait_for_csk_key_ready(name):
229+
"""Wait for Knot to signal completion of CSK import via .ready file."""
218230
ready_path = _key_ready_path(name)
219231
if not ready_path:
220232
return
@@ -228,6 +240,7 @@ def wait_for_csk_key_ready(name):
228240

229241

230242
def _dnskey_present(name, dnskey):
243+
"""Check whether a specific DNSKEY RR appears in the zone."""
231244
query = dns.message.make_query(name, dns.rdatatype.DNSKEY, want_dnssec=True)
232245
response = dns.query.tcp(
233246
query,
@@ -245,6 +258,7 @@ def _dnskey_present(name, dnskey):
245258

246259

247260
def _wait_for_dnskey(name, dnskey, *, attempts: int = 20, delay_seconds: float = 0.2):
261+
"""Poll for the presence of a DNSKEY RR, with bounded retries."""
248262
for _ in range(attempts):
249263
if _dnskey_present(name, dnskey):
250264
return True
@@ -254,6 +268,7 @@ def _wait_for_dnskey(name, dnskey, *, attempts: int = 20, delay_seconds: float =
254268

255269

256270
def _dnskey_set(name):
271+
"""Return the set of DNSKEY RR text values in the zone."""
257272
query = dns.message.make_query(name, dns.rdatatype.DNSKEY, want_dnssec=True)
258273
response = dns.query.tcp(
259274
query,
@@ -273,6 +288,7 @@ def _dnskey_set(name):
273288
def _wait_for_dnskey_set(
274289
name, expected, *, attempts: int = 60, delay_seconds: float = 0.5
275290
):
291+
"""Poll until the DNSKEY RRset equals the expected set."""
276292
for _ in range(attempts):
277293
if _dnskey_set(name) == expected:
278294
return True
@@ -282,6 +298,7 @@ def _wait_for_dnskey_set(
282298

283299

284300
def import_csk_key(name, *, dnskey, private_key=None):
301+
"""Import a CSK into Knot and optionally verify visibility."""
285302
if not wait_for_zone(name, attempts=60, interval_seconds=0.5):
286303
raise KnotException(f"Knot zone {name} not ready for updates")
287304
if private_key:
@@ -324,6 +341,7 @@ def import_csk_key(name, *, dnskey, private_key=None):
324341

325342

326343
def wait_for_zone(name, *, attempts=20, interval_seconds=0.5) -> bool:
344+
"""Poll for zone availability by querying SOA from Knot."""
327345
query = dns.message.make_query(name, dns.rdatatype.SOA)
328346
query_timeout = min(settings.NSLORD_KNOT_TIMEOUT, 1.0)
329347

@@ -349,12 +367,14 @@ def wait_for_zone(name, *, attempts=20, interval_seconds=0.5) -> bool:
349367

350368

351369
def _sleep(seconds: float) -> None:
370+
"""Sleep without blocking signals in some environments."""
352371
if seconds <= 0:
353372
return
354373
select.select([], [], [], seconds)
355374

356375

357376
def delete_zone(name):
377+
"""Delete a zone from the catalog."""
358378
catalog_update = _new_update(settings.CATALOG_ZONE)
359379
catalog_update.delete(_catalog_record_name(name), "PTR")
360380
_send_update(catalog_update)
@@ -413,23 +433,7 @@ def update_rrsets(
413433
has_changes = True
414434

415435
if has_changes:
416-
update_done = {"error": None}
417-
418-
def _apply_update():
419-
try:
420-
_send_update(update)
421-
except Exception as exc:
422-
update_done["error"] = exc
423-
424-
thread = threading.Thread(target=_apply_update, daemon=True)
425-
thread.start()
426-
thread.join(timeout=settings.NSLORD_KNOT_TIMEOUT * 2)
427-
if thread.is_alive():
428-
if settings.DEBUG:
429-
return
430-
raise KnotException("Knot update timed out")
431-
if update_done["error"] is not None:
432-
raise update_done["error"]
436+
_send_update(update)
433437

434438

435439
def import_zonefile_rrsets(name, rrsets):
@@ -463,6 +467,7 @@ def import_zonefile_rrsets(name, rrsets):
463467
def ensure_soa_serial_min(
464468
name, serial: int, *, attempts: int = 5, delay_seconds: float = 0.2
465469
):
470+
"""Ensure SOA serial is at least the given value by issuing updates."""
466471
query = dns.message.make_query(name, dns.rdatatype.SOA)
467472
for attempt in range(1, attempts + 1):
468473
response = dns.query.tcp(
@@ -513,6 +518,7 @@ def ensure_soa_serial_min(
513518

514519

515520
def get_zonefile(domain) -> bytes:
521+
"""Fetch an AXFR and render a filtered zonefile payload."""
516522
keyring, keyname, keyalgorithm = _transfer_keyring()
517523
zone_name = domain.name.rstrip(".") + "."
518524
xfr = dns.query.xfr(
@@ -549,6 +555,7 @@ def get_zonefile(domain) -> bytes:
549555

550556

551557
def get_keys(domain):
558+
"""Return DNSKEYs for a domain, including DS records where applicable."""
552559
query = dns.message.make_query(domain.name, dns.rdatatype.DNSKEY, want_dnssec=True)
553560
response = dns.query.tcp(
554561
query,

0 commit comments

Comments
 (0)