290 lines
12 KiB
Python
Executable File
290 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import networkx as nx
|
|
import ipaddress
|
|
import yaml
|
|
import sys
|
|
|
|
from networkx.drawing.nx_agraph import to_agraph
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
parser = argparse.ArgumentParser(description='ClosX - Clos Network Generator')
|
|
parser.add_argument('-ipr', action="store", dest="ip_range")
|
|
parser.add_argument('-mgmtr', action="store", dest="mgmt_range")
|
|
parser.add_argument('-spine', action="store", dest="spine_size")
|
|
parser.add_argument('-tier2', action="store", dest="tier2_size")
|
|
parser.add_argument('-tier1', action="store", dest="tier1_size")
|
|
parser.add_argument('-vendor', action="store", dest="vendor")
|
|
args = parser.parse_args()
|
|
|
|
class Clos():
|
|
def __init__(self, spine_size, tier2_size, tier1_size, ip_range, mgmt_range, vendor):
|
|
'''
|
|
Summary:
|
|
Takes a router list and builds a vars_dict to pass to the associated Jinja template.
|
|
|
|
Assigns:
|
|
self.G: Networkx graph object
|
|
self.nodes: Dict of lists, keyed by node type, dict example:
|
|
{
|
|
'spine_list': ['s1-r1', 's1-r2', 's1-r3', 's1-r4', 's1-r5', 's1-r6', 's1-r7', 's1-r8'],
|
|
'tier2_list': ['t2-r1', 't2-r2', 't2-r3', 't2-r4', 't2-r5', 't2-r6', 't2-r7', 't2-r8', 't2-r9', 't2-r10', 't2-r11', 't2-r12', 't2-r13', 't2-r14', 't2-r15', 't2-r16'],
|
|
'tier1_list': ['t1-r1', 't1-r2', 't1-r3', 't1-r4', 't1-r5', 't1-r6', 't1-r7', 't1-r8', 't1-r9', 't1-r10', 't1-r11', 't1-r12', 't1-r13', 't1-r14', 't1-r15', 't1-r16']
|
|
}
|
|
self.edges: List of edge tuples, list example:
|
|
[
|
|
('s1-r1', 't2-r1'),
|
|
('s1-r1', 't2-r2'),
|
|
('s1-r1', 't2-r3'),
|
|
('s1-r1', 't2-r4'),
|
|
('s1-r1', 't2-r5'),
|
|
('s1-r1', 't2-r6'),
|
|
...etc
|
|
]
|
|
self.spine_size: Size of spine as str, always converted to int
|
|
self.tier2_size: Size of tier2 as str, always converted to int
|
|
self.tier1_size: Size of tier1 as str, always converted to int
|
|
self.ip_range: IP range to split into /31 subnets as str, always converted to ipnetwork object
|
|
self.mgmt_range: IP range to split into /32 management IPs as str, always converted to ipnetwork object
|
|
self.total_nodes: Sum of spine_size, tier2_size and tier1_size as int
|
|
self.vendor: Vendor template to use for rendering as str
|
|
self.image_name: Name of the networkx graph object visualisation file as str
|
|
'''
|
|
self.G = nx.Graph()
|
|
self.nodes = {}
|
|
self.edges = []
|
|
self.spine_size = spine_size
|
|
self.tier2_size = tier2_size
|
|
self.tier1_size = tier1_size
|
|
self.ip_range = ip_range
|
|
self.mgmt_range = mgmt_range
|
|
self.total_nodes = int(spine_size) + int(tier2_size) + int(tier1_size)
|
|
self.vendor = vendor
|
|
self.image_name = 'clos.png'
|
|
|
|
def build(self):
|
|
'''
|
|
Summary:
|
|
Builds the Clos object.
|
|
Populates self.nodes and self.edges based on user provided args.
|
|
Populates self.G.nodes and self.G.edges with self.nodes and self.edges.
|
|
Nodes also include /32 IPs for management. Edges also include /31 IPs and port indexes.
|
|
Topology is drawn and visualised with pygraphviz.
|
|
Configuration is rendered with YAML and Jinja.
|
|
|
|
Takes:
|
|
self: Clos object
|
|
|
|
Produces:
|
|
Networkx graph object visualisation file.
|
|
Full Clos network configuration.
|
|
'''
|
|
self.nodes['spine_list'] = ['s1-r' + str(node + 1) for node in range(int(self.spine_size))]
|
|
self.nodes['tier2_list'] = ['t2-r' + str(node + 1) for node in range(int(self.tier2_size))]
|
|
self.nodes['tier1_list'] = ['t1-r' + str(node + 1) for node in range(int(self.tier1_size))]
|
|
|
|
mgmt_ips = self.get_mgmt_ips()
|
|
|
|
node_index = 0
|
|
for node_lists, nodes in self.nodes.items():
|
|
for node in nodes:
|
|
self.G.add_node(node)
|
|
self.G.nodes[node]['mgmt_ip'] = str(mgmt_ips[node_index].hosts()[0])
|
|
node_index +=1
|
|
|
|
for spine_router in self.nodes['spine_list']:
|
|
for tier2_router in self.nodes['tier2_list']:
|
|
self.edges.append((spine_router, tier2_router))
|
|
|
|
for tier2_router in self.nodes['tier2_list']:
|
|
for tier1_router in self.nodes['tier1_list']:
|
|
self.edges.append((tier2_router, tier1_router))
|
|
|
|
p2p_ips = self.get_p2p_ips()
|
|
port_indexes = self.get_port_indexes()
|
|
|
|
edge_index = 0
|
|
for edge in self.edges:
|
|
self.G.add_edge(edge[0], edge[1])
|
|
nx.set_edge_attributes(
|
|
self.G,
|
|
{
|
|
(edge[0], edge[1]):
|
|
{'a_end_ip': str(list(p2p_ips[edge_index].hosts())[0]),
|
|
'a_end': edge[0],
|
|
'a_end_index': port_indexes[edge_index]['a_end_index'],
|
|
'b_end_ip': str(list(p2p_ips[edge_index].hosts())[1]),
|
|
'b_end': edge[1],
|
|
'b_end_index': port_indexes[edge_index]['b_end_index']}
|
|
}
|
|
)
|
|
edge_index += 1
|
|
|
|
print('Drawing topology...')
|
|
vis = to_agraph(self.G)
|
|
vis.layout('dot')
|
|
vis.draw(self.image_name)
|
|
print(f'Topology drawn, saved as: {self.image_name}')
|
|
print(f'Rendered {self.total_nodes} Nodes and {len(self.edges)} Edges')
|
|
|
|
self.render_config('spine_list')
|
|
self.render_config('tier2_list')
|
|
self.render_config('tier1_list')
|
|
|
|
def get_p2p_ips(self):
|
|
'''
|
|
Summary:
|
|
Takes an IP range and splits it into /31 subnets.
|
|
Exits if the number of edges exceeds the number of /31 subnets, prompts user to provide a bigger IP range.
|
|
|
|
Takes:
|
|
self: Clos object
|
|
|
|
Returns:
|
|
p2p_ips: List of /31 subnets
|
|
'''
|
|
ips = ipaddress.ip_network(self.ip_range)
|
|
p2p_ips = list(ips.subnets(new_prefix=31))
|
|
|
|
if len(self.edges) > len(p2p_ips):
|
|
sys.exit(f'Insufficient IP space for allocating required amount of /31s. Edges: {len(self.edges)}, /31s: {len(p2p_ips)}, provide a bigger IP range')
|
|
|
|
return p2p_ips
|
|
|
|
def get_mgmt_ips(self):
|
|
'''
|
|
Summary:
|
|
Takes an IP range and splits it into /32 management IPs.
|
|
Exits if the number of nodes exceeds the number of /32 management IPs, prompts user to provide a bigger IP range.
|
|
|
|
Takes:
|
|
self: Clos object
|
|
|
|
Returns:
|
|
mgmt_ips: List of /32 management IPs
|
|
'''
|
|
ips = ipaddress.ip_network(self.mgmt_range)
|
|
mgmt_ips = list(ips.subnets(new_prefix=32))
|
|
|
|
if self.total_nodes > len(mgmt_ips):
|
|
sys.exit(f'Insufficient IP space for allocating required amount of /32s. Nodes: {len(self.total_nodes)}, /32s: {len(mgmt_ips)}, provide a bigger mgmt IP range')
|
|
|
|
return mgmt_ips
|
|
|
|
def render_config(self, router_list):
|
|
'''
|
|
Summary:
|
|
Takes a list of router names and builds a vars_dict dict for each router in it to pass to the associated Jinja template for rendering.
|
|
Iterates edges in the networkx graph object to extract edge attributes and add them into vars_dict.
|
|
Template to render against is matched to self.vendor, provided as an arg at runtime.
|
|
Configs are written to the configs/ folder named as hostname.conf.
|
|
|
|
vars_dict contains:
|
|
- hostname
|
|
- mgmt_ip
|
|
- interfaces: Dict of dicts, keyed by index, containing:
|
|
- a_end_ip
|
|
- a_end (hostname)
|
|
- a_end_index
|
|
- b_end_ip
|
|
- b_end (hostname)
|
|
- b_end_index
|
|
|
|
vars_dict dict example:
|
|
{
|
|
'hostname': 's1-r1',
|
|
'mgmt_ip': '172.16.0.0',
|
|
'interfaces': {
|
|
0: {'a_end_ip': '10.0.0.0', 'a_end': 's1-r1', 'a_end_index': 24, 'b_end_ip': '10.0.0.1', 'b_end': 't2-r1', 'b_end_index': 0},
|
|
1: {'a_end_ip': '10.0.0.2', 'a_end': 's1-r1', 'a_end_index': 25, 'b_end_ip': '10.0.0.3', 'b_end': 't2-r2', 'b_end_index': 0},
|
|
2: {'a_end_ip': '10.0.0.4', 'a_end': 's1-r1', 'a_end_index': 26, 'b_end_ip': '10.0.0.5', 'b_end': 't2-r3', 'b_end_index': 0},
|
|
3: {'a_end_ip': '10.0.0.6', 'a_end': 's1-r1', 'a_end_index': 27, 'b_end_ip': '10.0.0.7', 'b_end': 't2-r4', 'b_end_index': 0},
|
|
...etc
|
|
}
|
|
}
|
|
|
|
Takes:
|
|
self: Clos object
|
|
router_list: Name of router list to render configuration for
|
|
'''
|
|
for router in self.nodes[router_list]:
|
|
vars_dict = {}
|
|
vars_dict.update({'hostname': router, 'interfaces': {}})
|
|
|
|
for node in self.G.nodes(data=True):
|
|
if router == node[0]:
|
|
vars_dict.update({'mgmt_ip': node[1]['mgmt_ip']})
|
|
|
|
edge_index = 0
|
|
for edge in self.G.edges(router, data=True):
|
|
vars_dict['interfaces'].update({edge_index: edge[2]})
|
|
edge_index += 1
|
|
|
|
template_path = Environment(loader=FileSystemLoader('templates/'), trim_blocks=True, lstrip_blocks=True)
|
|
template = template_path.get_template(self.vendor + '.j2')
|
|
rendered_config = template.render(vars_dict)
|
|
|
|
config_path = 'configs/' + self.vendor + '/' + router + '.conf'
|
|
|
|
with open(config_path, 'w') as configuration:
|
|
configuration.write(rendered_config)
|
|
|
|
print(f'Rendered {config_path}')
|
|
|
|
def get_port_indexes(self):
|
|
'''
|
|
Summary:
|
|
Calculates port indexes for both sides of edges in the networkx graph object. The lowest available index is always used.
|
|
Once used, port indexes are removed from the free_port_indexes uplinks or downlinks list.
|
|
Design principles of a Clos network define that port radix should be equal north and south.
|
|
Max amount of uplink or downlink ports is 24.
|
|
Exits if the number of port indexes required exceed the max, prompts user to reduce spine, tier2 or tier1 size.
|
|
|
|
free_port_indexes dict example:
|
|
{
|
|
's1-r1': {
|
|
'uplinks': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
|
|
'downlinks': [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]
|
|
}
|
|
}
|
|
|
|
Takes:
|
|
self: Clos object
|
|
|
|
Returns:
|
|
allocated_port_indexes: Dict of dicts, keyed by edge index, containing assigned a and b end indexes.
|
|
|
|
allocated_port_indexes dict example:
|
|
{
|
|
0: {'a_end_index': 24, 'b_end_index': 0},
|
|
1: {'a_end_index': 25, 'b_end_index': 0},
|
|
2: {'a_end_index': 26, 'b_end_index': 0},
|
|
3: {'a_end_index': 27, 'b_end_index': 0},
|
|
...etc
|
|
}
|
|
'''
|
|
if int(self.spine_size) > 24 or int(self.tier2_size) > 24 or int(self.tier1_size) > 24:
|
|
sys.exit('Amount of required port indexes exceeds free port indexes, decrease the size of spine/tier2/tier1 if they exceed 24')
|
|
|
|
allocated_port_indexes = {}
|
|
free_port_indexes = {}
|
|
|
|
for node in self.G.nodes:
|
|
free_port_indexes.update({node: {'uplinks': [i for i in range(0, 24)], 'downlinks': [i for i in range(24, 48)]}})
|
|
|
|
edge_index = 0
|
|
for edge in self.edges:
|
|
lowest_available_downlink = min(free_port_indexes[edge[0]]['downlinks'])
|
|
lowest_available_uplink = min(free_port_indexes[edge[1]]['uplinks'])
|
|
allocated_port_indexes.update({edge_index: {'a_end_index': lowest_available_downlink, 'b_end_index': lowest_available_uplink}})
|
|
free_port_indexes[edge[0]]['downlinks'].remove(lowest_available_downlink)
|
|
free_port_indexes[edge[1]]['uplinks'].remove(lowest_available_uplink)
|
|
edge_index += 1
|
|
|
|
return allocated_port_indexes
|
|
|
|
if __name__ == "__main__":
|
|
Clos = Clos(args.spine_size, args.tier2_size, args.tier1_size, args.ip_range, args.mgmt_range, args.vendor)
|
|
Clos.build()
|