#!/usr/bin/python3 # # Maximilian Wilhelm # -- Sun 23 Jul 2023 04:46:19 PM CEST # from functools import cmp_to_key import ipaddress import re import ffho # The DNS zone base names used for generating zone files from IP address # configured on nodes interfaces. DNS_zone_names = { 'forward' : 'ffho.net', 'rev_v4' : [ '132.10.in-addr.arpa', '30.172.in-addr.arpa', ], 'rev_v6' : [ '2.4.3.2.0.6.2.2.3.0.a.2.ip6.arpa', ] } def _PTR_sort (PTR_entry_a, PTR_entry_b): PTR_a_octets = PTR_entry_a.split('.') PTR_b_octets = PTR_entry_b.split('.') # If both PTRs smell like IPv4, calculate 16 bit value and compare if len(PTR_a_octets) == 2 and len(PTR_b_octets) == 2: # Try to parse the octets as int and compare the values try: a_val = int(PTR_a_octets[1]) * 256 + int(PTR_a_octets[0]) b_val = int(PTR_b_octets[1]) * 256 + int(PTR_b_octets[0]) return ffho.cmp(a_val, b_val) except ValueError: # If that fails, falls back to comparing regularly pass # If both PTRs smell like an IPv6 PTR, reverse them and sort if len(PTR_entry_a) > 7 and len(PTR_entry_b) > 7: return ffho.cmp(PTR_entry_a[::-1], PTR_entry_b[::-1]) return ffho.cmp(PTR_entry_a, PTR_entry_b) def generate_DNS_entries (nodes_config, sites_config): forward_zone_name = "" forward_zone = [] zones = { # : [], # : [], # : [], # ... } zone_entries = { # : { # : # }, } # Fill zones dict with zones configured in DNS_zone_names at the top of this file. # Make sure the zone base names provided start with a leading . so the string # operations later can be done easily and safely. Proceed with fingers crossed. for entry, value in DNS_zone_names.items (): if entry == "forward": zone = value if not zone.startswith ('.'): zone = ".%s" % zone zones[zone] = forward_zone forward_zone_name = zone if entry in [ 'rev_v4', 'rev_v6' ]: for zone in value: if not zone.startswith ('.'): zone = ".%s" % zone zones[zone] = [] zone_entries[zone] = {} # Process all interfaace of all nodes defined in pillar and generate forward # and reverse entries for all zones defined in DNS_zone_names. Automagically # put reverse entries into correct zone. for fqdn in sorted (nodes_config): node_config = nodes_config.get (fqdn) ifaces = node_config.get("ifaces", {}) for iface in sorted (ifaces): iface_config = ifaces.get (iface) # We only care for interfaces with IPs configured prefixes = iface_config.get ("prefixes", None) if prefixes is None: continue # Ignore any interface in $VRF if iface_config.get ('vrf') is not None: continue if iface in ["anycast_srv", "srv"] or "_" in iface: continue for prefix in sorted (prefixes): ip = ipaddress.ip_address (u'%s' % prefix.split ('/')[0]) proto = 'v%s' % ip.version # The entry name is # if it's the primary IP # . else entry_name = "%s.%s" % (iface, fqdn) if prefix == node_config['primary_ips'].get(str(ip.version)): entry_name = fqdn # Ignore any anycast or service IP, or anything else configured on lo elif iface in ["lo"]: continue # Strip forward zone name from entry_name and store forward entry # with correct entry type for found IP address. forward_entry_name = re.sub (forward_zone_name, "", entry_name) forward_entry_typ = "A " if ip.version == 4 else "AAAA " # Longtest value currently present is 25 chars, so aling for 32 chars indent = " " + " " * (32 - len(forward_entry_name)) forward_zone.append (f"{forward_entry_name}{indent}IN {forward_entry_typ} {ip}") # Find correct reverse zone, if configured and strip reverse zone name # from calculated reverse pointer name. Store reverse entry if we found # a zone for it. If no configured reverse zone did match, this reverse # entry will be ignored. for zone in zones: if ip.reverse_pointer.find (zone) > 0: PTR_entry = re.sub (zone, "", ip.reverse_pointer) # IPv6 PTRs are always the same length (for /64 prefixes)... indent = " " # ... IPv4 are (for /16 prefixes), so align them nicely if ip.version == 4: indent += " " * (7 - len(PTR_entry)) zone_entries[zone][PTR_entry] = f"{indent}IN PTR {entry_name}." break for zone, entries in zone_entries.items(): if not entries: continue for PTR in sorted(entries.keys(), key = cmp_to_key(_PTR_sort)): zones[zone].append(f"{PTR}{entries[PTR]}") return zones