"""DNS update helper and thread for heartbeat daemon.""" from __future__ import annotations import threading import subprocess from subprocess import Popen, PIPE, STDOUT from typing import Optional def create_nsupdate_payload(hostname: str, newip: str, dyndomain: str, dnsttl: str = "5") -> str: D = {"domain": dyndomain, "fqdn": f"{hostname}.dy.{dyndomain}", "dnsttl": dnsttl, "newip": newip, "ts": __import__("time").strftime("%Y-%m-%d.%H:%M:%S", __import__("time").gmtime())} if ":" in newip: nsup = ( """update delete %(fqdn)s AAAA update add %(fqdn)s %(dnsttl)s AAAA %(newip)s update delete %(fqdn)s TXT update add %(fqdn)s %(dnsttl)s TXT "Created: %(ts)s" send answer """ % D ) else: nsup = ( """update delete %(fqdn)s A update add %(fqdn)s %(dnsttl)s A %(newip)s update delete %(fqdn)s TXT update add %(fqdn)s %(dnsttl)s TXT "Created: %(ts)s" send answer """ % D ) return nsup def nsupdate(hostname: str, newip: str, dyndomain: str, nsupdate_bin: str = "/usr/local/bin/nsupdate", rndc_key: str = "/etc/dhcpc/rndc-key") -> Optional[str]: """Perform DNS update via nsupdate command. Returns None on success, else returns combined stdout/stderr as a string. """ nsup = create_nsupdate_payload(hostname, newip, dyndomain) cmd = [nsupdate_bin, "-k", rndc_key, "-v"] try: p = Popen(cmd, shell=False, bufsize=0, stdin=PIPE, stdout=PIPE, stderr=STDOUT) except OSError as e: return f"nsupdate: execution failed: {e}" except Exception as e: return f"nsupdate: some error occured: {e}" (output, err) = p.communicate(nsup.encode()) out = output.decode() if output else "" if out.find("status: NOERROR") >= 0: return None return out def dnsupdatethread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None): """Thread target: process dns update queue from hbdclass.Host.dnsQ. hbdclass: module with Host class that exposes dnsQ queue cfg: configuration mapping with 'dyndomains' and 'nsupdate_bin' log: callable(host, message) email: callable(subject, message) """ while True: name, addr = hbdclass.Host.dnsQ.get() m = f"changed address to {addr}" for dyndomain in cfg.get("dyndomains", []): err = nsupdate(name, addr, dyndomain, nsupdate_bin=cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate")) if err: m += f", DNS update failed: {err}" if email: try: email("error: nsupdate failed", f"{name}.dy.{dyndomain}: {m}") except Exception: pass else: m += ", DNS updated." hbdclass.Host.dnsQ.task_done() if log: try: log(name, m) except Exception: pass def start_dns_thread(hbdclass, cfg: dict, log: Optional[callable] = None, email: Optional[callable] = None) -> threading.Thread: t = threading.Thread(target=dnsupdatethread, args=(hbdclass, cfg, log, email)) t.daemon = True t.start() return t