Skip to content

Releases: spatiumnorth/spatiumddi

Nightly 2026.09.16

Nightly 2026.09.16 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Sep 13:15
58e1246

Automated nightly build of main. Not a release — no support, no upgrade path.

built-from: 58e1246
date-tag: nightly-20260916

Pull the images:

docker pull ghcr.io/spatiumnorth/spatiumddi-api:nightly

The dated tag nightly-20260916 pins this exact build;
the newest 7 are kept.

The appliance ISO and slot upgrade image (+ .sha256) for this exact
build are attached below. Nightly pre-releases older than 7 days are
deleted automatically.

Nightly 2026.09.15

Nightly 2026.09.15 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 16 Sep 00:46
0f014df

Automated nightly build of main. Not a release — no support, no upgrade path.

built-from: 0f014df
date-tag: nightly-20260915

Pull the images:

docker pull ghcr.io/spatiumnorth/spatiumddi-api:nightly

The dated tag nightly-20260915 pins this exact build;
the newest 7 are kept.

The appliance ISO and slot upgrade image (+ .sha256) for this exact
build are attached below. Nightly pre-releases older than 7 days are
deleted automatically.

Nightly 2026.09.14

Nightly 2026.09.14 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 15 Sep 13:20
dedfe94

Automated nightly build of main. Not a release — no support, no upgrade path.

built-from: dedfe94
date-tag: nightly-20260914

Pull the images:

docker pull ghcr.io/spatiumnorth/spatiumddi-api:nightly

The dated tag nightly-20260914 pins this exact build;
the newest 7 are kept.

The appliance ISO and slot upgrade image (+ .sha256) for this exact
build are attached below. Nightly pre-releases older than 7 days are
deleted automatically.

Nightly 2026.09.13

Nightly 2026.09.13 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 14 Sep 21:19
ac48daf

Automated nightly build of main. Not a release — no support, no upgrade path.

built-from: ac48daf
date-tag: nightly-20260913

Pull the images:

docker pull ghcr.io/spatiumddi/spatiumddi-api:nightly

The dated tag nightly-20260913 pins this exact build;
the newest 7 are kept.

The appliance ISO and slot upgrade image (+ .sha256) for this exact
build are attached below. Nightly pre-releases older than 7 days are
deleted automatically.

Nightly 2026.09.12

Nightly 2026.09.12 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 13 Sep 00:41
30b076d

Automated nightly build of main. Not a release — no support, no upgrade path.

built-from: 30b076d
date-tag: nightly-20260912

Pull the images:

docker pull ghcr.io/spatiumddi/spatiumddi-api:nightly

The dated tag nightly-20260912 pins this exact build;
the newest 7 are kept.

The appliance ISO and slot upgrade image (+ .sha256) for this exact
build are attached below. Nightly pre-releases older than 7 days are
deleted automatically.

Nightly 2026.09.11

Nightly 2026.09.11 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 12 Sep 12:15
b21c439

Automated nightly build of main. Not a release — no support, no upgrade path.

built-from: b21c439
date-tag: nightly-20260911

Pull the images:

docker pull ghcr.io/spatiumddi/spatiumddi-api:nightly

The dated tag nightly-20260911 pins this exact build;
the newest 7 are kept.

The appliance ISO and slot upgrade image (+ .sha256) for this exact
build are attached below. Nightly pre-releases older than 7 days are
deleted automatically.

Nightly 2026.09.10

Nightly 2026.09.10 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Sep 00:42
ca1b6bf

Automated nightly build of main. Not a release — no support, no upgrade path.

built-from: ca1b6bf
date-tag: nightly-20260910

Pull the images:

docker pull ghcr.io/spatiumddi/spatiumddi-api:nightly

The dated tag nightly-20260910 pins this exact build;
the newest 7 are kept.

The appliance ISO and slot upgrade image (+ .sha256) for this exact
build are attached below. Nightly pre-releases older than 7 days are
deleted automatically.

SpatiumDDI 2026.09.04-1

Choose a tag to compare

@github-actions github-actions released this 04 Sep 22:24
feb331a

What's Changed

🚀 Highlights

