import dns.name import dns.zone import dns.rdatatype import dns.rdataclass from .logger import get_logger from netbox_dns.models import Zone from netbox_dns_bridge.models import IntegerKeyValueSetting, CatalogZoneMemberIdentifier from uuid import uuid4 from base64 import b32encode from django.db import ( transaction, close_old_connections, OperationalError, RelatedObjectDoesNotExist ) LOGGER = get_logger(__name__) def increment_soa_serial() -> int: with transaction.atomic(): try: soa_serial_obj = ( IntegerKeyValueSetting.objects.select_for_update().get_or_create( key="catalog-zone-soa-serial", defaults={"value": 1}, ) ) new_soa_serial = soa_serial_obj.value + 1 if new_soa_serial > 0xFFFFFFFF: LOGGER.warning( f"Catalog serial {soa_serial_obj.value} reached max — wrapping back to 1" ) new_soa_serial = 1 soa_serial_obj.value = new_soa_serial soa_serial_obj.save() soa_serial_obj.refresh_from_db() LOGGER.debug( f"Catalog zone SOA serial number is now {soa_serial_obj.value}" ) return soa_serial_obj.value except OperationalError as e: LOGGER.error( f"ERROR: Failed to increment the Catalog Zone's SOA serial: {e}" ) close_old_connections() def create_zone(name, view_name) -> dns.zone.Zone: # Zone origin origin = dns.name.from_text(name, dns.name.root) # Create a new empty zone zone = dns.zone.Zone(origin) zone.rdclass = dns.rdataclass.IN try: # get zones from netbox nb_zones = Zone.objects.filter( view__name=view_name, active=True ).select_related("catz_identifier") ptr_base = dns.name.from_text("zones", origin) for nb_zone in nb_zones: ttl = 0 qname = dns.name.from_text(nb_zone.name, dns.name.root) # Create PTR record try: catz_identifier = nb_zone.catz_identifier except RelatedObjectDoesNotExist: catz_identifier = nb_zone.catz_identifier.create( name=_generate_member_identifier() ) p_name = catz_identifier.name ptr_name = dns.name.from_text(p_name, ptr_base) if not ptr_name.is_subdomain(origin): raise ValueError( f"Catalog zone member identifier {ptr_name.to_text()} not a subdomain" ) rdata = dns.rdata.from_text( dns.rdataclass.IN, dns.rdatatype.PTR, qname.to_text() ) rdataset = zone.find_rdataset(ptr_name, dns.rdatatype.PTR, create=True) rdataset.add(rdata, ttl) # Configure DNSSec Policy for member Zone if DNSSec is enabled if nb_zone.dnssec_policy: rid = dns.name.from_text("group", ptr_name) policy_name = nb_zone.dnssec_policy.name.rstrip(" ") group_name = f"dnssec-policy-{policy_name}" rdata = dns.rdata.from_text( dns.rdataclass.IN, dns.rdatatype.TXT, group_name ) rdataset = zone.find_rdataset(rid, dns.rdatatype.TXT, create=True) rdataset.add(rdata, ttl) zone_root_node = zone.find_node(origin, create=True) # SOA record soa_rdataset = _create_soa_rdataset() zone_root_node.rdatasets.append(soa_rdataset) # NS record ns_rdataset = _create_ns_rdataset() zone_root_node.rdatasets.append(ns_rdataset) # version.catz. record version_rdataset = _create_version_rdataset() zone_version_node = zone.find_node( dns.name.from_text("version", origin), create=True ) zone_version_node.rdatasets.append(version_rdataset) return zone except OperationalError as e: LOGGER.error(f"ERROR: Failed to create Catalog Zone: {e}") close_old_connections() return None def _generate_member_identifier() -> str: return b32encode(uuid4().bytes)[0:26].lower().decode("UTF-8") def update_member_identifier(zone: Zone) -> None: try: CatalogZoneMemberIdentifier.objects.update_or_create( zone=zone, defaults={"name": _generate_member_identifier()}, ) except OperationalError as e: LOGGER.error(f"ERROR: Failed to update Catalog Zone member identifier: {e}") close_old_connections() def _create_soa_rdataset() -> dns.rdataset: try: serial = IntegerKeyValueSetting.objects.get_or_create( key="catalog-zone-soa-serial", defaults={"value": 1} ).value # SOA Record components ttl = 0 rclass = dns.rdataclass.IN rtype = dns.rdatatype.SOA # Create SOA rdata object rdata = dns.rdata.from_text( rclass, rtype, f"invalid. invalid. {serial} 60 10 1209600 0", ) # Create Rdataset and add the RDATA to it rdataset = dns.rdataset.Rdataset(rclass, rtype) rdataset.add(rdata, ttl) return rdataset except OperationalError as e: LOGGER.error(f"ERROR: Failed to access catz SOA record in database: {e}") close_old_connections() def _create_ns_rdataset() -> dns.rdataset: # NS record for catz. name = dns.name.from_text("invalid", dns.name.root) rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.NS, str(name)) rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.NS) rdataset.add(rdata, 0) return rdataset def _create_version_rdataset() -> dns.rdataset: rdata = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.TXT, '"2"') rdataset = dns.rdataset.Rdataset(dns.rdataclass.IN, dns.rdatatype.TXT) rdataset.add(rdata, 0) return rdataset