1+ import yaml
2+ import argparse
3+ import sys
4+ import os
5+
6+ def save_yaml (data , filename ):
7+ """Save data as YAML to a file"""
8+ with open (filename , "w" ) as f :
9+ yaml .dump (data , f , sort_keys = False , default_flow_style = False )
10+
11+ def load_config (config_file ):
12+ try :
13+ with open (config_file , 'r' ) as f :
14+ return yaml .safe_load (f )
15+ except FileNotFoundError :
16+ print (f"Error: Configuration file '{ config_file } ' not found." )
17+ sys .exit (1 )
18+ except yaml .YAMLError as e :
19+ print (f"Error: Invalid YAML in configuration file: { e } " )
20+ sys .exit (1 )
21+
22+ def add_bidirectional_link (config , from_node , to_node , link_counter , preset = "default" ):
23+ config ["links" ][f"link{ link_counter } " ] = {
24+ "from" : from_node , "to" : to_node , "preset-name" : preset
25+ }
26+ link_counter += 1
27+ config ["links" ][f"link{ link_counter } " ] = {
28+ "from" : to_node , "to" : from_node , "preset-name" : preset
29+ }
30+ return link_counter + 1
31+
32+ def generate_fat_tree_config (config_params ):
33+ switch_ports_count = config_params ["switch_ports_count" ]
34+ link_presets = config_params ["link_presets" ]
35+ switch_presets = config_params ["switch_presets" ]
36+ packet_spraying = config_params ["packet_spraying" ]
37+
38+ if switch_ports_count % 2 != 0 or switch_ports_count < 2 :
39+ raise ValueError (f"Switch's number of ports must be an even integer >= 2, but got { switch_ports_count } " )
40+
41+ num_pods = switch_ports_count
42+ edge_per_pod = switch_ports_count // 2
43+ aggr_per_pod = switch_ports_count // 2
44+ hosts_per_pod = edge_per_pod * (switch_ports_count // 2 )
45+ core_switches = (switch_ports_count // 2 ) ** 2
46+ hosts_per_edge = switch_ports_count // 2
47+ core_per_aggr = switch_ports_count // 2
48+
49+ config = {
50+ "presets" : {
51+ "link" : link_presets ,
52+ "switch" : switch_presets
53+ },
54+ "packet-spraying" : packet_spraying ,
55+ "hosts" : {},
56+ "switches" : {},
57+ "links" : {}
58+ }
59+
60+ host_name = lambda pod_idx , host_idx : f"pod{ pod_idx } _host{ host_idx } "
61+ aggr_name = lambda pod_idx , aggr_idx : f"pod{ pod_idx } _aggr{ aggr_idx } "
62+ edge_name = lambda pod_idx , edge_idx : f"pod{ pod_idx } _edge{ edge_idx } "
63+ core_name = lambda core_idx : f"core{ core_idx } "
64+
65+ for p in range (1 , num_pods + 1 ):
66+ for h in range (1 , hosts_per_pod + 1 ):
67+ config ["hosts" ][host_name (p , h )] = {"layer" : 3 }
68+
69+ for i in range (1 , core_switches + 1 ):
70+ config ["switches" ][core_name (i )] = {"preset-name" : "core" , "layer" : 0 }
71+
72+ for p in range (1 , num_pods + 1 ):
73+ for a in range (1 , aggr_per_pod + 1 ):
74+ config ["switches" ][aggr_name (p , a )] = {"preset-name" : "aggr" , "layer" : 1 }
75+ for e in range (1 , edge_per_pod + 1 ):
76+ config ["switches" ][edge_name (p , e )] = {"preset-name" : "edge" , "layer" : 2 }
77+
78+ link_counter = 1
79+
80+ # Edge-host
81+ for pod_idx in range (1 , num_pods + 1 ):
82+ for edge_idx in range (1 , edge_per_pod + 1 ):
83+ for h in range (1 , hosts_per_edge + 1 ):
84+ host_idx = (edge_idx - 1 ) * hosts_per_edge + h
85+ link_counter = add_bidirectional_link (
86+ config ,
87+ host_name (pod_idx , host_idx ),
88+ edge_name (pod_idx , edge_idx ),
89+ link_counter ,
90+ "edge-host"
91+ )
92+
93+ # Aggr-edge
94+ for pod_idx in range (1 , num_pods + 1 ):
95+ for edge_idx in range (1 , edge_per_pod + 1 ):
96+ for aggr_idx in range (1 , aggr_per_pod + 1 ):
97+ link_counter = add_bidirectional_link (
98+ config ,
99+ edge_name (pod_idx , edge_idx ),
100+ aggr_name (pod_idx , aggr_idx ),
101+ link_counter ,
102+ "aggr-edge"
103+ )
104+
105+ # Aggr-core
106+ for pod_idx in range (1 , num_pods + 1 ):
107+ for aggr_idx in range (1 , aggr_per_pod + 1 ):
108+ core_offset = (aggr_idx - 1 ) * core_per_aggr
109+ for core_idx in range (1 , core_per_aggr + 1 ):
110+ core_id = core_offset + core_idx
111+ link_counter = add_bidirectional_link (
112+ config ,
113+ aggr_name (pod_idx , aggr_idx ),
114+ core_name (core_id ),
115+ link_counter ,
116+ "aggr-core"
117+ )
118+
119+ return config
120+
121+ def write_config_to_file (config , output_path ):
122+ with open (output_path , 'w' ) as f :
123+ yaml .dump (config , f , sort_keys = False , width = 120 , indent = 2 )
124+
125+ print (f"Configuration written to { output_path } " )
126+ return output_path
127+
128+ if __name__ == "__main__" :
129+ # Set up command-line argument parsing
130+ parser = argparse .ArgumentParser (
131+ description = 'Generate Fat-Tree network configuration.' \
132+ 'You may see more about it here: ' \
133+ 'https://packetpushers.net/blog/demystifying-dcn-topologies-clos-fat-trees-part2/' )
134+ curr_file_path = os .path .realpath (__file__ )
135+ curr_dir_path = os .path .dirname (curr_file_path )
136+ default_config_full_path = os .path .join (curr_dir_path , "fat_tree_config.yaml" )
137+ default_config_rel_path = os .path .relpath (default_config_full_path , os .getcwd ())
138+
139+ parser .add_argument ('-c' , '--config' ,
140+ default = default_config_abs_path ,
141+ help = f'Path to configuration file (default: { default_config_abs_path } ). See given default to get format & structure of this config' )
142+
143+ parser .add_argument ('-o' , '--output_path' ,
144+ default = 'fat_tree_topology.yaml' ,
145+ help = 'Path to the output topology config file' ,
146+ )
147+
148+
149+ args = parser .parse_args ()
150+
151+ # Load configuration from file
152+ config_params = load_config (args .config )
153+
154+ # Generate and write the fat-tree configuration
155+ topology = generate_fat_tree_config (config_params )
156+ save_yaml (topology , args .output_path )
0 commit comments