def allocate_dns_servers(network): """Allocates DNS according to rules defined above TODO: allow 3 level (ie no pop caching, clients connect to AS server) TODO: make DNS servers standalone rather that co-hosted with router TODO: note set dns level on dns graph, but ibgp level on physical graph - inconsistent! """ dns_graph = nx.DiGraph() dns_advertise_graph = nx.DiGraph() LOG.debug("DNS currently disabled") hierarchical_dns = config.settings['DNS']['hierarchical'] if hierarchical_dns: LOG.info("Configuring hierarchical DNS") dns_levels = 4 else: dns_levels = 1 LOG.debug("Non-hierarchical DNS not yet implemented") #TODO: do "flat" dns - one root server, add all other devices as children for both resolving and authoritative return def nodes_by_eccentricity(graph): if len(graph) == 1: return graph.nodes() # need to crop the global shortest paths otherwise get #NetworkXError: Graph not connected: infinite path length eccentricities = nx.eccentricity(graph) return sorted(eccentricities.keys(), key = lambda n: eccentricities[n]) def format_asn(asn): """Returns unique format for asn, so don't confuse with property of the same, eg if ibgp_l2_cluster = 1 in as2, it could match as1 routers as 1==1 so set asn_1 so 1 != asn_1""" return "asn_%s" % asn def get_l2_cluster(node): """syntactic sugar to access cluster""" return dns_graph.node[node].get("dns_l2_cluster") def get_l3_cluster(node): """syntactic sugar to access cluster""" return dns_graph.node[node].get("dns_l3_cluster") def level(u): return int(dns_graph.node[u]['level']) servers_per_l2_cluster = config.settings['DNS']['Server Count']['l2 cluster'] servers_per_l3_cluster = config.settings['DNS']['Server Count']['l3 cluster'] root_dns_servers = config.settings['DNS']['Server Count']['root'] global_eccentricities = nodes_by_eccentricity(network.graph) #TODO: add count of each cluster occurence so can round servers down - dont want 3 servers in a one router network! # Add routers, these form the level 1 clients dns_graph.add_nodes_from(network.graph.nodes(), level=1) for node, data in network.graph.nodes(data=True): #TODO: the cluster should never be manually set, so can remove checks if not data.get("dns_l2_cluster"): dns_graph.node[node]['dns_l2_cluster'] = data.get("pop") or format_asn(network.asn(node)) if not data.get("dns_l3_cluster"): dns_graph.node[node]['dns_l3_cluster'] = format_asn(network.asn(node)) for my_as in ank.get_as_graphs(network): asn = my_as.asn if not nx.is_strongly_connected(my_as): LOG.info("AS%s not fully connected, skipping DNS configuration" % asn) continue l2_clusters = list(set(dns_graph.node[n].get("dns_l2_cluster") for n in my_as)) for l2_cluster in l2_clusters: for index in range(servers_per_l2_cluster): label = "l2_%s_dns_%s" % (l2_cluster, index+1) if l2_cluster == format_asn(asn): # Don't put asn into server name twice "AS2_asn_2_l2dns_1" vs "asn_2_l2dns_1" server_name = "%s_l2dns_%s" % (l2_cluster, index+1) else: server_name = "AS%s_%s_l2dns_%s" % (asn, l2_cluster, index+1) #TODO: see what other properties to retain node_name = network.add_device(server_name, asn=asn, device_type='server', label=label) dns_graph.add_node(node_name, level=2, dns_l2_cluster=l2_cluster, asn = asn, dns_l3_cluster = format_asn(asn)) for index in range(servers_per_l3_cluster): label = "l3_%s_dns_%s" % (asn, index+1) server_name = "AS%s_l3dns_%s" % (asn, index+1) node_name = network.add_device(server_name, asn=asn, device_type='server', label=label) #TODO: check if need to add l2 here - was coded before, possible mistake? dns_graph.add_node(node_name, level=3, asn = asn, dns_l3_cluster = format_asn(asn)) # and level 4 connections #TODO: need to determine the right place to put the server - order issue between allocating for root as need an ASN for the device before know best place - for now use asn = 1, and move if needed for index in range(root_dns_servers): attach_point = global_eccentricities.pop() server_name = "root_dns_%s" % (index+1) asn = ank.asn(attach_point) LOG.debug("Attaching %s to %s in %s" % (server_name, ank.label(attach_point), asn)) node_name = network.add_device(server_name, asn=asn, device_type='server') network.add_link(node_name, attach_point) dns_graph.add_node(node_name, level=4) # now connect #TODO: scale to handle multiple levels same as ibgp (see doco at start for details) edges_to_add = [] all_edges = [ (s,t) for s in dns_graph for t in dns_graph if s != t] same_l3_cluster_edges = [ (s,t) for (s,t) in all_edges if get_l3_cluster(s) == get_l3_cluster(t) != None] same_l2_cluster_edges = [ (s,t) for (s,t) in same_l3_cluster_edges if get_l2_cluster(s) == get_l2_cluster(t) != None] # l1 -> l2 same l2 cluster edges_to_add += [(s,t, 'up') for (s,t) in same_l2_cluster_edges if level(s) == 1 and level(t) == 2] # l2 -> l2 ??? # l2 -> l3 edges_to_add += [(s,t, 'up') for (s,t) in same_l3_cluster_edges if level(s) == 2 and level(t) == 3] # l3 -> l4 edges_to_add += [(s,t, 'up') for (s,t) in all_edges if level(s) == 3 and level(t) == 4] # format into networkx format edges_to_add = ( (s,t, {'dns_dir': dns_dir}) for (s, t, dns_dir) in edges_to_add) dns_graph.add_edges_from(edges_to_add) # and create attach points # take advantage of Python sorts being stable # refer http://wiki.python.org/moin/HowTo/Sorting #TODO: note assumes routers are level 1 - need to also check type is router! routers = set(network.routers()) devices = dns_graph.nodes() devices = sorted(devices, key= get_l2_cluster) devices = sorted(devices, key= get_l3_cluster) devices = sorted(devices, key= ank.asn) for asn, asn_devices in itertools.groupby(devices, key = ank.asn): # if no asn set, then root server, which has already been allocated if asn: # asn is set, look at l3 groups for l3_cluster, l3_cluster_devices in itertools.groupby(asn_devices, key = get_l3_cluster): if not l3_cluster: #TODO: see why getting empty cluster continue l3_cluster_devices = set(l3_cluster_devices) l3_cluster_servers = set(n for n in l3_cluster_devices if level(n) == 3) l3_cluster_routers = set(n for n in l3_cluster_devices if n in routers) l3_cluster_physical_graph = network.graph.subgraph(l3_cluster_routers) l3_cluster_eccentricities = nodes_by_eccentricity(l3_cluster_physical_graph) # Cycle in event more servers to attach than routers l3_cluster_eccentricities = itertools.cycle(l3_cluster_eccentricities) for server in l3_cluster_servers: attach_point = l3_cluster_eccentricities.next() LOG.debug("Attaching %s to %s in %s" % (ank.label(server), ank.label(attach_point), asn)) network.add_link(server, attach_point) l1l2_devices = l3_cluster_devices - set(l3_cluster_servers) # resort after set operations for groupby to work correctly l1l2_devices = sorted(l1l2_devices, key= get_l2_cluster) for l2_cluster, l2_cluster_devices in itertools.groupby(l1l2_devices, key = get_l2_cluster): l2_cluster_devices = set(l2_cluster_devices) l2_cluster_servers = set(n for n in l2_cluster_devices if level(n) == 2) l2_cluster_routers = set(n for n in l2_cluster_devices if level(n) == 1 and n in routers) l2_cluster_physical_graph = network.graph.subgraph(l2_cluster_routers) l2_cluster_eccentricities = nodes_by_eccentricity(l2_cluster_physical_graph) # Cycle in event more servers to attach than routers l2_cluster_eccentricities = itertools.cycle(l2_cluster_eccentricities) for server in l2_cluster_servers: attach_point = l2_cluster_eccentricities.next() LOG.debug("Attaching %s to %s in %s" % (ank.label(server), ank.label(attach_point), asn)) network.add_link(server, attach_point) #TODO: authoritative might need to be a graph also # setup domains for server in dns_servers(network): children = dns_auth_children(server) if len(children): # does auth, set domain network.g_dns.node[server]['domain'] = "AS%s" % server.asn # TODO: handle different levels # in 3 level model, l3 servers advertise for AS for my_as in ank.get_as_graphs(network): devices = [ n for n in my_as] as_l3_servers = (n for n in my_as if level(n) == 3) edges = itertools.product(devices, as_l3_servers) dns_advertise_graph.add_edges_from(edges) network.g_dns = dns_graph network.g_dns_auth = dns_advertise_graph