The appliance sizing release. Two days of measured load against a real appliance — 250k A+PTR records, 35k devices, 75-minute cells judged on restarts, cgroup kills and the ratios Kea and BIND report themselves — turned up a family of defects that do not exist at lab scale and are invisible below it. A freshly bulk-loaded DNS group could not converge at all: the agent config bundle materialised all 500,000 pending record ops plus every zone's records as tracked ORM instances, and uvicorn was memcg-killed at 4.18 GB anon-RSS eighteen times in eighty-nine minutes without the bundle ever reaching the data plane (#948#949). The Kea and BIND DaemonSets carried no resources block, so Kubernetes ran the data plane as BestEffort — the lowest CFS weight on the node — and under a device rush the api starved Kea down to 7.5 % of a CPU, its socket queue overflowed, and only 37 % of the DISCOVERs on the wire were answered while Kea answered 100 % of what reached it (#952#953). The chart's api and worker memory limits are BYO-cluster defaults that no appliance can build a 250k bundle under, and every kubectl set resources was wiped by the next k3s restart, so the supervisor now sizes them from the node's RAM durably (#946#947).

The same campaign found two zone-lifecycle defects and a control-plane formation defect. POST …/records/bulk-delete on an agent-based zone deleted nothing: it gated the database delete on the wire op coming back applied, and agent-based servers never answer inline — they queue pending for the next long-poll — so every record came back wire delete failed: unknown and deleted: 0 while the agent removed it from the daemon anyway (#950#951, and the identical gate in DNS sync as #962). A 3-node control plane always promotes two members in one call, and one of them regularly died reaching the seed; the backend cleared the promote desired-state on the first reported failure, so one formation in three lost a member until an operator re-promoted by hand (#960#961).

The second thread is contracts an external client can actually consume, continuing #907's work now that the mobile app lives in its own repo. Five issues the mobile client filed reduced to one finding — data the server already has that a REST client cannot get, or cannot get typed — because non-negotiable #13 guarantees MCP tools for every REST surface and nothing guaranteed the converse (#917). Four capabilities got routes, ~113 handlers publishing an unconstrained object got a guard, and a whole-API conformance fuzz over ~1,150 routes found ten references to columns no model has — each an AttributeError the first time its line runs, so list_dhcp_scopes, list_dhcp_servers, list_dhcp_server_groups and list_network_devices had never returned a row since they shipped, and proposing a DHCP reservation from the copilot had never once worked (#923).

Alongside those: InfluxDB v1/v2/v3 push export (#889), the writer the tech-stack table had claimed for months while grep -ri influx over backend/ returned nothing; service control on Compose and Helm (#890), which had worked on exactly one deployment shape; fingerprint-driven DHCP device policies (#700), joining device profiling to Kea client classes; and moving a DNS server or zone between server groups (#934, #935), which discussion #933 pointed out was possible on the DHCP side and nowhere on the DNS side.

Two things had been quietly reporting success while doing nothing. The rpki_roa_expiring rule fired one alert event per ROA against Cloudflare's VRP cache entry rather than the ROA itself — measured against the live global dump, 100 % of 997,298 ROAs expire within seven days — so the 30-day threshold matched every ROA that will ever exist, permanently, and stormed the alerts framework with 928 open events on a small dev estate (#942). And no scheduled nightly had built since 2026-08-09: the DST twin-cron gate tested the Eastern wall clock when the job ran, GitHub starts those crons 1–13 hours late, so both twins skipped every night and 56 scheduled runs completed "success" in 10–18 seconds having done nothing (#970).

Operators must pull new agent images. The BIND9 renderer gained the rpz and rpz-passthru log categories (#914 — without them #699's per-client blocklist attribution recorded nothing on the only path that ships it), the responselog statement and its rndc follow-up, and a key-file include derived from state_dir rather than hardcoded (#920). The Kea renderer gained device-policy client classes (#700). A control-plane upgrade alone changes none of the rendered files.

✨ Added

  • InfluxDB v1 / v2 / v3 push export (#889). InfluxDBTarget plus backend/app/services/influxdb/, a 30 s beat task with per-target interval gating, CRUD at /settings/influxdb-targets with a real test-write, and a Settings → Metrics → InfluxDB Export surface. - "All versions" is three declared versions over two wire dialects. v3 is not a third client: every InfluxDB 3 product accepts the v2 write endpoint, so it reuses that path with Authorization: Bearer instead of Token and a database named in the bucket parameter. All three were verified against a live server, v1 through InfluxDB 2.7's DBRP compatibility mapping. - The idempotency (non-negotiable #9) is the server's, not ours. Line protocol overwrites a point with an identical measurement, tag set and timestamp, so a retry is free — which is what lets each push run two queries per source on separate row budgets: a forward drain and a replay of the closed (watermark − 5 min, watermark] window, so a bucket an agent reported late is exported rather than skipped permanently and silently. The separation is load-bearing: fold the replay into the drain's lower bound and a fleet dense enough to fill the row cap inside that window returns a truncated batch whose maximum sits below the watermark, pulling the cursor backwards every tick with every push still reporting success. - Two shapes of metric, and the difference shows on a dashboard. DNS/DHCP counter deltas carry the agent's own 60 s bucket timestamp, so a backfill lands on the hour the traffic happened; the IPAM and lease gauges are sampled at push time and have no backfill, documented as such rather than called "realtime". active_leases counts distinct addresses — a Kea HA pair mirrors every lease twice, so COUNT(*) would report 2× on exactly the deployments redundancy is for. - Explicitly not a feature module (non-negotiable #14): no sidebar section, no router prefix, and "off" is already enabled=false on the row.
  • Capability-aware service control on Compose and Helm (#890). One surface at Admin → Platform Insights → Services over backend/app/services/service_control/, with opt-in RBAC in charts/spatiumddi and k8s/service-control/. - The capability is answered before anything is attempted. The same 503 used to mean both "this deployment cannot do that" and "the daemon is down", and those need opposite responses from the operator, so GET /system/services reports the live backend, whether the gate is open, and the exact toggle to flip. - The inventory is the allowlist, resolved server-side against a fresh listing. On Compose the scope is the api container's own com.docker.compose.project label and fails closed: a container that cannot identify its own project reports the backend unavailable rather than falling back to a name prefix a co-tenant could match on purpose. - start / stop are deliberately absent on Kubernetes — they would mean scaling to zero and back, and restoring the previous replica count needs somewhere durable to remember it. - Restarting the api that serves the page is allowed and flagged; the audit row commits and the 202 returns before the daemon is signalled, and records accepted, not success.
  • Fingerprint-driven DHCP device policies (#700). DHCPDevicePolicy compiles an operator's chosen fingerbank device classes into a real Kea client-class test, carrying an option set, a per-class valid-lifetime and a stable generated class name a pool's class_restriction can bind to — NAC-lite with no 802.1X and no switch configuration. - The compiler cannot match the category, and says so. Kea has no device-class == IoT predicate, so it matches the signatures observed and classified into the selected classes — honestly "classify on first lease, apply on renewal", stated in the UI. - Ambiguous signatures are excluded by default, counted and listed: a parameter request list like 1,3,6,15 comes from a doorbell and a rack server alike, so without this the headline use is also how the CEO's laptop gets quarantined. - Nothing device-controlled becomes syntax — both halves of every term are emitted as hex, so a vendor class of ' or 1-- is inert bytes. A policy compiling to nothing is dropped rather than rendered testless, because a Kea class with no test matches every packet. - Validated against a live fingerbank key, which corrected the issue text: there is no plain Printer or IoT class. The picker now shows fingerbank_score and flags anything under 30, because a low score means fingerbank failed to identify the device and fell back to the MAC vendor.
  • Move a DNS server between server groups (#934, #937). group_id on the server PUT, routed through services/dns/server_move.py, plus a Server group picker. The DHCP side has carried this since #430 — an asymmetry, not a design decision. The move purges the old group's zone state and queued ops, clears config_apply_status, generates the target's TSIG key if absent, wakes both group channels and the server's own (an agent parked in a long-poll subscribed using its old group), and on the appliance repoints Appliance.assigned_dns_group_id so the per-role firewall does not ...
Read more

SpatiumDDI 2026.08.22-1

Choose a tag to compare

@github-actions github-actions released this 23 Aug 00:06
b333df5

What's Changed

🚀 Highlights

The "stored, shipped, never rendered" release. One bug class runs through most of this one, and it is the worst kind a control plane can have: a setting the operator sets, the API returns, the UI displays and the ETag hashes — that never reaches the file the daemon actually reads. Every symptom is silence. A scope's DNS Servers option never reached Kea (#856), so clients took a lease with no resolver while option 3 worked, which is what made it look arbitrary. Pool options, PXE profiles and phone classes never reached agent-managed Kea either (#858) — and because the ETag did move when they changed, agents dutifully resynced and re-rendered byte-identical config. acl {} definitions were never rendered at all (#899): the ACLs tab stored, listed and edited rows that applied to nothing, and citing one took the group's whole config down rather than losing one statement. DNSServerOptions.forward_policy was settable, persisted and shipped, and no forward statement was ever written — so only, which is how an operator forces every query through a filtering upstream, silently behaved as BIND's default first and let queries leak straight past it. allow_transfer was the same on both the server and the zone (#734). And the DNS + DHCP heartbeats declared daemon and config fields that neither handler read, so the degraded verdict the agents already computed was discarded at the door (#882).

The lesson is recorded in DNS.md §8.2 in one line — assert on the rendered config, not the stored row — and the DHCP wire-coverage test now enforces it mechanically: every ConfigBundle field must be consciously classified as shipped-to-agents or withheld-with-a-reason, so adding a field fails the suite until someone makes that call.

The second thread is documents that lie about the server. The published OpenAPI document expressed nullability the one way strict code generators cannot model, so a generated client silently lost 3,291 schema properties and 297 query parameters — limit among them, so it could not paginate at all — while compiling cleanly and passing review (#907). Timestamps went out with six fractional digits, which most generated decoders reject, making 6 of 7 endpoints undecodable, every one of them a 200 OK. The agent /config long-poll's ETag was not a valid entity-tag at all, and being strong meant nginx's gzip filter deleted it outright (#862). And conformance fuzzing against a live appliance turned up thirteen 5xx-class defects (#861), including a password over 72 bytes 500-ing every password-gated endpoint and a kubelet restarting a perfectly healthy api pod every few minutes because it was the only workload in the chart with no probe timeoutSeconds.

Alongside those: global search rebuilt (#879) — which had been applying no permission filtering at all, the widest read surface in the product and the only one that checked nothing; a platform-wide scrubbed support bundle (#875) that works on all three deployment shapes; a family filter with real SafeSearch enforcement (#878); and agents that now revert to their last known-good config and say so upward instead of serving one thing while the database says another (#882).

Operators must pull new agent images. The DHCP option fixes (#856, #858), the TSIG allow-transfer grant (#734), the ACL and forward rendering (#899), the RPZ exception + dedup fixes (#878) and the last-known-good revert (#882) all live in the dns-bind9, dns-powerdns and dhcp-kea images. A control-plane upgrade alone changes none of the rendered files.

✨ Added

  • Global search v2 — ranking, RBAC, 20 types, command palette (#879). Matching, ranking and gating move out of the router into backend/app/services/search/, and the global_search Copilot tool now calls that engine instead of carrying its own copy of the fan-out over the router's private helpers. Coverage goes 7 types → 20 through a SearchProvider registry that the engine, the scope chips, the MCP tool and GET /search/types all read from, so there is no second list to keep in step. - Ranking is computed in SQL, before each type's LIMIT. The ordering complaint in the issue was the visible half of a worse bug: with no ORDER BY the database returned any N matching rows, and the exact hit was routinely not among them — which sorting in Python afterwards cannot recover. - Trigram GIN indexes back the leading-wildcard ILIKE on the tables that actually grow; small tables are left unindexed on purpose. Extension creation runs in a SAVEPOINT and degrades to sequential scans rather than failing the upgrade. - Frontend: scope chips, sessionStorage recents, and go-to-page commands sourced from the sidebar's own nav tree (extracted to lib/navigation.ts, so the palette cannot drift from the sidebar — the same argument that shaped the #737 shortcut map). - Deferred and stated: an expression index for custom-field values (the field name is chosen at runtime, so no trigram index can serve it), and action commands that do something rather than navigate.
  • Platform-wide scrubbed support bundle (#875). One-click diagnostics archive at POST /system/support-bundle with /preview and /decode-map beside it — superadmin, audited, and working on Compose, Kubernetes and the appliance alike, unlike the appliance-only diagnostics bundle it supersedes (whose pod-log and self-test halves go through kubeapi and 503 everywhere else). - The premise is that attachments are public. GitHub has no private channel for this: attachment URLs on a public repo follow repository visibility, and deleting the comment does not reliably purge the file. There are no confidential issues, secret gists are unlisted rather than private, and private vulnerability reporting is for advisories. So the design is scrubbing, not secrecy. - Two tiers. Secrets — Fernet blobs, bcrypt / argon2 hashes, PEM private keys, JWTs, PSKs, credentials in URL userinfo — are matched by field name and value shape and hard-excluded in every mode, including the unscrubbed one: there is no version of "let me read my own logs" improved by shipping the key that decrypts the database. Identifiers are pseudonymised HMAC-deterministically off SECRET_KEY, so mappings are stable per install (support can correlate two bundles) and unguessable outside it. - Topology survives, which is what keeps the archive worth reading for a DDI product: one real /24 lands in one synthetic /24 with the host octet intact, zone and subdomain grouping are preserved, and loopback / link-local / multicast / in-addr.arpa pass through untouched because they identify nobody and are load-bearing in a log. - Two choices are product-specific rather than copied from sos: synthetic IPv4 goes to 240.0.0.0/6, not sos's CGNAT range, because #42 makes CGNAT a real modelled concept here and obfuscating into it would make a bundle read as documenting genuine CGNAT deployments; and the IPv6 interface ID is discarded rather than mapped, because a SLAAC address embeds the MAC (RFC 4291 modified EUI-64) and preserving it would route hardware identity straight past the MAC scrubber. - Preview → review → download, because the review step is the point: the operator sees the file list, what was replaced and a sample before deciding to share. The decode map is a separate endpoint and never in the archive — a bundle carrying its own decoder is not scrubbed, merely inconvenient to read. Unscrubbed needs a verbatim confirmation string and is named -UNSCRUBBED- so it is recognisable in a downloads folder a week later. - A last-chance safety net sweeps the assembled text and reports what it caught, in the manifest and in red in the UI: a net firing means a collector has a bug, and hiding that would let the bug live behind a net that may not catch the next variant. - 1 MCP tool (get_support_bundle_preview, default off — a broad read). No feature-module gate: an ops primitive like backup. The bug-report template now says plainly that attachments are public.
  • Family filter — adult blocklists + SafeSearch enforcement (#878). The blocklist catalog gains two kinds of thing beside the feeds it already had: templates, entry sets shipped inline for rules with no upstream feed to subscribe to, and profiles, named compositions applied in one action. - SafeSearch ships as a template because it is not a blocklist at all — it is a set of RPZ rewrites pointing each engine at the provider's own filtered endpoint, riding the entry_type="redirect" path that already existed. The Family filter profile pairs the adult and gambling feeds with the DoH / VPN / proxy bypass lists, because a filter one browser setting routes around is not one. - Data taken from each provider's own documentation: all 194 Google country domains, not just www.google.com, which is bypassed by typing google.de; exactly the five YouTube hostnames Google documents, since they warn that rewriting youtube.com / youtu.be / s.ytimg.com breaks playback; edgeservices.bing.com alongside www.bing.com, because it is the Edge sidebar / Copilot entry point. Never wildcards — *.youtube.com would also match the rewrite target restrict.youtube.com, and BIND turns that CNAME loop into SERVFAIL. - Applying a profile assigns the lists to nothing. Auto-scoping would filter the server VLAN along with the kids' one, so assignment stays a deliberate second step (see #876, which is what makes that step reachable from the UI at all). - DNS.md gains a sizing section: two RPZ records per feed entry, so the Family filter's ~596k entries render ~1.2M records, measured at 1,192,147. BIND9 only — PowerDNS, Windows and the cloud drivers render no RPZ, and §8.1 is explicit that DNS filtering is bypassable at all. - 1 MCP tool (list_blocklist_templates).
  • **Per-view blocklist scoping ...
Read more

SpatiumDDI 2026.08.12-1

Choose a tag to compare

@github-actions github-actions released this 13 Aug 01:31
4435b1f

What's Changed

🚀 Highlights

The migration + field-hardening release. Two additions change what the platform can take on. The guided Windows → SpatiumDDI cutover (#756) is the half the importers stop short of: they land a copy of a Windows estate and prove nothing about what happens next, because the Windows server is still running, still authoritative, and still the thing clients actually talk to. The cutover verifies the two sides agree, runs them in parallel against real traffic, performs the switch per zone and per scope with a rollback, and tracks the decommission — while creating no zones, scopes, pools or records of its own. Alongside it, an agentless Technitium driver (#810) closes the last one-shape-only backend: paste an API URL and a token, and the control plane drives an install the operator already runs with nothing deployed. Also landing: searchable, permission-filtered resource pickers on the self-service request portal (#759), which was asking low-privilege users to type raw UUIDs, and an appliance Web UI firewall reachability self-check (#779) that answers the one question a self-curl cannot.

The bulk of the release, though, is field-found correctness, and one bug stands out. Under split-horizon views a DNS record created through the API was rendered correctly into its zone file and then never served — 201 from the API, NXDOMAIN on the wire, permanently, for every record created after the zone's first load (#704, #707). rndc reconfig is defined not to re-read existing zone files, which was the correct signal while record edits rode RFC 2136 and became the wrong one the moment views made re-rendering the only path. The same investigation found two named daemons sharing :53 under SO_REUSEPORT on a live appliance, which turned rndc into a coin flip and had one zone answering from two different serials depending on which process the kernel picked. Both fixes are in the DNS agent image, so operators must pull new agent images to get them.

Three more arcs run through this one. The backup and restore hardening (#781) found that selective restore was truncating with CASCADE and silently emptying up to 130 tables it never refilled, and that the cross-install secret rewrap covered 21 of the schema's 47 encrypted columns and none of the five JSONB-embedded ones — so the appliance CA key, TLS private keys, k3s join tokens, ACME account keys, SNMPv3 passphrases and every integration credential survived a restore still encrypted under a key the operator no longer has. The appliance slot-upgrade surface stopped lying about which machine it was acting on: on a multi-node control plane the status endpoint was answered by an arbitrary node and the write endpoints armed whichever one the load balancer picked, so a rolling upgrade could upgrade one node three times and never touch the other two. And OPNsense was mirroring nothing at all on 25.7+, reporting a clean sync with zero results forever, because it parsed an interface shape that has never existed and asked only an ISC dhcpd that moved out of core.

One upgrade-path note, unchanged from where it was documented (#787): if you are coming from 2026.07.11-1 or 2026.07.21-1, uploading an appliance slot image through the Web UI will 413 before FastAPI ever sees it — the nginx client_max_body_size fix first shipped in 2026.07.30-1. Use the URL source or the host CLI instead; neither crosses that nginx location.

✨ Added

  • Guided Windows → SpatiumDDI cutover (#756). The unit of work is a plan: a source Windows DNS and/or DHCP server, a target server group, and a list of items, where one item is one zone or one scope. Items are independent — they can be cut over on different days and each rolls back on its own. There is no big-bang step anywhere. Behind the default-on migration.cutover feature module (group Tools, because one plan covers both protocols and either "DNS" or "DHCP" would be a lie about half the surface), superadmin on every endpoint — a cutover is strictly more dangerous than an import, so inventing a grantable permission for it would be a weaker posture than the surface it extends. - Phase 1 — parity. Diffs each object against the live Windows server and classifies every difference by why the two sides differ: value_mismatch / drifted_since_import / never_imported / intentionally_diverged. A changed record pairs into one decision rather than the missing+extra pair the drift report produces. Two traps are handled explicitly: a Path-B PowerShell pull cannot emit CAA / TLSA / SSHFP, so those report not_compared instead of missing; and an unparseable PowerShell response is indistinguishable from an empty zone, so that case reports unverified rather than "everything diverged". - Phase 2 — parallel run. Replays recently-observed queries from the query log against both sides and compares answers, so parity is demonstrated against production traffic rather than asserted from config equality. Falls back to the zone's own records when the query log is empty and says which via sample_source, because the two are not equally strong evidence. Queries address an IP literal, never a hostname — the exact dns.query failure that made the drift report fail 100% against hostname-addressed servers — and go out with RD=0, so a cached answer from somewhere else cannot stand in for the server under test. - Phase 3 — the switch. A TTL pre-flight that snapshots the originals once and restores them exactly; a DHCP lease handover that promotes live Windows leases to reservations, so a renewing client keeps the address it already holds instead of meeting a Kea with an empty lease database; then the switch itself, which deactivates the Windows scope before activating the managed one and puts the old one back if the new side fails to come up. - Phase 4 — a 15-item decommission checklist, three of them advisory-evaluated from SpatiumDDI's own data (DC SRV registration, reverse-zone ownership, zone-transfer ACLs — the three an operator is most likely to get wrong). None is ever auto-ticked. - One refusal force cannot bypass. An AD-integrated zone set to "Secure only" dynamic updates is a hard block, because GSS-TSIG is unimplemented (#444) and a domain controller that cannot register its SRV records turns a DNS migration into a domain outage. It fails closed: a dynamic-update mode we cannot interpret on an AD-integrated zone is treated as Secure. 16 readiness blocker codes in all, each block or warn with an operator-facing fix string. Never having run parity and having run one that proved nothing are treated identically. - A markdown runbook (GET …/runbook) to paste into a change ticket — deliberately not a summary of what SpatiumDDI already did. It carries the Windows-side PowerShell SpatiumDDI deliberately does not run for you (lowering the authoritative TTLs resolvers actually cache — lowering only our side is worse than lowering neither, because it promises a five-minute rollback while the world holds hour-long answers), the exact order of operations, and a rollback with a real number attached, derived from that item's own pre-flight TTL or lease time. - Ordering is sealed inside a transaction where it has to be. Both DHCP paths paired an irreversible WinRM call with a database write and let the router commit after both, leaving two windows: a cutover where Windows is deactivated and then the commit fails (the subnet has no DHCP server), and a rollback where both sides end up active for one subnet — the exact overlap the deactivate-before-activate order exists to prevent. The commit point now lives with the ordering rules. - Four MCP tools — find_cutover_plans, find_cutover_plan_status and count_cutover_blockers default-on, find_cutover_parity_check default-off because it runs a live WinRM pull. Registered in both the backup catalog (its own migration section — the pre-flight TTL snapshot is the only copy of the pre-migration TTLs, so a restore without it loses the ability to roll a cutover back) and factory reset.
  • Agentless Technitium DNS driver — technitium_api (#810). Technitium was the only backend SpatiumDDI supported in exactly one shape: BIND9 has the agent path and RFC 2136, Windows DNS has Path A and Path B, the cloud providers are agentless-only. An operator with an existing Technitium install had to migrate off it, stand up a second one under our agent, or not use SpatiumDDI — for the backend whose entire control surface is a plain HTTP API and is therefore the easiest one to drive remotely. Zone and record CRUD plus topology pull; DNSSEC, forwarders and blocklists stay agent-managed. It coexists with the agent-managed technitium driver, and since a group is single-driver, a mixed estate is one group each. No migration — credentials ride the existing DNSServer.credentials_encrypted. - Agentless, but explicitly not a cloud driver. It subclasses CloudDNSDriverBase — which despite the name is really "agentless driver that keeps a credential dict" — while staying out of CLOUD_DNS_DRIVERS, the set that gates the cloud-import flow and the hosted-provider UI. This server is operator-hosted on an operator-supplied URL, which is also why create and update run it through the SSRF guard. The registry grows two sets so the distinction is expressible, CREDENTIALED_DNS_DRIVERS and TOPOLOGY_PULL_DRIVERS, the latter replacing two inline windows_dns or CLOUD_DNS_DRIVERS comparisons that had to be kept in step by hand. - Errors arrive as HTTP 200. Technitium reports application failures in the response body, so a driver trusting raise_for_status() reads every auth failure as an empty success — and an empty zone list handed to a sync diff is exactly the #430 shape that proposes deleting everything SpatiumDDI knows about. One unwrap path reads every response, and an ...
Read more