Skip to content

Commit c8159ac

Browse files
wrideout-aristaabhishek-nexthop
authored andcommitted
Converge cEOSLab peer containers via VRFs (sonic-net#22171)
* Converge cEOSLab peer containers via VRFs Converging the total number of peer switches into the fewest possible number of cEOSLab containers reduces the overall resource constraints required to run large numbers of peers. The basic premises behind convergence are as follows: Approach: cEOSLab peers in docker containers may be converged into a smaller number of host peers. The SONiC-facing configuration of each BGP peer may be separated in routing and bridging via the use of VRFs. The PTF-facing configuration of each BGP peer may be separated within each VRF via VLAN tagging, enabling the use of a single backplane interface on each host cEOSLab container. Each VRF includes a number of interfaces either facing the SONiC DUT or the backplane. Changes should be as transparent to the SONiC DUT as possible. At the time of testbed setup, the ansible topology file for the testbed is modified to include new metadata specific to multi-vrf configuration, and the VMs list is trimmed to only include those containers which will host multiple BGP peerings, separated by VRF. The new metadata includes mappings between host containers and VRFs, backplane VLAN mappings, and BGP session parameters. VLAN tag 2000 is used as the starting value for all VLANs between the test infrastructure PTF container interfaces and cEOSLab device interfaces. The IP and IPv6 addresses used to connect the cEOSLab peer and infrastructure PTF container are generated in order to make the backplane connections clearer, more unique, and easier to implement. In general, backplane L3 addresses used by the CEOSLab peer end in even numbers, and those used by the PTF container end in odd numbers. All addresses generated for use in backplane connections start with the value 100 (0x64) in the least-significant octet or hextet (depending on the family of the address). The address changes are mapped and stored in the new multi-vrf metadata in the ansible topology file. Multiple BGP features, such as local-as and next-hop-peer, are used in order to aid in the resolution of routes. This is necessary to keep the SONiC DUT multi-vrf-agnostic as possible. Enabling multi-VRF mode: Multi-VRF mode may be enabled by including the set attribute use_converged_peers: true in the testbed definition found in sonic-mgmt/ansible/testbed.yaml. This file is read the TesbedProcessing.py script, which sets global variables indicating to other ansible tasks and libraries that the testbed is to be started in multi-VRF mode. In addition, the value of max_fp_nums must be adjusted such that each CEOSLab docker container has enough resources to run all the new BGP sessions in each vrf. This can be done dynamically, of course, however for the full-scale topologies the maximum supported by cEOSLab, 127, must be used. Known limitations: cEOSLab instances do not allow for the creation of interfaces with interface-IDs greater than 127, when interfaces are layed out unidimensionally. The use of multiple VRFs has not been tested in conjunction with asynchronous ansible tasks. Test library changes: Test libraries needed to be made aware of the new underlying structure of cEOSLab containers, VRFs, and BGP adjacencies. In many cases this was done by reference to the testbed topology YAML passed into library functions. In other cases, most notably BGP libraries, the nbrhosts fixture was adjusted to include multi-VRF-specific metadata which callers could leverage to navigate the relationship between containers, VRFs, and BGP neighborship. Signed-off-by: Will Rideout <wrideout@arista.com> Signed-off-by: Abhishek <abhishek@nexthop.ai>
1 parent 1cf8b72 commit c8159ac

34 files changed

Lines changed: 1756 additions & 364 deletions

ansible/TestbedProcessing.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from collections import OrderedDict
99
import copy
1010

11+
from ceos_topo_converger import converge_testbed
12+
1113
""""
1214
Testbed Processing
1315
@@ -297,7 +299,8 @@ def fill_missing_fields(data, template):
297299

298300

299301
def makeTestbed(data, outfile):
300-
csv_columns = "# conf-name,group-name,topo,ptf_image_name,ptf,ptf_ip,ptf_ipv6,server,vm_base,dut,comment"
302+
csv_columns = ("# conf-name,group-name,topo,ptf_image_name,ptf,ptf_ip,ptf_ipv6,server,vm_base,dut,comment,"
303+
"use_converged_peers")
301304
topology = data
302305
csv_file = outfile
303306

@@ -316,6 +319,7 @@ def makeTestbed(data, outfile):
316319
dut = groupDetails.get("dut")
317320
ptf = groupDetails.get("ptf")
318321
comment = groupDetails.get("comment")
322+
use_converged_peers = str(groupDetails.get("use_converged_peers", False)).lower()
319323

320324
# catch empty types
321325
if not groupName:
@@ -346,7 +350,7 @@ def makeTestbed(data, outfile):
346350

347351
row = confName + "," + groupName + "," + topo + "," + ptf_image_name + "," + ptf + \
348352
"," + ptf_ip + "," + ptf_ipv6 + "," + server + \
349-
"," + vm_base + "," + dut + "," + comment
353+
"," + vm_base + "," + dut + "," + comment + "," + use_converged_peers
350354
f.write(row + "\n")
351355
except IOError:
352356
print("I/O error: issue creating testbed.yaml")
@@ -1093,6 +1097,19 @@ def main():
10931097
print("\tCREATING TEST BED: " + args.basedir + testbed_file)
10941098
# Generate testbed.yaml (TESTBED)
10951099
makeTestbed(testbed, args.basedir + testbed_file)
1100+
# If specified the testbed, overwrite the topology file the testbed will use with
1101+
# one which uses the fewest number of ceoslab peers possible.
1102+
for data in testbed.values():
1103+
topo = data.get("topo", "")
1104+
if topo and data.get("use_converged_peers", False):
1105+
topofile = os.path.join("vars", "topo_{}.yml".format(topo))
1106+
if os.path.exists(os.path.join(args.basedir, topofile)):
1107+
print("\tCONVERGING PEER INSTANCES: {}".format(topofile))
1108+
copyfile(topofile,
1109+
os.path.join("/tmp/", "topo_{}.yml.orig".format(topo)))
1110+
converge_testbed(topofile, topofile) # overwrites contents of topofile
1111+
else:
1112+
print("Error: could not locate original topo file at " + topofile)
10961113
print("\tCREATING VM_HOST/CREDS: " + args.basedir + vmHostCreds_file)
10971114
# Generate vm_host\creds.yml (CREDS)
10981115
makeVMHostCreds(veos, args.basedir + vmHostCreds_file)

ansible/ceos_topo_converger.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
#!/usr/bin/env python3
2+
3+
'''Converts SONiC topologies to use fewer cEOSLab peers, based on the roles required
4+
in the topology
5+
'''
6+
7+
from copy import deepcopy
8+
from ipaddress import ip_address
9+
from typing import Dict, List, Union
10+
import yaml
11+
12+
CEOSLAB_INTF_LIMIT = 127 # 128, minus one for backplane interface
13+
BASE_VLAN_ID = 2000
14+
15+
16+
class ListIndentDumper(yaml.Dumper):
17+
def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
18+
return super().increase_indent(flow, False)
19+
20+
21+
class SonicTopoConverger:
22+
23+
def __init__(self, topology: Dict[str, Union[int, str]], file_out: str) -> None:
24+
self.topo = topology
25+
self.converged_topo = {
26+
"topology": {},
27+
"configuration_properties": {},
28+
"configuration": {}}
29+
self.file_out = file_out
30+
self.prime_device_mapping = {}
31+
self.prime_devices = []
32+
33+
def parse_properties(self) -> None:
34+
'''
35+
The base configuration items of the topology must be parsed first, as they
36+
inform how other sections will be translated. Most important of these items
37+
is the roles that each cEOSLab docker peer may fulfill. At minimum we need
38+
one instance per role. These instances are referred to as "prime" instances,
39+
and will contain the converged configuration of all other instances in the
40+
input topology.
41+
'''
42+
labels = []
43+
roles_by_label = {}
44+
config_properties = self.topo["configuration_properties"]
45+
for label in config_properties:
46+
if "swrole" in config_properties[label]:
47+
labels.append(label)
48+
roles_by_label[label] = config_properties[label]["swrole"]
49+
50+
# Select a prime device for each role (defined above) and unique BGP ASN
51+
# combination. The entire peer topology will be reduced into these devices.
52+
#
53+
# cEOSLab peers only support 128 interfaces (with LLDP running). Each
54+
# pre-converged peer in the topology will become a single BGP instance
55+
# running inside a VRF on a primary peer device. Each VRF will require an
56+
# downstream link to PTF/exabgp instance/other test infrastructure, and an
57+
# upstream link the DUT. This is achieved by creating a backplane interface
58+
# on each primary peer that is a trunk interface, with each VRF
59+
# containing a backplane SVI. So, if the pre-converged peer topology
60+
# requires more than 127 peers in a single BGP AS, we distribute them across
61+
# multiple primary peers.
62+
config = self.topo["configuration"]
63+
cur_prime_devs = {}
64+
dev_count = 0
65+
for device in config:
66+
create_new_prime = False
67+
device_properties = config[device]["properties"]
68+
69+
dev_count += 1
70+
71+
if dev_count == CEOSLAB_INTF_LIMIT:
72+
create_new_prime = True
73+
dev_count = 0
74+
75+
for label in device_properties:
76+
if label in labels:
77+
if label not in cur_prime_devs or create_new_prime:
78+
cur_prime_devs[label] = device
79+
self.prime_devices.append(device)
80+
prime = cur_prime_devs[label]
81+
if prime not in self.prime_device_mapping:
82+
self.prime_device_mapping[prime] = []
83+
self.prime_device_mapping[prime].append(device)
84+
85+
def converge_vms(self) -> Dict[str, Union[int, str]]:
86+
'''
87+
Helper to converge the "VMs" section of the input topology, where vlans and
88+
offsets are defined, per cEOSLab instance.
89+
'''
90+
prime_rev_map = {}
91+
for key, names in self.prime_device_mapping.items():
92+
for name in names:
93+
prime_rev_map[name] = key
94+
95+
old_vms = self.topo["topology"]["VMs"]
96+
vms = {}
97+
for i, dev in enumerate(self.prime_devices):
98+
vms[dev] = {"vlans": [], "vm_offset": i}
99+
100+
for vm_name, vm in old_vms.items():
101+
prime = prime_rev_map[vm_name]
102+
for vlan in vm["vlans"]:
103+
vms[prime]["vlans"].append(vlan)
104+
105+
return vms
106+
107+
def modify_l3_address(self, address: str, offset: int) -> str:
108+
delim = ":" if ":" in address else "."
109+
octets = address.split(delim)
110+
addr = octets[:-1] + ["0"]
111+
addr = ip_address(delim.join(addr))
112+
return str(addr + offset)
113+
114+
def converge_peers(self,
115+
if_index_mapping: Dict[str, List[int]],
116+
offset_mapping: Dict[str, int]) -> Dict[str, Union[int, str]]:
117+
'''
118+
Helper to converge the section of the input topology where the actual cEOSLab
119+
instance configuration is laid out. This is where interface and BGP
120+
configuration is translated.
121+
'''
122+
peers = self.topo["configuration"]
123+
convergence_data = {}
124+
new_peers = {}
125+
bp_addrs = {}
126+
for dev in self.prime_devices:
127+
properties = deepcopy(peers[dev]["properties"])
128+
asn = peers[dev]["bgp"]["asn"]
129+
new_peers[dev] = {"properties": properties,
130+
"vrf": {},
131+
"bgp": {"asn": asn},
132+
"intf_mapping": {}}
133+
134+
# Backplane L3 addresses are laid out for clarity-- addresses with odd
135+
# least-signifcant octets or hextets are assigned to the interfaces of the
136+
# PTF container, and those with even least-signifcant octets or hextets are
137+
# assigned to the cEOSLab containers. The addresses alternate. We start at
138+
# 100 as the IPv4 addresses used for backplane connections in most of the
139+
# testbed topology files used with this conversion script lie in that range.
140+
# We use a similar range for IPv6 for simplicity.
141+
peer_bp_addr_offset = 100
142+
ptf_bp_addr_offset = 101
143+
base_v4_addr = "10.10.246.0"
144+
base_v6_addr = "fc0a::"
145+
for prime_dev, peer_list in self.prime_device_mapping.items():
146+
intf_counter_base = 1
147+
eth_intf_index = 1
148+
offset = 0
149+
for i, peer_name in enumerate(peer_list):
150+
# For simplicity, VRFs are just peer names.
151+
vlan_id = BASE_VLAN_ID + offset_mapping[peer_name]
152+
peer = peers[peer_name]
153+
vrf_name = peer_name
154+
peer_intfs = peer["interfaces"]
155+
orig_intf_map = {}
156+
157+
intf_index = i + intf_counter_base
158+
vrf = {f"Vlan{vlan_id}": {}}
159+
160+
for intf, config in peer_intfs.items():
161+
if "Ethernet" not in intf:
162+
continue
163+
eth_intf = f"Ethernet{eth_intf_index}"
164+
vrf[eth_intf] = deepcopy(peer_intfs[intf])
165+
orig_intf_map[intf] = eth_intf
166+
eth_intf_index += 1
167+
168+
if "Port-Channel1" in peer_intfs:
169+
po_intf = f"Port-Channel{intf_index}"
170+
orig_intf_map["Port-Channel1"] = po_intf
171+
vrf[po_intf] = deepcopy(
172+
peer_intfs["Port-Channel1"])
173+
if "Loopback0" in peer_intfs:
174+
lo_intf = f"Loopback{intf_index}"
175+
orig_intf_map["Loopback0"] = lo_intf
176+
vrf[lo_intf] = deepcopy(peer_intfs["Loopback0"])
177+
178+
new_peers[prime_dev]["vrf"][vrf_name] = vrf
179+
180+
bp_addr_data = {}
181+
if "ipv4" in peer["bp_interface"]:
182+
bp_addr_data["ipv4"] = f"{self.modify_l3_address(base_v4_addr, ptf_bp_addr_offset)}/31"
183+
vrf[f"Vlan{vlan_id}"]["ipv4"] = f"{self.modify_l3_address(base_v4_addr, peer_bp_addr_offset)}/31"
184+
if "ipv6" in peer["bp_interface"]:
185+
bp_addr_data["ipv6"] = f"{self.modify_l3_address(base_v6_addr, ptf_bp_addr_offset)}/127"
186+
bp_addr_data["router-id"] = f"{self.modify_l3_address(base_v4_addr, ptf_bp_addr_offset)}"
187+
vrf[f"Vlan{vlan_id}"]["ipv6"] = f"{self.modify_l3_address(base_v6_addr, peer_bp_addr_offset)}/127"
188+
if bp_addr_data:
189+
bp_addr_data["vlan"] = vlan_id
190+
bp_addrs[peer_name] = bp_addr_data
191+
192+
if not new_peers[prime_dev]["intf_mapping"]:
193+
# If we are filling in a prime_dev for the first time, reset the
194+
# offset
195+
offset = 0
196+
new_peers[prime_dev]["intf_mapping"][vrf_name] = {"offset": offset, "orig_intf_map": orig_intf_map}
197+
offset += 1
198+
peer_bp_addr_offset += 2
199+
ptf_bp_addr_offset += 2
200+
201+
convergence_data["converged_peers"] = new_peers
202+
convergence_data["convergence_mapping"] = deepcopy(self.prime_device_mapping)
203+
convergence_data["interface_index_mapping"] = if_index_mapping
204+
convergence_data["vm_offset_mapping"] = offset_mapping
205+
if bp_addrs:
206+
convergence_data["ptf_backplane_addrs"] = bp_addrs
207+
return convergence_data
208+
209+
def converge_topo(self) -> None:
210+
'''
211+
Converge the read DUT/cEOSLab topology into the fewest cEOSLab docker
212+
instances as possible. The number of containers is based on the roles
213+
required by the topology
214+
215+
i.e. a topology with the "tor" and "spine" roles defined with be converged to
216+
use two cEOSLab docker instances, one per role.
217+
'''
218+
new_topo = self.converged_topo["topology"]
219+
old_topo = self.topo["topology"]
220+
221+
self.converged_topo["topo_is_multi_vrf"] = True
222+
223+
# We don't need to change the host_interfaces portion of the passed topo, so
224+
# copy
225+
# it over as is.
226+
key = "host_interfaces"
227+
if key in old_topo:
228+
new_topo[key] = old_topo[key].copy()
229+
230+
key = "VMs"
231+
# Save off which vm had which interface index as we will need this later
232+
interface_indexes = {}
233+
offsets = {}
234+
for vm, data in self.topo["topology"]["VMs"].items():
235+
interface_indexes[vm] = data["vlans"]
236+
offsets[vm] = data["vm_offset"]
237+
vms = self.converge_vms()
238+
new_topo[key] = vms
239+
240+
# The DUT configuration and general configuration properties should be
241+
# unchanged as well.
242+
key = "DUT"
243+
if key in old_topo:
244+
new_topo[key] = old_topo[key].copy()
245+
246+
new_topo = self.converged_topo
247+
old_topo = self.topo
248+
key = "configuration_properties"
249+
new_topo[key] = old_topo[key].copy()
250+
251+
# convergence metadata
252+
key = "configuration"
253+
new_topo[key] = old_topo[key].copy()
254+
new_topo["convergence_data"] = self.converge_peers(interface_indexes, offsets)
255+
256+
def run(self) -> None:
257+
self.parse_properties()
258+
self.converge_topo()
259+
with open(self.file_out, "w", encoding="utf-8") as out_file:
260+
yaml.dump(self.converged_topo, out_file,
261+
Dumper=ListIndentDumper, sort_keys=False)
262+
263+
264+
def converge_testbed(input_file: str, output_file: str) -> None:
265+
with open(input_file, "r", encoding="utf-8") as in_file:
266+
topo = yaml.safe_load(in_file)
267+
converger = SonicTopoConverger(topo, output_file)
268+
converger.run()

0 commit comments

Comments
 (0)