Major refactoring of the codebase, including restructuring of files and directories, renaming of modules and classes, and improvements to the overall organization and readability of the code. This refactoring aims to enhance maintainability, scalability, and clarity of the codebase while preserving existing functionality. The changes include:

- Restructuring of the project directory into client and server components
- Renaming of modules and classes to better reflect their purpose and functionality
- Moving common utilities and configurations to a shared location
- Updating import statements to reflect the new structure
- Adding new documentation files for better clarity on various aspects of the project
- Removing deprecated or unused code to streamline the codebase
- Ensuring that all existing functionality is preserved and that the codebase remains functional after the refactoring.
This commit is contained in:
Andreas Wrede
2026-03-29 11:13:40 -04:00
parent 7e2038ecac
commit 0543266c92
65 changed files with 11371 additions and 140 deletions
+224
View File
@@ -0,0 +1,224 @@
"""DNS update helper and pure asyncio worker for heartbeat daemon."""
from __future__ import annotations
from subprocess import Popen, PIPE, STDOUT
from typing import Optional
import asyncio
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
async def dns_update_worker(
hbdclass,
cfg: dict,
async_queue=None,
log: Optional[callable] = None,
pushmsg: Optional[callable] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
):
"""Pure async DNS worker that processes updates from asyncio.Queue.
Exits when it receives a None sentinel.
"""
if loop is None:
loop = asyncio.get_running_loop()
dnsq = async_queue
if not dnsq:
if log:
try:
await loop.run_in_executor(
None, log, None, "dns_update_worker: no queue available"
)
except Exception:
pass
return
while True:
try:
item = await dnsq.get()
except Exception as e:
if log:
try:
await loop.run_in_executor(
None, log, None, f"dns_update_worker: error getting item: {e}"
)
except Exception:
pass
break
if item is None:
break
try:
name, addr = item
except Exception:
try:
dnsq.task_done()
except Exception:
pass
continue
m = f"changed address to {addr}"
for dyndomain in cfg.get("dyndomains", []):
err = await loop.run_in_executor(
None,
nsupdate,
name,
addr,
dyndomain,
cfg.get("nsupdate_bin", "/usr/local/bin/nsupdate"),
cfg.get("rndc_key", "/etc/dhcpc/rndc-key"),
)
if err:
m += f", DNS update failed: {err}"
if pushmsg:
try:
await loop.run_in_executor(
None,
pushmsg,
"error: nsupdate failed",
f"{name}.dy.{dyndomain}: {m}",
)
except Exception:
pass
else:
m += ", DNS updated."
try:
dnsq.task_done()
except Exception:
pass
if log:
try:
await loop.run_in_executor(None, log, name, m)
except Exception:
pass
if log:
try:
await loop.run_in_executor(None, log, None, "dns_update_worker exiting")
except Exception:
pass
def start_dns_worker(
hbdclass,
cfg: dict,
log: Optional[callable] = None,
pushmsg: Optional[callable] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
):
"""Start the async DNS worker and return the Task.
Replaces Host.dnsQ with an asyncio.Queue wrapped in a thread-safe bridge
so legacy synchronous put() calls from UDP handlers still work.
"""
if loop is None:
loop = asyncio.get_event_loop()
# Create asyncio.Queue and wrap in a bridge for thread-safe puts
async_q = asyncio.Queue()
class _QueueBridge:
"""Thread-safe wrapper around asyncio.Queue for synchronous callers."""
def __init__(self, loop, aq):
self._loop = loop
self._aq = aq
def put(self, item):
"""Thread-safe put that schedules onto event loop."""
try:
# Try to detect if we're in the event loop thread
asyncio.get_running_loop()
# We're in event loop context, use put_nowait directly
self._aq.put_nowait(item)
except RuntimeError:
# We're in a different thread, schedule safely
try:
self._loop.call_soon_threadsafe(self._aq.put_nowait, item)
except Exception:
pass
def task_done(self):
"""Delegate task_done to asyncio.Queue."""
try:
self._aq.task_done()
except Exception:
pass
bridge = _QueueBridge(loop, async_q)
hbdclass.Host.dnsQ = bridge
task = loop.create_task(
dns_update_worker(
hbdclass, cfg, async_queue=async_q, log=log, pushmsg=pushmsg, loop=loop
)
)
return task