Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41d48f2807 | ||
|
|
e50cfd7cb6 | ||
|
|
55f6622997 | ||
|
|
fd8a5a0d9d | ||
|
|
b72fa9b664 |
No files matched your search
@@ -74,27 +74,29 @@ multiple views yields unpredicted behavior.
|
|||||||
#### NOTIFY
|
#### NOTIFY
|
||||||
Clients may be notified of zone changes using the NOTIFY mechanism defined in RFC 1996.
|
Clients may be notified of zone changes using the NOTIFY mechanism defined in RFC 1996.
|
||||||
When enabled, the zone transfer endpoint keeps track of any client that successfully queried the
|
When enabled, the zone transfer endpoint keeps track of any client that successfully queried the
|
||||||
endpoint and when a zone changes. Once a zone has changed, NetBox informs each client of change.
|
endpoint. Once a zone has changed, NetBox informs each client about the zone changed so that the
|
||||||
|
client can request a new zone transfer.
|
||||||
|
|
||||||
Note that this feature uses NetBox's background job system to schedule the messages asynchronously.
|
Note that this feature uses NetBox's background job system to schedule the messages asynchronously.
|
||||||
In order to work, you need at least one `rq-worker` service running in the background to handle
|
In order to work, you need at least one `rq-worker` service running in the background to handle
|
||||||
the queried jobs.
|
the queued jobs.
|
||||||
|
|
||||||
To enable the NOTIFY feature, set following:
|
##### NOTIFY Settings
|
||||||
```
|
```
|
||||||
'notify_clients': True
|
'notify_clients': True
|
||||||
```
|
```
|
||||||
##### NOTIFY Settings
|
This enables the NOTIFY system. Defaults to False
|
||||||
|
|
||||||
```
|
```
|
||||||
notify_client_alive_threshold_hours: 24
|
'notify_client_alive_threshold_hours': 24
|
||||||
```
|
```
|
||||||
This sets how long a client is considred "alive" after it last queried the transfer endpoint.
|
This sets how long a client is considred "alive" after it last queried the transfer endpoint.
|
||||||
Once the client has failed to check in for this amout of time, it is automatically removed from the
|
Once the client has failed to check in for this amout of time, it is automatically removed from the
|
||||||
list of clients to be notified on zone changes. Default is 24 hours.
|
list of clients to be notified on zone changes. Default is 24 hours.
|
||||||
```
|
```
|
||||||
notify_over_tcp: True
|
'notify_over_tcp': True
|
||||||
```
|
```
|
||||||
Switch to using TCP instead of UDP. Default: False
|
Switch to using TCP instead of UDP. Defaults to False
|
||||||
|
|
||||||
## Plugin compatibility
|
## Plugin compatibility
|
||||||
This plugin extends the netbox-plugin-dns plugin. As such the versioning was changed to match the
|
This plugin extends the netbox-plugin-dns plugin. As such the versioning was changed to match the
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from netbox.plugins import PluginConfig
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from .logger import get_logger
|
from .logger import get_logger
|
||||||
|
|
||||||
__version__ = "1.5.4"
|
__version__ = "1.5.6"
|
||||||
|
|
||||||
LOGGER = get_logger(__name__)
|
LOGGER = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import dns.zone
|
|||||||
import dns.rdatatype
|
import dns.rdatatype
|
||||||
import dns.rdataclass
|
import dns.rdataclass
|
||||||
from .logger import get_logger
|
from .logger import get_logger
|
||||||
from netbox_dns.models import Zone
|
from netbox_dns.models import Zone as NBZone
|
||||||
from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier
|
from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from base64 import b32encode
|
from base64 import b32encode
|
||||||
@@ -12,30 +12,30 @@ from django.db import transaction, close_old_connections, OperationalError
|
|||||||
LOGGER = get_logger(__name__)
|
LOGGER = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def init() -> None:
|
|
||||||
_init_soa_serial()
|
|
||||||
_create_missing_member_identifiers()
|
|
||||||
|
|
||||||
|
|
||||||
def increment_soa_serial() -> int:
|
def increment_soa_serial() -> int:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
soa_serial = IntegerKeyValueSetting.objects.select_for_update().get(
|
soa_serial_obj = (
|
||||||
key="catalog-zone-soa-serial"
|
IntegerKeyValueSetting.objects.select_for_update().get_or_create(
|
||||||
|
key="catalog-zone-soa-serial",
|
||||||
|
defaults={"value": 1},
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
new_soa_serial = soa_serial.value + 1
|
new_soa_serial = soa_serial_obj.value + 1
|
||||||
if new_soa_serial > 0xFFFFFFFF:
|
if new_soa_serial > 0xFFFFFFFF:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
f"Catalog serial {soa_serial.value} reached max — wrapping back to 1"
|
f"Catalog serial {soa_serial_obj.value} reached max — wrapping back to 1"
|
||||||
)
|
)
|
||||||
new_soa_serial = 1
|
new_soa_serial = 1
|
||||||
soa_serial.value = new_soa_serial
|
soa_serial_obj.value = new_soa_serial
|
||||||
soa_serial.save()
|
soa_serial_obj.save()
|
||||||
soa_serial.refresh_from_db()
|
soa_serial_obj.refresh_from_db()
|
||||||
LOGGER.debug(f"Catalog zone SOA serial number is now {soa_serial.value}")
|
LOGGER.debug(
|
||||||
|
f"Catalog zone SOA serial number is now {soa_serial_obj.value}"
|
||||||
|
)
|
||||||
|
|
||||||
return soa_serial.value
|
return soa_serial_obj.value
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
LOGGER.error(
|
LOGGER.error(
|
||||||
f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}"
|
f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}"
|
||||||
@@ -53,7 +53,7 @@ def create_zone(name, view_name) -> dns.zone.Zone:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# get zones from netbox
|
# get zones from netbox
|
||||||
nb_zones = Zone.objects.filter(
|
nb_zones = NBZone.objects.filter(
|
||||||
view__name=view_name, active=True
|
view__name=view_name, active=True
|
||||||
).select_related("catz_identifier")
|
).select_related("catz_identifier")
|
||||||
|
|
||||||
@@ -64,7 +64,14 @@ def create_zone(name, view_name) -> dns.zone.Zone:
|
|||||||
qname = dns.name.from_text(nb_zone.name, dns.name.root)
|
qname = dns.name.from_text(nb_zone.name, dns.name.root)
|
||||||
|
|
||||||
# Create PTR record
|
# Create PTR record
|
||||||
p_name = nb_zone.catz_identifier.name
|
try:
|
||||||
|
catz_identifier = nb_zone.catz_identifier
|
||||||
|
except NBZone.catz_identifier.RelatedObjectDoesNotExist:
|
||||||
|
catz_identifier = CatalogZoneMemberIdentifier.objects.create(
|
||||||
|
zone=nb_zone, name=_generate_member_identifier()
|
||||||
|
)
|
||||||
|
|
||||||
|
p_name = catz_identifier.name
|
||||||
|
|
||||||
ptr_name = dns.name.from_text(p_name, ptr_base)
|
ptr_name = dns.name.from_text(p_name, ptr_base)
|
||||||
if not ptr_name.is_subdomain(origin):
|
if not ptr_name.is_subdomain(origin):
|
||||||
@@ -113,25 +120,11 @@ def create_zone(name, view_name) -> dns.zone.Zone:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _init_soa_serial() -> None:
|
|
||||||
try:
|
|
||||||
IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial")
|
|
||||||
LOGGER.info("Catalog zone SOA serial number loaded from database")
|
|
||||||
except IntegerKeyValueSetting.DoesNotExist:
|
|
||||||
IntegerKeyValueSetting.objects.create(key="catalog-zone-soa-serial", value=1)
|
|
||||||
LOGGER.debug(
|
|
||||||
"Catalog zone SOA serial number was not set in the database. Set to 1"
|
|
||||||
)
|
|
||||||
except OperationalError as e:
|
|
||||||
LOGGER.error(f"ERROR: Failed to initialize the Catalog Zone's SOA serial: {e}")
|
|
||||||
close_old_connections()
|
|
||||||
|
|
||||||
|
|
||||||
def _generate_member_identifier() -> str:
|
def _generate_member_identifier() -> str:
|
||||||
return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8")
|
return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8")
|
||||||
|
|
||||||
|
|
||||||
def update_member_identifier(zone: Zone) -> None:
|
def update_member_identifier(zone: NBZone) -> None:
|
||||||
try:
|
try:
|
||||||
CatalogZoneMemberIdentifier.objects.update_or_create(
|
CatalogZoneMemberIdentifier.objects.update_or_create(
|
||||||
zone=zone,
|
zone=zone,
|
||||||
@@ -142,40 +135,11 @@ def update_member_identifier(zone: Zone) -> None:
|
|||||||
close_old_connections()
|
close_old_connections()
|
||||||
|
|
||||||
|
|
||||||
def _create_missing_member_identifiers() -> None:
|
|
||||||
try:
|
|
||||||
existing_zone_ids = CatalogZoneMemberIdentifier.objects.values_list(
|
|
||||||
"zone_id", flat=True
|
|
||||||
)
|
|
||||||
|
|
||||||
missing_zones = Zone.objects.exclude(id__in=existing_zone_ids)
|
|
||||||
|
|
||||||
new_objects = [
|
|
||||||
CatalogZoneMemberIdentifier(
|
|
||||||
zone=zone,
|
|
||||||
name=_generate_member_identifier(),
|
|
||||||
)
|
|
||||||
for zone in missing_zones
|
|
||||||
]
|
|
||||||
|
|
||||||
for identifier in new_objects:
|
|
||||||
LOGGER.debug(
|
|
||||||
f"Zone {identifier.zone} missing catz member identifier. Creating..."
|
|
||||||
)
|
|
||||||
|
|
||||||
CatalogZoneMemberIdentifier.objects.bulk_create(
|
|
||||||
new_objects,
|
|
||||||
ignore_conflicts=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
except OperationalError as e:
|
|
||||||
LOGGER.error(f"ERROR: Could not create catz member identifier: {e}")
|
|
||||||
close_old_connections()
|
|
||||||
|
|
||||||
|
|
||||||
def _create_soa_rdataset() -> dns.rdataset:
|
def _create_soa_rdataset() -> dns.rdataset:
|
||||||
try:
|
try:
|
||||||
serial = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial").value
|
serial = IntegerKeyValueSetting.objects.get_or_create(
|
||||||
|
key="catalog-zone-soa-serial", defaults={"value": 1}
|
||||||
|
).value
|
||||||
|
|
||||||
# SOA Record components
|
# SOA Record components
|
||||||
ttl = 0
|
ttl = 0
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from django.core.management.base import BaseCommand
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import close_old_connections, OperationalError
|
from django.db import close_old_connections, OperationalError
|
||||||
from netbox_dns.models import View
|
from netbox_dns.models import View
|
||||||
from netbox_dns_bridge import catalog_zone_manager as catzm
|
|
||||||
from netbox_dns_bridge.request_handler import UDPRequestHandler, TCPRequestHandler
|
from netbox_dns_bridge.request_handler import UDPRequestHandler, TCPRequestHandler
|
||||||
from netbox_dns_bridge.dns_server import UDPDNSServer, TCPDNSServer
|
from netbox_dns_bridge.dns_server import UDPDNSServer, TCPDNSServer
|
||||||
from netbox_dns_bridge.logger import get_logger
|
from netbox_dns_bridge.logger import get_logger
|
||||||
@@ -80,7 +79,6 @@ class Command(BaseCommand):
|
|||||||
# Load parameters
|
# Load parameters
|
||||||
port = options["port"]
|
port = options["port"]
|
||||||
address = options["address"]
|
address = options["address"]
|
||||||
catzm.init()
|
|
||||||
|
|
||||||
self.tsig_keys = SETTINGS.get("tsig_keys", None)
|
self.tsig_keys = SETTINGS.get("tsig_keys", None)
|
||||||
if not self.tsig_keys:
|
if not self.tsig_keys:
|
||||||
|
|||||||
@@ -52,12 +52,14 @@ class SendDNSNotify(JobRunner):
|
|||||||
view_name = kwargs["view_name"]
|
view_name = kwargs["view_name"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
catz_serial = IntegerKeyValueSetting.objects.get(key="catalog-zone-soa-serial").value
|
catz_serial = IntegerKeyValueSetting.objects.get(
|
||||||
|
key="catalog-zone-soa-serial"
|
||||||
|
).value
|
||||||
|
|
||||||
zones = [
|
zones = [
|
||||||
(kwargs["zone_name"], kwargs["soa_serial"]),
|
(kwargs["zone_name"], kwargs["soa_serial"]),
|
||||||
("catz", catz_serial),
|
("catz", catz_serial),
|
||||||
(f"{view_name}.catz", catz_serial)
|
(f"{view_name}.catz", catz_serial),
|
||||||
]
|
]
|
||||||
|
|
||||||
view = View.objects.get(name=view_name)
|
view = View.objects.get(name=view_name)
|
||||||
@@ -72,7 +74,7 @@ class SendDNSNotify(JobRunner):
|
|||||||
for zone_name, soa_serial in zones:
|
for zone_name, soa_serial in zones:
|
||||||
zone_name = dns.name.from_text(zone_name, dns.name.root).to_text()
|
zone_name = dns.name.from_text(zone_name, dns.name.root).to_text()
|
||||||
msg = dns.message.make_query(zone_name, dns.rdatatype.SOA)
|
msg = dns.message.make_query(zone_name, dns.rdatatype.SOA)
|
||||||
msg.flags = 0
|
msg.flags |= dns.flags.AA # = 0
|
||||||
msg.set_opcode(dns.opcode.NOTIFY)
|
msg.set_opcode(dns.opcode.NOTIFY)
|
||||||
|
|
||||||
soa_rdata = dns.rdata.from_text(
|
soa_rdata = dns.rdata.from_text(
|
||||||
@@ -80,9 +82,7 @@ class SendDNSNotify(JobRunner):
|
|||||||
dns.rdatatype.SOA,
|
dns.rdatatype.SOA,
|
||||||
f"invalid. invalid. {soa_serial} 60 10 1209600 0",
|
f"invalid. invalid. {soa_serial} 60 10 1209600 0",
|
||||||
)
|
)
|
||||||
soa_rrset = dns.rrset.from_rdata(
|
soa_rrset = dns.rrset.from_rdata(zone_name, 0, soa_rdata)
|
||||||
zone_name, 0, soa_rdata
|
|
||||||
)
|
|
||||||
msg.authority.append(soa_rrset)
|
msg.authority.append(soa_rrset)
|
||||||
msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name)
|
msg.use_tsig(keyring={tsig_key.name: tsig_key}, keyname=tsig_key.name)
|
||||||
|
|
||||||
@@ -114,13 +114,9 @@ class SendDNSNotify(JobRunner):
|
|||||||
|
|
||||||
def schedule(zone: Zone):
|
def schedule(zone: Zone):
|
||||||
try:
|
try:
|
||||||
soa_record = Record.objects.filter(
|
soa_record = Record.objects.filter(zone=zone, type="SOA", active=True).first()
|
||||||
zone=zone, type="SOA", active=True
|
|
||||||
).first()
|
|
||||||
if soa_record is None:
|
if soa_record is None:
|
||||||
LOGGER.error(
|
LOGGER.error(f"Zone {zone.name} has no active SOA record — skipping NOTIFY")
|
||||||
f"Zone {zone.name} has no active SOA record — skipping NOTIFY"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
soa_serial = int(soa_record.value.split()[2])
|
soa_serial = int(soa_record.value.split()[2])
|
||||||
@@ -130,9 +126,7 @@ def schedule(zone: Zone):
|
|||||||
close_old_connections()
|
close_old_connections()
|
||||||
return
|
return
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
LOGGER.error(
|
LOGGER.error(f"Failed to parse SOA serial for zone {zone.name}: {e}")
|
||||||
f"Failed to parse SOA serial for zone {zone.name}: {e}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
SendDNSNotify.enqueue(
|
SendDNSNotify.enqueue(
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ class DNSBaseRequestHandler(socketserver.BaseRequestHandler):
|
|||||||
else:
|
else:
|
||||||
rrsets.append(rrset)
|
rrsets.append(rrset)
|
||||||
|
|
||||||
|
if soa_rrset is None:
|
||||||
|
LOGGER.error(f"Zone {dname} has no SOA — aborting")
|
||||||
|
return
|
||||||
|
|
||||||
rrsets.insert(0, soa_rrset) # Opening SOA
|
rrsets.insert(0, soa_rrset) # Opening SOA
|
||||||
rrsets.append(soa_rrset) # Closing SOA
|
rrsets.append(soa_rrset) # Closing SOA
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,15 @@ def zone_post_save(sender, instance, created, **kwargs):
|
|||||||
zone = instance
|
zone = instance
|
||||||
|
|
||||||
# Quit if this is not the first signal in this session for this zone.
|
# Quit if this is not the first signal in this session for this zone.
|
||||||
|
try:
|
||||||
request = current_request.get()
|
request = current_request.get()
|
||||||
if request is not None:
|
|
||||||
handled_zones = getattr(request, "_handled_zones", set())
|
handled_zones = getattr(request, "_handled_zones", set())
|
||||||
if zone.pk in handled_zones:
|
if zone.pk in handled_zones:
|
||||||
return
|
return
|
||||||
handled_zones.add(zone.pk)
|
handled_zones.add(zone.pk)
|
||||||
request._handled_zones = handled_zones
|
request._handled_zones = handled_zones
|
||||||
|
except LookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
# On commit, increment soa serial and if notify is enabled, schedule bg job.
|
# On commit, increment soa serial and if notify is enabled, schedule bg job.
|
||||||
def _on_commit():
|
def _on_commit():
|
||||||
@@ -53,6 +55,7 @@ def zone_post_save(sender, instance, created, **kwargs):
|
|||||||
if old_name and zone.name != old_name:
|
if old_name and zone.name != old_name:
|
||||||
catzm.update_member_identifier(zone)
|
catzm.update_member_identifier(zone)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Zone)
|
@receiver(post_delete, sender=Zone)
|
||||||
def zone_post_delete(sender, instance, **kwargs):
|
def zone_post_delete(sender, instance, **kwargs):
|
||||||
catzm.increment_soa_serial()
|
catzm.increment_soa_serial()
|
||||||
Reference in new issue
Block a user