Use parent subnet for DNS/TFTP server if none available in child subnet

Change-Id: Id58483cc739de570812cd6493ef5114a56f81625
diff --git a/scripts/netbox_edgeconfig.py b/scripts/netbox_edgeconfig.py
index a9b2172..1ba9dc5 100644
--- a/scripts/netbox_edgeconfig.py
+++ b/scripts/netbox_edgeconfig.py
@@ -38,6 +38,9 @@
 device_services_cache = {}
 interface_mac_cache = {}
 
+# parent prefixes
+parent_prefixes = {}
+
 
 def parse_nb_args():
     """
@@ -124,7 +127,7 @@
     return data
 
 
-def create_dns_zone(extension, devs):
+def create_dns_zone(extension, devs, parent_devs={}):
     # Checks for dns entries
 
     a_recs = {}  # PTR records created by inverting this
@@ -201,6 +204,23 @@
             if svc["port"] == 53 and svc["protocol"] == "udp":
                 ns_recs.append("%s.%s." % (dns_name, extension))
 
+    # iterate over the parent devs to add additional nameservers
+    for pname, pval in parent_devs.items():
+        if "services" in pval:
+            for svc in pval["services"]:
+                # look for DNS servers
+                if svc["port"] == 53 and svc["protocol"] == "udp":
+                    # make name
+                    dns_name = re.sub("[^a-z0-9.-]", "-", pname, 0, re.ASCII)
+
+                    # add an a record for this nameserver if IP is outside of subnet
+                    a_recs[dns_name] = pval["ip4"]
+
+                    # add a NS record if it doesn't already exist
+                    ns_name = "%s.%s." % (dns_name, extension)
+                    if ns_name not in ns_recs:
+                        ns_recs.append(ns_name)
+
     return {
         "a": a_recs,
         "cname": cname_recs,
@@ -210,7 +230,7 @@
     }
 
 
-def create_dhcp_subnet(prefix, prefix_search, devs):
+def create_dhcp_subnet(prefix, prefix_search, devs, parent_devs={}):
     # makes DHCP subnet information
 
     subnet = {}
@@ -218,46 +238,75 @@
     subnet["subnet"] = prefix
     subnet["dns_search"] = [prefix_search]
 
-    hosts = []
-    dns_servers = []
+    def dhcp_iterate(devs):
+        # inner function to iterate over a dev list
+        ihosts = []
+        idyn_range = None
+        irouter = None
+        idns_servers = []
+        itftpd_server = None
 
-    for name, value in devs.items():
+        for name, value in devs.items():
 
-        # handle a DHCP range
-        if name == "prefix_dhcp":
-            subnet["range"] = value["dhcp_range"]
-            continue
+            # handle a DHCP range
+            if name == "prefix_dhcp":
+                idyn_range = value["dhcp_range"]
+                continue
 
-        # handle a router reservation
-        if name == "router":
-            subnet["routers"] = value["ip4"]
-            continue
+            # handle a router reservation
+            if name == "router":
+                irouter = value["ip4"]
+                continue
 
-        # has a MAC address, and it's not null
-        if "macaddr" in value and value["macaddr"]:
+            # has a MAC address, and it's not null
+            if "macaddr" in value and value["macaddr"]:
 
-            hosts.append(
-                {
-                    "name": name,
-                    "ip_addr": value["ip4"],
-                    "mac_addr": value["macaddr"].lower(),
-                }
-            )
+                ihosts.append(
+                    {
+                        "name": name,
+                        "ip_addr": value["ip4"],
+                        "mac_addr": value["macaddr"].lower(),
+                    }
+                )
 
-        # Add dns based on service entries
-        if "services" in value:
-            for svc in value["services"]:
+            # Add dns based on service entries
+            if "services" in value:
+                for svc in value["services"]:
 
-                # add DNS server
-                if svc["port"] == 53 and svc["protocol"] == "udp":
-                    dns_servers.append(value["ip4"])
+                    # add DNS server
+                    if svc["port"] == 53 and svc["protocol"] == "udp":
+                        idns_servers.append(value["ip4"])
 
-                # add tftp server
-                if svc["port"] == 69 and svc["protocol"] == "udp":
-                    subnet["tftpd_server"] = value["ip4"]
+                    # add tftp server
+                    if svc["port"] == 69 and svc["protocol"] == "udp":
+                        itftpd_server = value["ip4"]
 
+        return (ihosts, idyn_range, irouter, idns_servers, itftpd_server)
+
+    # run inner function and build
+    hosts, dyn_range, router, dns_servers, tftpd_server = dhcp_iterate(devs)
+
+    # assign only hosts, dynamic range, based on the prefix
     subnet["hosts"] = hosts
-    subnet["dns_servers"] = dns_servers
+    subnet["range"] = dyn_range
+
+    # only assign router if specified
+    if router:
+        subnet["routers"] = router
+
+    # find parent prefix devices, to fill in where needed
+    phosts, pdyn_range, prouter, pdns_servers, ptftpd_server = dhcp_iterate(parent_devs)
+
+    # use parent prefix devices if dns/tftp services needed aren't found within prefix
+    if dns_servers:
+        subnet["dns_servers"] = dns_servers
+    else:
+        subnet["dns_servers"] = pdns_servers
+
+    if tftpd_server:
+        subnet["tftpd_server"] = tftpd_server
+    else:
+        subnet["tftpd_server"] = ptftpd_server
 
     return subnet
 
@@ -267,9 +316,14 @@
 
     first_ip = str(netaddr.IPAddress(netaddr.IPNetwork(prefix).first + 1))
 
+    # look for interface corresponding to first IP address in range
     for name, value in devs.items():
-        if value["ip4"] == first_ip:
-            return value["iface"]
+        if "ip4" in value:
+            if value["ip4"] == first_ip:
+                return value["iface"]
+
+    # if interface not found, return None and ignore
+    return None
 
 
 def get_device_services(device_id, filters=""):
@@ -445,6 +499,27 @@
     return devs
 
 
+def get_parent_prefix(child_prefix):
+    # returns a parent prefix given a child prefix
+    # FIXME: only returns the first found prefix, so doesn't handle more than 2 layers of  hierarchy
+
+    # get all devices in a prefix
+    url = "%s%s" % (
+        settings["api_endpoint"],
+        "api/ipam/prefixes/?contains=%s" % child_prefix,
+    )
+
+    raw_prefixes = json_api_get(url, headers, validate_certs=settings["validate_certs"])
+
+    logger.debug(raw_prefixes)
+
+    for prefix in raw_prefixes["results"]:
+        if prefix["prefix"] != child_prefix:
+            return prefix["prefix"]
+
+    return None
+
+
 def get_prefix_data(prefix):
 
     # get all devices in a prefix
@@ -494,11 +569,28 @@
     dhcpd_subnets = []
     dhcpd_interfaces = []
     devs_per_prefix = {}
+    prefixes = {}
+    parent_prefixes = {}
 
     for prefix in settings["ip_prefixes"]:
 
         prefix_data = get_prefix_data(prefix)
 
+        parent_prefix = get_parent_prefix(prefix)
+        prefix_data["parent"] = parent_prefix
+
+        pdevs = {}
+        if parent_prefix:
+            if parent_prefix in parent_prefixes:
+                pdevs = devs_per_prefix[parent_prefix]
+            else:
+                pdevs = get_prefix_devices(parent_prefix)
+                devs_per_prefix[parent_prefix] = pdevs
+
+        prefix_data["parent_devs"] = pdevs
+
+        prefixes[prefix] = prefix_data
+
         prefix_domain_extension = prefix_data["description"]
 
         devs = get_prefix_devices(prefix)
@@ -506,16 +598,18 @@
         devs_per_prefix[prefix] = devs
 
         dns_zones[prefix_domain_extension] = create_dns_zone(
-            prefix_domain_extension, devs
+            prefix_domain_extension, devs, pdevs
         )
 
         dns_zones[prefix_domain_extension]["ip_range"] = prefix
 
-        dhcpd_subnets.append(create_dhcp_subnet(prefix, prefix_domain_extension, devs))
+        dhcpd_subnets.append(
+            create_dhcp_subnet(prefix, prefix_domain_extension, devs, pdevs)
+        )
 
         dhcpd_if = find_dhcpd_interface(prefix, devs)
 
-        if dhcpd_if not in dhcpd_interfaces:
+        if dhcpd_if and dhcpd_if not in dhcpd_interfaces:
             dhcpd_interfaces.append(dhcpd_if)
 
     yaml_out.update(
@@ -524,7 +618,9 @@
             "dns_rev_zones": dns_rev_zones,
             "dhcpd_subnets": dhcpd_subnets,
             "dhcpd_interfaces": dhcpd_interfaces,
-            # "devs_per_prefix": devs_per_prefix,  # useful when debugging
+            # the below are useful when debugging
+            # "devs_per_prefix": devs_per_prefix,
+            # "prefixes": prefixes,
         }
     )