fix: the name server is the primary of any number of domains, each with secondary name servers and a switch for the records of the sites.
Release / release (push) Successful in 13s

This commit is contained in:
slawek committed 2026-10-26 21:26:47 +01:00
1 parent 57e8bbcffe
commit 1697e1aa38
9 files changed
+412 -150

No files matched your search

+44 -19
View File
@@ -52,8 +52,8 @@ Pages:
/sites the web sites Caddy serves: the settings of sites, list, add, change and remove (see sites()),
and the files of each: list, download, upload, extract a zip archive, create a folder, rename and
remove (see Handler.site_files()); a member of a site's editors group may use the site's files
/dns the name server of dns.zone: its settings, the records it serves, and the manual records: add and
remove (see dns())
/dns the primary name server of the router's domains: its settings, and the domains: add, change and
remove, each with its secondaries, the records it serves, and its manual records (see dns())
/postgresql
the databases of each PostgreSQL cluster on the router with their sizes, as JSON (see
postgresql_databases()); /config has the clusters' settings
@@ -1264,44 +1264,69 @@ def site_file_action(name, target, body):
def dns(method, path, query, body):
"""Answer a /dns request with vpnrouter_dns: the HTTP status and the JSON document.
GET /dns {"settings", "installed", "running", "records", "manual", "problem"}: the dns section of the
configuration, whether PowerDNS is installed and runs, the records the name server serves,
the manual records, and why the records cannot be read, null when they can
GET /dns {"settings", "installed", "running", "domains", "problem"}: the dns section of the
configuration, whether PowerDNS is installed and runs, the domains, each {"name", "sites",
"ttl", "secondaries", "manual", "records"} with its secondaries {"nameserver", "address"},
its manual records and the records the name server serves, and why the domains cannot be
read, null when they can
PUT /dns change any of the settings of the dns section
POST /dns/records add a manual record: {"name", "type", "content", "ttl"?}; name is relative to dns.zone, @
for the zone itself, or absolute with a final dot; ttl is dns.ttl without it
DELETE /dns/records remove the manual records of name= and type=, with content= only that one
POST /dns/domains add a domain: {"name", "sites"?, "ttl"?}; sites gives the host names of the enabled sites
in it records; ttl is dns.ttl when null or absent; an existing name is 409
PUT /dns/domains/<domain>
change "sites" or "ttl" of the domain
DELETE /dns/domains/<domain>
remove the domain with its secondaries and manual records
POST /dns/domains/<domain>/secondaries
add a secondary name server of the domain, or change its address: {"nameserver", "address"}
DELETE /dns/domains/<domain>/secondaries/<nameserver>
remove the secondary name server
POST /dns/domains/<domain>/records
add a manual record: {"name", "type", "content", "ttl"?}; name is relative to the domain,
@ for the domain itself, or absolute with a final dot; ttl is the domain's without it
DELETE /dns/domains/<domain>/records
remove the manual records of name= and type=, with content= only that one
A change is applied by restarting vpn-router-setup without waiting for it.
"""
if body == 'invalid' or (body is not None and not isinstance(body, dict)):
return 400, {'error': 'the body must be a JSON object'}
body = body or {}
parts = path.strip('/').split('/')
route = (method, *parts[:2], *parts[3:4])
try:
cfg = vpnrouter.load_config()
if (method, path) == ('GET', '/dns'):
served, manual, problem = [], [], None
if route == ('GET', 'dns'):
domains, problem = [], None
try:
manual = vpnrouter_dns.manual()
served = vpnrouter_dns.served(cfg['dns']['zone'].strip())
domains = vpnrouter_dns.listing()
except vpnrouter_dns.DatabaseError as exc:
problem = str(exc)
return 200, {'settings': cfg['dns'], 'installed': vpnrouter_dns.PDNS_SERVER.exists(),
'running': unit_state(vpnrouter_dns.UNIT) == 'active', 'records': served, 'manual': manual,
'running': unit_state(vpnrouter_dns.UNIT) == 'active', 'domains': domains,
'problem': problem}
if (method, path) == ('PUT', '/dns'):
if route == ('PUT', 'dns'):
changed = section_change('dns', body)
return changed if isinstance(changed, tuple) else (200, {'settings': changed})
if (method, path) == ('POST', '/dns/records'):
document, code = vpnrouter_dns.add(cfg, body), 201
elif (method, path) == ('DELETE', '/dns/records'):
if route == ('POST', 'dns', 'domains') and len(parts) == 2:
document, code = vpnrouter_dns.domain_add(body), 201
elif route == ('PUT', 'dns', 'domains') and len(parts) == 3:
document, code = vpnrouter_dns.domain_change(parts[2], body), 200
elif route == ('DELETE', 'dns', 'domains') and len(parts) == 3:
document, code = vpnrouter_dns.domain_remove(parts[2]), 200
elif route == ('POST', 'dns', 'domains', 'secondaries') and len(parts) == 4:
document, code = vpnrouter_dns.secondary_add(parts[2], body), 201
elif route == ('DELETE', 'dns', 'domains', 'secondaries') and len(parts) == 5:
document, code = vpnrouter_dns.secondary_remove(parts[2], parts[4]), 200
elif route == ('POST', 'dns', 'domains', 'records') and len(parts) == 4:
document, code = vpnrouter_dns.add(cfg, parts[2], body), 201
elif route == ('DELETE', 'dns', 'domains', 'records') and len(parts) == 4:
content = query.get('content', [None])[0]
document = vpnrouter_dns.remove(cfg, query.get('name', [''])[0], query.get('type', [''])[0], content)
document = vpnrouter_dns.remove(parts[2], query.get('name', [''])[0], query.get('type', [''])[0], content)
code = 200
else:
return 404, {'error': f'no route {method} {path}'}
except vpnrouter_dns.Failure as failure:
return (404 if failure.status == 3 else 400), {'error': str(failure)}
return {3: 404, 4: 409}.get(failure.status, 400), {'error': str(failure)}
except vpnrouter_dns.DatabaseError as exc:
return 500, {'error': str(exc)}
except SystemExit as exc: