deleted: docs/architecture-overview.drawio
deleted: docs/architecture-overview.svg
modified: docs/install-systemd-service.md
new file: docs/installation-guide.md
Documentation cleanup
modified: pyproject.toml
change old provisioner link to bridge
- New NotifyJobRunner base class (extends netbox.jobs.JobRunner) holds
the plugin SETTINGS and the helpers shared by both NOTIFY jobs:
load_tsig_key(), send_notify() and get_soa_serial() (promoted from
the former module-level _-prefixed functions).
- SendClientNotify and SendNSNotify extend the base and access the
helpers via self. Behavior, job names and kwargs are unchanged.
- The nameserver-only helpers moved into SendNSNotify as
notify_ns_enabled() (classmethod, reads cls.SETTINGS) and
resolve_zone_nameserver_ips() (instance method).
- No changes to any callers: schedule_client_notify(),
schedule_ns_notify() and the catalog zone NOTIFY scheduling keep
their names and call sites.
- New setting 'catalog_zone_name' (default: 'catz') to change the name of
the per-view catalog zone. Full DNS names (multiple labels) are
supported; names are handled as dns.name.Name objects, making the
comparison case-insensitive and tolerant of trailing dots.
- New setting 'catalog_zone_name_includes_view' (default: False). When
enabled, the catalog zone name is prefixed with the view name:
'<view>.<catalog_zone_name>'.
- The transfer endpoint now serves exactly the one configured catalog
zone name per view instead of accepting both 'catz' and
'<view>.catz'.
- The catalog zone NOTIFY now targets only the configured name instead
of notifying for both variants. Scheduling moved to
catalog_zone_manager, which owns all catalog zone naming.
Not really helpful. Just more work. Bye bye!
modified: netbox_dns_bridge/__init__.py
Bump version to 1.6.0-rc1
new file: netbox_dns_bridge/api/__init__.py
new file: netbox_dns_bridge/api/serializers.py
new file: netbox_dns_bridge/api/urls.py
new file: netbox_dns_bridge/api/views.py
Adds API to the plugin. In case someone needs it :P
This fixes a serializer bug on Zone creation in the same stroke.
modified: netbox_dns_bridge/models.py
deleted: netbox_dns_bridge/migrations/0003_catalogzone_and_per_zone_identifier.py
new file: netbox_dns_bridge/migrations/0003_full_netbox_models.py
This assumes the migration starts at v1.5.8.
Adjust model access and function calls to match new models:
modified: netbox_dns_bridge/signals/netbox_dns_zones.py
modified: netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py
modified: netbox_dns_bridge/management/commands/dns_transfer_endpoint/request_handler.py
Update UI elements and permission interactions:
modified: netbox_dns_bridge/filtersets.py
modified: netbox_dns_bridge/forms.py
modified: netbox_dns_bridge/jobs/notify.py
modified: netbox_dns_bridge/navigation.py
modified: netbox_dns_bridge/tables.py
deleted: netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_members.html
new file: netbox_dns_bridge/templates/netbox_dns_bridge/catalogzone.html
new file: netbox_dns_bridge/templates/netbox_dns_bridge/catalogzonemember.html
deleted: netbox_dns_bridge/templates/netbox_dns_bridge/notify_overview.html
new file: netbox_dns_bridge/templates/netbox_dns_bridge/seentransferclient.html
modified: netbox_dns_bridge/urls.py
modified: netbox_dns_bridge/views.py
to DB driven to permit user modification:
modified: netbox_dns_bridge/models.py
Add 4 cols for values.
modified: netbox_dns_bridge/migrations/0003_catalogzone_and_per_zone_identifier.py
Adjust migration. This expects that the last migration the user has is 0002 (<=v1.5.8)
modified: netbox_dns_bridge/jobs/notify.py
modified: netbox_dns_bridge/management/commands/dns_transfer_endpoint/catalog_zone_manager.py
Update to use new db values for SOA creation.
Change UI to permit editing for SOA values and disable member identifier editing.
modified: netbox_dns_bridge/forms.py
modified: netbox_dns_bridge/navigation.py
new file: netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_edit.html
new file: netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_list.html
new file: netbox_dns_bridge/templates/netbox_dns_bridge/catalog_zone_members.html
deleted: netbox_dns_bridge/templates/netbox_dns_bridge/catz_overview.html
modified: netbox_dns_bridge/tables.py
modified: netbox_dns_bridge/urls.py
modified: netbox_dns_bridge/views.py
modified: netbox_dns_bridge/filtersets.py
GUI components are entirely AI generated. Plumbing also with AI assist but code verified.
deleted: netbox_dns_bridge/migrations/0003_integerkeyvaluesetting_key_unique.py
Migration 3 not required since model is being replaced anyway. Removed.
Track the catalog zone SOA serial per view instead of a single global value.
The new CatalogZone model has a 1:1 FK to netbox_dns.models.View and its own
soa_serial field; each view's catalog zone now increments independently. The
existing global serial is preserved during migration by copying it into every
view's new CatalogZone row.
Change CatalogZoneMemberIdentifier.name from globally unique to unique per
catalog zone (UniqueConstraint(name, catalog_zone)) and add a catalog_zone FK
to enforce it.
Remove the dns-settings management command, which was a generic interface over
the now-removed IntegerKeyValueSetting model.
Increment both catalog zone serials when a zone moves between views: the old
view's catalog zone loses a member and the new view's gains one, so both must
be bumped. Also refresh the member identifier's catalog_zone FK on a view move
so it does not go stale. The pre_save handler now captures _old_view_id and the
post_save handler detects view changes, increments the old view's serial inside
on_commit (before the new view's), and re-creates the identifier.
AI generated, code manually verified.
new file: netbox_dns_bridge/forms.py
new file: netbox_dns_bridge/tables.py
modified: netbox_dns_bridge/templates/netbox_dns_bridge/catz_overview.html
modified: netbox_dns_bridge/views.py
Change catz table to ObjectListView
new file: netbox_dns_bridge/navigation.py
new file: netbox_dns_bridge/templates/netbox_dns_bridge/info.html
new file: netbox_dns_bridge/urls.py
new file: netbox_dns_bridge/views.py
Add information view
RFC 1996 section 3.10 requires the SOA RR carrying the new zone serial
to be placed in the answer section of a NOTIFY message. BIND9's notify
processing looks for the SOA there and, when it is absent, logs that the
NOTIFY does not contain a SOA serial.
Both SendClientDNSNotify and SendNSNotify were appending the SOA RRset
to the authority section instead of the answer section, which is why
BIND9 reported the missing serial. Move the append to msg.answer in both
handlers so receivers can correctly extract the new serial.
The command loaded the zone with Zone.objects.get(...) and then tested
the result for truthiness:
nb_zone = Zone.objects.get(...)
if nb_zone:
...export...
else:
self.stderr.write('Zone not found in Netbox. Aborting')
Zone.objects.get() either returns a model instance (always truthy) or
raises Zone.DoesNotExist. It never returns a falsy value, so the else
branch was unreachable dead code. A missing zone is already handled
correctly: DoesNotExist is a subclass of Exception, which the
surrounding try/except catches and converts into a CommandError.
Drop the dead if/else and call export_bind_zone_file directly.
AXFR is a TCP-only operation (RFC 5936), but the request handler did
not enforce that. When a UDP datagram carrying an AXFR query arrived,
control reached _handle_axfr_request, which builds the multi-message
response and writes each chunk with:
self.request.sendall(len(wire).to_bytes(2, 'big') + wire)
For a UDP handler self.request is a (data, sock) tuple, not a socket,
so sendall raised AttributeError. The exception was caught by the
outer try/except in UDPRequestHandler.handle, so the server stayed up,
but the client received no response at all and the error was only
visible in a generic 'Error handling request' traceback.
Add a 'transport' class attribute ('tcp' / 'udp') to the concrete
handler classes and, immediately after the qtype check (before TSIG
validation and zone lookup), deny AXFR-over-UDP with REFUSED. The
client now gets a proper refusal and the AXFR handler is never entered
on a UDP socket.
IntegerKeyValueSetting is used as a singleton-style key/value store:
callers do get(key=...) and get_or_create(key=..., ...) and assume at
most one row per key. The model, however, did not enforce uniqueness on
the key field, so nothing prevented duplicate rows at the database
level.
This was a real race for _create_soa_rdataset in catalog_zone_manager,
which calls get_or_create without select_for_update (unlike
increment_soa_serial, which does take a lock). Two concurrent AXFR
queries for a catalog zone could both pass the get_or_create existence
check and both insert a row for key='catalog-zone-soa-serial'. Every
subsequent IntegerKeyValueSetting.objects.get(key=...) call would then
raise MultipleObjectsReturned, breaking catalog-zone SOA rendering and
serial increments.
Add unique=True to the key CharField and ship migration 0003 to enforce
the invariant at the database level. Existing deployments are assumed to
have clean state (no duplicate keys); if duplicates exist, the migration
should be preceded by manual deduplication.
In _load_tsig_key_settings, the OperationalError handler for the
View.objects.get(name=view_name) lookup called close_old_connections()
but should have exited the script instead. This is the initial startup
and if loading of settings fails, the script should simply exit.
Also contains some code cleaning
DNS names are case-insensitive (RFC 4343), but the catalog-zone
dispatch compared dname verbatim:
if dname == "catz" or dname == f"{nb_view.name}.catz":
dname is derived from qname.to_text(), which preserves the casing the
client sent. A query for CATZ, Public.catz, or any other mixed-case
variant failed the comparison and fell through to _get_zone_from_nb,
which returned None, so the request was denied instead of being served
as a catalog zone.
Lowercase both sides before comparing. Pass the lowercased name to
create_zone so catalog zones always get a canonical lowercase origin,
and pass the original dname to _get_zone_from_nb to leave the regular
zone-lookup behavior unchanged.
export_bind_zone_file diverged from the AXFR path in four ways, each a
bug:
- It iterated nb_zone.records.all(), including inactive records, whereas
the AXFR handler filtered Record.objects.filter(zone=nb_zone,
active=True). Exported zone files could contain stale/withdrawn
records.
- It passed record.ttl straight to rdataset.add(), crashing with a
TypeError when the NetBox DNS Record had no TTL set. The AXFR path
used 'ttl = record.ttl or nb_zone.default_ttl'.
- It had no TXT quoting logic, so TXT values containing spaces or
semicolons were mis-tokenized by dns.rdata.from_text (a ';' was
treated as the start of a comment). The AXFR path quotes and chunks
TXT values for exactly this reason.
- It appended a fresh rdataset to the node for every record instead of
merging records that share a name and type, producing duplicate
rdatasets and malformed zone files.
Extract build_dns_zone(nb_zone) in utils.py containing the corrected,
AXFR-proven record-building logic (active filter, TTL fallback, TXT
quoting/chunking, rdataset merging via rdatasets_dict). Rewrite
export_bind_zone_file to build the zone through build_dns_zone and then
write it. Rewrite request_handler._get_zone_from_nb to look up the
NetBox DNS Zone and delegate to build_dns_zone, keeping only the
DoesNotExist/OperationalError handling that is specific to the query
path.
Both code paths now produce identical zone data.
_getZoneFromNB was the only camelCase function in the codebase,
inconsistent with the snake_case convention used everywhere else.
Rename it and update its callers and the adjacent comment.
Pure rename, no behavior change.
The logging call passed the exception as a positional argument with no
%s placeholder in the format string:
LOGGER.error("Error parsing query: ", e)
Python's logging applies msg % args, which raised TypeError (not all
arguments converted during string formatting) inside logging.handleError.
The logging framework swallows that secondary error, so neither the
intended message nor the exception was ever recorded.
Use an f-string so the exception is actually rendered and logged.
Per PR review feedback:
- Rename settings: custom_field_force_notify -> notify_ns_custom_field_name,
force_notify_port -> notify_ns_port.
- Add notify_ns_all_zones (bool, default False) to enable the feature globally
for every zone; it coexists with the per-zone custom field — a NOTIFY is sent
if the global switch is on OR the zone's custom field is set.
- Rename the internal job/scheduler/log strings to the notify_ns naming for
consistency (SendZoneNSNotify, schedule_notify_ns).
- Update README and CHANGELOG.
v1.5.6 fixed the wrong RelatedObjectDoesNotExist import and the catz
member-identifier creation, but two get_or_create() calls still used the
returned value directly instead of unpacking the (obj, created) tuple:
- increment_soa_serial(): `soa_serial_obj.value` on a tuple
- _create_soa_rdataset(): `.get_or_create(...).value` on a tuple
Both raised `'tuple' object has no attribute 'value'` at runtime — the
first on every zone save, the second on every catalog-zone (catz) query.
Unpack the tuple in both places.
- Introduced `custom_field_force_notify` setting to trigger immediate NOTIFY to a zone's nameservers when its SOA serial number changes and the custom field is set to True.
- Updated README to document the new feature and its configuration.
- Refactored notify handler to include a new job for sending forced NOTIFY messages directly to zone nameservers.
- Enhanced zone signal handling to check for SOA serial changes and trigger forced NOTIFY accordingly.
modified: netbox_dns_bridge/catalog_zone_manager.py
Fix wrong import in previous version and slight change to
meachanism to create catz member identifier.
modified: netbox_dns_bridge/management/commands/dns-transfer-endpoint.py
modified: netbox_dns_bridge/request_handler.py
modified: netbox_dns_bridge/signals/netbox_dns_zones.py
Range of bug fixes and improvements
modified: __init__.py
modified: catalog_zone_manager.py
modified: dns_server.py
modified: notify_handler.py
Adding serial to notify messages to allow client to determine state.