def dump_identifiers(network, filename): with open( filename, 'w') as f_dump: # writes out lo_ip for routers, and identifying IP for servers for my_as in ank.get_as_graphs(network): for router in sorted(network.routers(my_as.asn), key = lambda x: x.fqdn): f_dump.write( "%s\t%s\n" % (router, router.lo_ip.ip)) for server in sorted(network.servers(my_as.asn), key = lambda x: x.fqdn): f_dump.write( "%s\t%s\n" % (server, server_ip(server))) f_dump.write("\n")
def inv_cap_weights(network): """Updates link weights based on inverse of link speed.""" #TODO: rewrite this to be cleaner iteration and setting for graph in ank.get_as_graphs(network): for (src, dst, data) in graph.edges_iter(data=True): # only update if non default weight if 'speed' in data and 'weight' in data and data['weight'] == 1: # assume largest link is 10gb #TODO: use Cisco guidelines for this scale_speed = 100000 speed = float(data['speed']) weight = int((1/speed)*scale_speed) weight = max(weight, 1) if weight is 0: weight = 1 graph[src][dst]['weight'] = weight network.set_edge_property(src, dst, 'weight', weight) return
def configure(self): """Configure C-BGP""" LOG.info("Configuring C-BGP") self.initialise() default_weight = 1 template = lookup.get_template("cbgp/cbgp.mako") physical_graph = self.network.graph igp_graph = ank.igp_graph(self.network) ibgp_graph = ank.get_ibgp_graph(self.network) ebgp_graph = ank.get_ebgp_graph(self.network) as_graphs = ank.get_as_graphs(self.network) ip_as_allocs = ank.get_ip_as_allocs(self.network) # Allocs for ebgp announcements physical_topology = defaultdict(dict) ibgp_topology = {} igp_topology = {} ebgp_topology = {} ebgp_prefixes = {} bgp_routers = {} # Fast lookup of loopbacks - the unique router ID for cBGP loopback = dict( (n, self.network.lo_ip(n).ip) for n in physical_graph) # Physical topology for as_graph in as_graphs: asn = as_graph.name physical_topology[asn]['nodes'] = [loopback[n] for n in as_graph] physical_topology[asn]['links'] = [ (loopback[s], loopback[t]) for (s,t) in unidirectional(as_graph.edges())] # Interdomain links interdomain_links = [ (loopback[s], loopback[t]) for (s,t) in unidirectional(ebgp_graph.edges())] #IGP configuration for as_graph in as_graphs: asn = as_graph.name igp_topology[asn] = {} # Subgraph of IGP graph for this AS as_igp_graph = igp_graph.subgraph(as_graph.nodes()) igp_topology[asn]['nodes'] = [loopback[n] for n in as_igp_graph] igp_topology[asn]['links'] = [ (loopback[s], loopback[t], data.get('weight', default_weight)) for (s,t,data) in (as_graph.edges(data=True))] # iBGP configuration #TODO: if ibgp graph is a clique then use "bgp domain 1 full-mesh" where 1 is asn # use nx.graph_clique_number(G) and compare to size of G, if same then is a clique # otherwise create ibgp session by session #TODO: add support for non full-mesh (need to find documentation on this) for as_graph in as_graphs: asn = as_graph.name for router in as_graph: if not router.is_router: continue if router not in ibgp_graph: # likely single node AS continue ibgp_topology[router] = [] for peer in ibgp_graph.neighbors(router): ibgp_topology[router].append(peer) bgp_routers[asn] = [n.lo_ip.ip for n in ank.bgp_routers(self.network) if n.asn == asn] # eBGP configuration for node in ebgp_graph.nodes(): node_id = loopback[node] peers = [] for peer in ebgp_graph.neighbors(node): peers.append( (self.network.asn(peer), loopback[peer])) ebgp_topology[node_id] = peers # Prefixes to originate adv_subnet = ip_as_allocs[self.network.asn(node)] ebgp_prefixes[node_id] = adv_subnet #TODO: see if can just do for node in ebgp_graph ie without the .nodes() on end # bgp policy bgp_policy = {} for router in self.network.routers(): for peer in self.network.g_session.neighbors(router): pol_egress = self.network.g_session[router][peer]['egress'] pol_ingress = self.network.g_session[peer][router]['ingress'] if len(pol_ingress) or len(pol_egress): try: bgp_policy[router][peer] = { 'ingress': pol_ingress, 'egress': pol_egress, } except KeyError: bgp_policy[router] = {} bgp_policy[router][peer] = { 'ingress': pol_ingress, 'egress': pol_egress, } # tags dict for mapping from tag to community value, and for prefixes tags = self.network.g_session.graph['tags'] prefixes = self.network.g_session.graph['prefixes'] with open( cbgp_file(), 'w') as f_cbgp: f_cbgp.write( template.render( physical_topology = physical_topology, interdomain_links = interdomain_links, igp_topology = igp_topology, ibgp_topology = ibgp_topology, ebgp_topology = ebgp_topology, ebgp_prefixes = ebgp_prefixes, bgp_routers = bgp_routers, bgp_policy = bgp_policy, tags = tags, prefixes = prefixes, ))
def allocate_subnets(network, address_block=IPNetwork("10.0.0.0/8")): """Allocates subnets and IP addresses to links in the network. Args: address_block (IPNetwork): The address block to use. Returns: ip_as_allocs Example usage: >>> network = ank.example_multi_as() >>> allocate_subnets(network) >>> print ank.debug_nodes(network.graph, "lo_ip") {'1a.AS1': IPNetwork('10.0.0.32/32'), '1b.AS1': IPNetwork('10.0.0.33/32'), '1c.AS1': IPNetwork('10.0.0.34/32'), '2a.AS2': IPNetwork('10.1.0.64/32'), '2b.AS2': IPNetwork('10.1.0.65/32'), '2c.AS2': IPNetwork('10.1.0.66/32'), '2d.AS2': IPNetwork('10.1.0.67/32'), '3a.AS3': IPNetwork('10.2.0.0/32')} >>> print ank.debug_edges(network.graph, "ip") {('1a.AS1', '1b.AS1'): IPAddress('10.0.0.10'), ('1a.AS1', '1c.AS1'): IPAddress('10.0.0.22'), ('1b.AS1', '1a.AS1'): IPAddress('10.0.0.9'), ('1b.AS1', '1c.AS1'): IPAddress('10.0.0.26'), ('1b.AS1', '3a.AS3'): IPAddress('10.0.0.17'), ('1c.AS1', '1a.AS1'): IPAddress('10.0.0.21'), ('1c.AS1', '1b.AS1'): IPAddress('10.0.0.25'), ('1c.AS1', '2a.AS2'): IPAddress('10.0.0.29'), ('2a.AS2', '1c.AS1'): IPAddress('10.0.0.30'), ('2a.AS2', '2b.AS2'): IPAddress('10.1.0.10'), ('2a.AS2', '2d.AS2'): IPAddress('10.1.0.26'), ('2b.AS2', '2a.AS2'): IPAddress('10.1.0.9'), ('2b.AS2', '2c.AS2'): IPAddress('10.1.0.18'), ('2c.AS2', '2b.AS2'): IPAddress('10.1.0.17'), ('2c.AS2', '2d.AS2'): IPAddress('10.1.0.30'), ('2d.AS2', '2a.AS2'): IPAddress('10.1.0.25'), ('2d.AS2', '2c.AS2'): IPAddress('10.1.0.29'), ('2d.AS2', '3a.AS3'): IPAddress('10.1.0.33'), ('3a.AS3', '1b.AS1'): IPAddress('10.0.0.18'), ('3a.AS3', '2d.AS2'): IPAddress('10.1.0.34')} >>> print ank.debug_edges(network.graph, "sn") {('1a.AS1', '1b.AS1'): IPNetwork('10.0.0.8/30'), ('1a.AS1', '1c.AS1'): IPNetwork('10.0.0.20/30'), ('1b.AS1', '1a.AS1'): IPNetwork('10.0.0.8/30'), ('1b.AS1', '1c.AS1'): IPNetwork('10.0.0.24/30'), ('1b.AS1', '3a.AS3'): IPNetwork('10.0.0.16/30'), ('1c.AS1', '1a.AS1'): IPNetwork('10.0.0.20/30'), ('1c.AS1', '1b.AS1'): IPNetwork('10.0.0.24/30'), ('1c.AS1', '2a.AS2'): IPNetwork('10.0.0.28/30'), ('2a.AS2', '1c.AS1'): IPNetwork('10.0.0.28/30'), ('2a.AS2', '2b.AS2'): IPNetwork('10.1.0.8/30'), ('2a.AS2', '2d.AS2'): IPNetwork('10.1.0.24/30'), ('2b.AS2', '2a.AS2'): IPNetwork('10.1.0.8/30'), ('2b.AS2', '2c.AS2'): IPNetwork('10.1.0.16/30'), ('2c.AS2', '2b.AS2'): IPNetwork('10.1.0.16/30'), ('2c.AS2', '2d.AS2'): IPNetwork('10.1.0.28/30'), ('2d.AS2', '2a.AS2'): IPNetwork('10.1.0.24/30'), ('2d.AS2', '2c.AS2'): IPNetwork('10.1.0.28/30'), ('2d.AS2', '3a.AS3'): IPNetwork('10.1.0.32/30'), ('3a.AS3', '1b.AS1'): IPNetwork('10.0.0.16/30'), ('3a.AS3', '2d.AS2'): IPNetwork('10.1.0.32/30')} """ LOG.debug("Allocating subnets") # Initialise IP list to be graph edge format ip_as_allocs = {} # allocates subnets to the edges and loopback in network graph # Put into dictionary, indexed by ASN (the name attribute of each as graph) # for easy appending of eBGP links asgraphs = dict((my_as.asn, my_as) for my_as in ank.get_as_graphs(network)) # Simple method: break address_block into a /16 for each network #TODO: check this is feasible - ie against required host count subnet_list = address_block.subnet(16) ebgp_edges = ank.ebgp_edges(network) visited_ebgp_edges = set() for src, dst in sorted(ebgp_edges): # Add the dst (external peer) to AS of src node so they are allocated # a subnet. (The AS choice is arbitrary) if (dst, src) in visited_ebgp_edges: continue src_as = asgraphs[src.asn] src_as.add_edge(src, dst) # record for DNS purposes ank.dns_advertise_link(src, dst) visited_ebgp_edges.add( (src, dst)) for my_as in sorted(asgraphs.values(), key = lambda x: x.asn): asn = my_as.asn as_subnet = subnet_list.next() as_internal_nodes = [n for n in sorted(my_as.nodes()) if network.asn(n) == asn] host_count = my_as.number_of_nodes() # record this subnet ip_as_allocs[my_as.asn] = as_subnet # split into subnets for loopback and ptp ptp_count = my_as.number_of_edges() # Now subnet network into subnets of the larger of these two # TODO tidy up this comment # Note ptp subnets required a /30 ie 4 ips req_sn_count = max(host_count, 4*ptp_count) if req_sn_count == 0: # Nothing to allocate for this AS continue req_pref_len = int(32 - math.ceil(math.log(req_sn_count, 2)) ) # Subnet as subnet into subnets of this size sn_iter = as_subnet.subnet(req_pref_len) # And allocate a subnet for each ptp and loopback if ptp_count > 0: # Don't allocate a ptp subnet if there are no ptp links ptp_subnet = sn_iter.next() loopback_subnet = sn_iter.next() if ptp_count > 0: link_subnet = ptp_subnet.subnet(30) # Now iterate over edges in this as and apply for src, dst in sorted(my_as.edges()): # Note we apply this back to the main graph # not to the as graph! subnet = link_subnet.next() #TODO: fix the technique for accessing edges # as it breaks with multigraphs, as it creates a new edge if network.asn(dst) != asn: # eBGP link where dst has IP allocated from subnet of this AS network.graph[dst][src]['remote_as_sn_block'] = True network.graph[src][dst]['sn'] = subnet network.graph[dst][src]['sn'] = subnet # allocate an ip to each end network.graph[src][dst]['ip'] = subnet[1] network.graph[dst][src]['ip'] = subnet[2] # Allocate an loopback interface to each router #TODO: check if next step is necessary loopback_ips = loopback_subnet.subnet(32) for rtr in sorted(as_internal_nodes): lo_ip = loopback_ips.next() network.graph.node[rtr]['lo_ip'] = lo_ip network.ip_as_allocs = ip_as_allocs
def alloc_tap_hosts(network, address_block=IPNetwork("172.16.0.0/16")): """Allocates TAP IPs for connecting using Netkit >>> network = ank.example_multi_as() >>> alloc_tap_hosts(network) >>> print ank.debug_nodes(network.graph, "tap_ip") {'1a.AS1': IPAddress('172.16.1.1'), '1b.AS1': IPAddress('172.16.1.2'), '1c.AS1': IPAddress('172.16.1.3'), '2a.AS2': IPAddress('172.16.2.1'), '2b.AS2': IPAddress('172.16.2.2'), '2c.AS2': IPAddress('172.16.2.3'), '2d.AS2': IPAddress('172.16.2.4'), '3a.AS3': IPAddress('172.16.3.1')} """ LOG.debug("Allocating TAP hosts") network.tap_sn = address_block as_graph = ank.get_as_graphs(network) # Try allocating /24 to each subnet as cleaner # then check if this is feasible prefix_len = 24 # Check this will fit into provided network #TODO: check what these 2 lines of code are doing if prefix_len <= address_block.prefixlen: # Try with smaller prefix len prefix_len += 1 # Number of bits required for AS subnet (network bits) # need to add one on for tap host subnet, eg 172.16.0.0 and 172.16.0.1 hosts req_network_bits = int( math.ceil(math.log(len(as_graph) + 1, 2)) ) upper_bound = prefix_len # Find the subnet with the most hosts in it max_req_hosts = max(len(my_as) for my_as in as_graph) req_host_bits = int(math.ceil(math.log(max_req_hosts, 2))) #TODO: there is an off by one error here mking the tap subnets /9 rather than /8 # so end up with 172.16.64.1 and 172.16.128.1 not .1 .2 etc # Check subnetting is feasible lower_bound = address_block.prefixlen + req_network_bits upper_bound = lower_bound + req_host_bits if upper_bound > 32: #TODO: throw error print "Unfeasible tap subnet allocation" return else: prefix_len = lower_bound # Neatness: use a Class C, B, A (in order of preference) if feasible for x in [24, 16, 8]: if lower_bound < x < upper_bound: prefix_len = x elif lower_bound < upper_bound < x: # eg both fit inside a class A, B or C prefix_len = x def set_tap_ips(network, nodes, host_ips): # Allocate in order of node name for node in sorted(nodes, key=network.label): network.graph.node[node]['tap_ip'] = host_ips.next() return # assign /required subnet size to each as then append pops to list if len(as_graph) == 1: # Single AS, don't need to subnet the address block host_ips = address_block.iter_hosts() network.tap_host = host_ips.next() _ = host_ips.next() # IP of tap VM my_as = as_graph.pop() # Allocate directly from address block set_tap_ips(network, my_as.nodes(), host_ips) else: sn_iter = address_block.subnet(prefix_len) tap_host_subnet = sn_iter.next() #TODO: make consistent with previous section [1] vs .next() network.tap_host = tap_host_subnet[1] for my_as in as_graph: LOG.debug("Setting tap IPs for %s" % my_as.asn) host_ips = sn_iter.next().iter_hosts() set_tap_ips(network, my_as.nodes(), host_ips) #TODO: make this a generic function which allocates items from an # generator to each node in the specified network # rather than IP addresses specifically return
def configure_bgp(self): """Generates BGP specific configuration files""" ip_as_allocs = ank.get_ip_as_allocs(self.network) LOG.debug("Configuring BGP") template = lookup.get_template("quagga/bgp.mako") route_maps = {} ibgp_graph = ank.get_ibgp_graph(self.network) ebgp_graph = ank.get_ebgp_graph(self.network) physical_graph = self.network.graph for my_as in ank.get_as_graphs(self.network): asn = my_as.asn LOG.debug("Configuring IGP for AS %s " % asn) # get nodes ie intersection #H = nx.intersection(my_as, ibgp_graph) # get ibgp graph that contains only nodes from this AS for router in self.network.routers(asn): bgp_groups = {} route_maps = [] ibgp_neighbor_list = [] ibgp_rr_client_list = [] route_map_groups = {} if router in ibgp_graph: for src, neigh, data in ibgp_graph.edges(router, data=True): route_maps_in = self.network.g_session[neigh][router]['ingress'] rm_group_name_in = None if len(route_maps_in): rm_group_name_in = "rm_%s_in" % neigh.folder_name route_map_groups[rm_group_name_in] = [match_tuple for route_map in route_maps_in for match_tuple in route_map.match_tuples] route_maps_out = self.network.g_session[router][neigh]['egress'] rm_group_name_out = None if len(route_maps_out): rm_group_name_in = "rm_%s_out" % neigh.folder_name route_map_groups[rm_group_name_out] = [match_tuple for route_map in route_maps_out for match_tuple in route_map.match_tuples] description = data.get("rr_dir") + " to " + ank.fqdn(self.network, neigh) if data.get('rr_dir') == 'down': ibgp_rr_client_list.append( { 'id': self.network.lo_ip(neigh).ip, 'description': description, 'route_maps_in': rm_group_name_in, 'route_maps_out': rm_group_name_out, }) elif (data.get('rr_dir') in set(['up', 'over', 'peer']) or data.get('rr_dir') is None): ibgp_neighbor_list.append( { 'id': self.network.lo_ip(neigh).ip, 'description': description, 'route_maps_in': rm_group_name_in, 'route_maps_out': rm_group_name_out, }) bgp_groups['internal_peers'] = { 'type': 'internal', 'neighbors': ibgp_neighbor_list } if len(ibgp_rr_client_list): bgp_groups['internal_rr'] = { 'type': 'internal', 'neighbors': ibgp_rr_client_list, 'cluster': self.network.lo_ip(router).ip, } if router in ebgp_graph: external_peers = [] for peer in ebgp_graph.neighbors(router): route_maps_in = self.network.g_session[peer][router]['ingress'] rm_group_name_in = None if len(route_maps_in): rm_group_name_in = "rm_%s_in" % peer.folder_name route_map_groups[rm_group_name_in] = [match_tuple for route_map in route_maps_in for match_tuple in route_map.match_tuples] # Now need to update the sequence numbers for the flattened route maps route_maps_out = self.network.g_session[router][peer]['egress'] rm_group_name_out = None if len(route_maps_out): rm_group_name_out = "rm_%s_out" % peer.folder_name route_map_groups[rm_group_name_out] = [match_tuple for route_map in route_maps_out for match_tuple in route_map.match_tuples] peer_ip = physical_graph[peer][router]['ip'] external_peers.append({ 'id': peer_ip, 'route_maps_in': rm_group_name_in, 'route_maps_out': rm_group_name_out, 'peer_as': self.network.asn(peer)}) bgp_groups['external_peers'] = { 'type': 'external', 'neighbors': external_peers} # Ensure only one copy of each route map, can't use set due to list inside tuples (which won't hash) # Use dict indexed by name, and then extract the dict items, dict hashing ensures only one route map per name community_lists = {} prefix_lists = {} node_bgp_data = self.network.g_session.node.get(router) if node_bgp_data: community_lists = node_bgp_data.get('tags') prefix_lists = node_bgp_data.get('prefixes') policy_options = { 'community_lists': community_lists, 'prefix_lists': prefix_lists, 'route_maps': route_map_groups, } f_handle = open(os.path.join(zebra_dir(self.network, router), "bgpd.conf"),'wb') #TODO: remove community_lists and prefix_lists as they are put into policy_options f_handle.write(template.render( hostname = router.device_hostname, asn = self.network.asn(router), password = self.zebra_password, enable_password = self.zebra_password, router_id = self.network.lo_ip(router).ip, community_lists = community_lists, policy_options = policy_options, prefix_lists = prefix_lists, #TODO: see how this differs to router_id identifying_loopback = self.network.lo_ip(router), bgp_groups = bgp_groups, ibgp_neighbor_list = ibgp_neighbor_list, ibgp_rr_client_list = ibgp_rr_client_list, route_maps = route_maps, logfile = "/var/log/zebra/bgpd.log", debug=True, use_debug=True, dump=False, snmp=False, interfaces = self.configure_interfaces(router) ))
def configure_igp(self): """Generates IGP specific configuration files (eg ospfd)""" LOG.debug("Configuring IGP") template = lookup.get_template("quagga/ospf.mako") default_weight = 1 # configures IGP for each AS as_graphs = ank.get_as_graphs(self.network) for my_as in as_graphs: asn = my_as.asn LOG.debug("Configuring IGP for AS %s " % asn) if my_as.number_of_edges() == 0: # No edges, nothing to configure LOG.debug("Skipping IGP for AS%s as no internal links" % asn) continue for router in self.network.routers(asn): #TODO: can probably through most of these straight into the template and use properties there! interface_list = [] network_list = [] # Add loopback info lo_ip = router.lo_ip interface_list.append ( {'id': "lo", 'weight': 1, 'remote_router': "NA (loopback)", 'remote_int': "Loopback"}) network_list.append ( { 'cidr': lo_ip.cidr, 'ip': lo_ip.ip, 'netmask': lo_ip.netmask, 'area': 0, 'remote_ip': "Loopback" }) for link in self.network.links(router, my_as): int_id = self.interface_id(link.id) weight = link.weight or default_weight interface_list.append ({ 'id': int_id, 'weight': weight, 'remote_router': link.remote_host, } ) # fetch and format the ip details subnet = link.subnet local_ip = link.local_ip remote_ip = link.remote_ip network_list.append ( { 'cidr': subnet.cidr, 'ip': local_ip, 'netmask': subnet.netmask, 'remote_ip': remote_ip, 'area': 0, } ) #TODO: see if need to use router-id for ospfd in quagga f_handle = open( os.path.join(zebra_dir(self.network, router), "ospfd.conf"), 'wb') f_handle.write(template.render ( hostname = router.device_hostname, password = self.zebra_password, enable_password = self.zebra_password, interface_list = interface_list, network_list = network_list, routerID = router, use_igp = True, logfile = "/var/log/zebra/ospfd.log", use_debug = False, ))
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
def configure_ibgp_rr(network): """Configures route-reflection properties based on work in (NEED CITE). Note: this currently needs ibgp_level to be set globally for route-reflection to work. Future work will implement on a per-AS basis. """ LOG.debug("Configuring iBGP route reflectors") # Add all nodes from physical graph # TODO: if no network.g_session.add_nodes_from(network.graph) def level(u): return int(network.graph.node[u]["ibgp_level"]) 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 default_ibgp_level = 1 # TODO: make "asn" eg "asn_1" as could conflict if asn=1 and ibgp_l2_cluster = 1 elsewhere and match the same for my_as in ank.get_as_graphs(network): # TODO: for neatness, look at redefining the above functions inside here setting my_as as network asn = my_as.name nodes_without_level_set = [n for n in my_as if not network.graph.node[n].get("ibgp_level")] if len(nodes_without_level_set): LOG.debug( "Setting default ibgp_level of %s for nodes %s" % (default_ibgp_level, ", ".join(str(n) for n in nodes_without_level_set)) ) for node in nodes_without_level_set: network.graph.node[node]["ibgp_level"] = default_ibgp_level max_ibgp_level = max(level(n) for n in my_as) LOG.debug("Max ibgp level for %s is %s" % (my_as.asn, max_ibgp_level)) if max_ibgp_level >= 2: for node, data in my_as.nodes(data=True): if not data.get("ibgp_l2_cluster"): # due to boolean evaluation will set in order from left to right network.graph.node[node]["ibgp_l2_cluster"] = data.get("pop") or format_asn(asn) if max_ibgp_level == 3 and not data.get("ibgp_l3_cluster"): # due to boolean evaluation will set in order from left to right network.graph.node[node]["ibgp_l3_cluster"] = format_asn(asn) # Now connect edges_to_add = [] # List of edges for easier iteration (rather than doing each time) as_edges = [(s, t) for s in my_as for t in my_as if s != t] if max_ibgp_level > 1: same_l2_cluster_edges = [ (s, t) for (s, t) in as_edges if network.graph.node[s]["ibgp_l2_cluster"] == network.graph.node[t]["ibgp_l2_cluster"] ] if max_ibgp_level > 2: same_l3_cluster_edges = [ (s, t) for (s, t) in as_edges if network.graph.node[s]["ibgp_l3_cluster"] == network.graph.node[t]["ibgp_l3_cluster"] ] if max_ibgp_level == 1: # 1 asn None edges_to_add += [(s, t, "peer") for (s, t) in as_edges] else: edges_to_add += [(s, t, "up") for (s, t) in same_l2_cluster_edges if level(s) == 1 and level(t) == 2] edges_to_add += [(s, t, "down") for (s, t) in same_l2_cluster_edges if level(s) == 2 and level(t) == 1] if max_ibgp_level == 2: edges_to_add += [(s, t, "peer") for (s, t) in as_edges if level(s) == level(t) == 2] elif max_ibgp_level == 3: edges_to_add += [(s, t, "peer") for (s, t) in same_l2_cluster_edges if level(s) == level(t) == 2] edges_to_add += [(s, t, "up") for (s, t) in same_l3_cluster_edges if level(s) == 2 and level(t) == 3] edges_to_add += [(s, t, "down") for (s, t) in same_l3_cluster_edges if level(s) == 3 and level(t) == 2] edges_to_add += [(s, t, "peer") for (s, t) in same_l3_cluster_edges if level(s) == level(t) == 3] # format into networkx format edges_to_add = [(s, t, {"rr_dir": rr_dir}) for (s, t, rr_dir) in edges_to_add] # LOG.debug("iBGP edges %s" % pprint.pformat(edges_to_add)) LOG.debug("Adding iBGP edges") network.g_session.add_edges_from(edges_to_add) LOG.debug("Added iBGP edges") for node, data in network.graph.nodes(data=True): # is route_reflector if level > 1 network.graph.node[node]["route_reflector"] = int(data.get("ibgp_level")) > 1
def summarydoc(network): """ Plot the network """ ank_main_dir = config.ank_main_dir html_template = lookup.get_template("autonetkit/summary_html.mako") ank_css_template = lookup.get_template("autonetkit/style_css.mako") ebgp_graph = ank.get_ebgp_graph(network) ibgp_graph = ank.get_ibgp_graph(network) # Network wide stats network_stats = {} network_stats['device_count'] = len(list(network.devices())) network_stats['router_count'] = len(list(network.routers())) network_stats['server_count'] = len(list(network.servers())) network_stats['edge_count'] = network.graph.number_of_edges() as_graphs = ank.get_as_graphs(network) network_stats['as_count'] = len(as_graphs) as_stats = {} for my_as in as_graphs: #print single_as.nodes(data=True) # Get ASN of first node asn = my_as.asn #print asn node_list = {} loopbacks = [] virtual_nodes = {} for router in network.routers(asn): node_label = network.fqdn(router) loopbacks.append( (node_label, network.lo_ip(router).ip)) node_list[node_label] = {} interface_list = [] ibgp_list = [] ebgp_list = [] for _, dst, data in network.graph.edges(router, data=True): interface_list.append( (ank.fqdn(network, dst), data['sn'])) node_list[node_label]['interface_list'] = interface_list for _, dst, data in ebgp_graph.edges(router, data=True): ebgp_list.append( (ank.fqdn(network, dst), network.lo_ip(dst).ip)) node_list[node_label]['ebgp_list'] = ebgp_list for _, dst, data in ibgp_graph.edges(router, data=True): ibgp_list.append( (ank.fqdn(network, dst), network.lo_ip(dst).ip)) node_list[node_label]['ibgp_list'] = ibgp_list for virtual_node in network.virtual_nodes(asn): links = [] for link in network.links(virtual_node): links.append( (link.local_ip, link.remote_ip, link.dst)) virtual_nodes[virtual_node] = { 'links': links, } as_stats[my_as.name] = { 'asn': asn, 'loopbacks': loopbacks, 'node_list': node_list, 'virtual_nodes': virtual_nodes, } plot_dir = config.plot_dir if not os.path.isdir(plot_dir): os.mkdir(plot_dir) timestamp = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime()) css_filename = os.path.join(plot_dir, "ank_style.css") with open( css_filename, 'w') as f_css: f_css.write( ank_css_template.render()) # put html file in main plot directory html_filename = os.path.join(plot_dir, "summary.html") #print html_filename with open( html_filename, 'w') as f_html: f_html.write( html_template.render( network_stats = network_stats, as_stats = as_stats, timestamp=timestamp, css_filename = "./ank_style.css", ) )