Skip to content

Commit 001eaa0

Browse files
w1ndar12f
authored andcommitted
[testbed-cli] Code change on add-topo and deploy-minigraph for deploying testbed with peers on multiple servers (sonic-net#15643)
In PR sonic-net#15547, we define server index in topology file which make topology file and testbed yaml file is coupled. To decouple them, in new design sonic-net#15395, we remove server index in topology file and add dut_interfaces in testbed yaml, so there is no more change on topology file schema. What is the motivation for this PR? Decouple topology file and testbed file in design. How did you do it? Record all information in testbed schema How did you verify/test it? Deploy testbed
1 parent ffaa779 commit 001eaa0

19 files changed

+300
-41
lines changed

ansible/config_sonic_basedon_testbed.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171

7272
- name: set vm
7373
set_fact:
74-
vm_base: "{% if testbed_facts['vm_base'] != '' %}{{ testbed_facts['vm_base'] }}{% else %}''{% endif %}"
74+
vm_base: "{% if 'vm_base' in testbed_facts and testbed_facts['vm_base'] != '' %}{{ testbed_facts['vm_base'] }}{% else %}''{% endif %}"
7575
when: testbed_name is defined
7676

7777
- name: find supervisor dut of testbed
@@ -222,7 +222,7 @@
222222
remote_dut: "{{ ansible_ssh_host }}"
223223

224224
- name: gather testbed VM information
225-
testbed_vm_info: base_vm={{ testbed_facts['vm_base'] }} topo={{ testbed_facts['topo'] }} vm_file={{ vm_file }}
225+
testbed_vm_info: base_vm={{ testbed_facts['vm_base'] if 'vm_base' in testbed_facts else "" }} topo={{ testbed_facts['topo'] }} vm_file={{ vm_file }} servers_info={{ testbed_facts['servers'] | default({}) }}
226226
delegate_to: localhost
227227
when: "(VM_topo | bool) and ('cable' not in topo)"
228228

@@ -433,7 +433,7 @@
433433

434434
- name: Update TACACS server address to PTF IP
435435
set_fact:
436-
tacacs_servers: ["{{ testbed_facts['ptf_ip'] }}"]
436+
tacacs_servers: ["{{ testbed_facts['multi_servers_tacacs_ip'] if 'multi_servers_tacacs_ip' in testbed_facts else testbed_facts['ptf_ip'] }}"]
437437
when: use_ptf_tacacs_server is defined and use_ptf_tacacs_server|bool == true
438438

439439
- debug: msg="tacacs_servers {{ tacacs_servers }}"

ansible/fanout_connect.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- set_fact:
1212
server: "{{ inventory_hostname|lower }}"
1313
server_port: "{{ external_port }}"
14+
clean_before_add: "{{ clean_before_add | default('y') }}"
1415

1516
- set_fact: root_fanout_connect=true
1617
when: root_fanout_connect is not defined

ansible/library/announce_routes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from multiprocessing.pool import ThreadPool
1616
from ansible.module_utils.basic import AnsibleModule
1717
from ansible.module_utils.debug_utils import config_module_logging
18+
from ansible.module_utils.multi_servers_utils import MultiServersUtils
1819

1920
if sys.version_info.major == 3:
2021
UNICODE_TYPE = str
@@ -1150,6 +1151,7 @@ def main():
11501151
action=dict(required=False, type='str',
11511152
default='announce', choices=["announce", "withdraw"]),
11521153
path=dict(required=False, type='str', default=''),
1154+
dut_interfaces=dict(required=False, type='str', default=''),
11531155
log_path=dict(required=False, type='str', default='')
11541156
),
11551157
supports_check_mode=False)
@@ -1160,11 +1162,17 @@ def main():
11601162
topo_name = module.params['topo_name']
11611163
ptf_ip = module.params['ptf_ip']
11621164
action = module.params['action']
1165+
dut_interfaces = module.params['dut_interfaces']
11631166
path = module.params['path']
11641167

11651168
topo = read_topo(topo_name, path)
11661169
if not topo:
11671170
module.fail_json(msg='Unable to load topology "{}"'.format(topo_name))
1171+
if dut_interfaces:
1172+
topo['topology']['VMs'] = MultiServersUtils.get_vms_by_dut_interfaces(topo['topology']['VMs'], dut_interfaces)
1173+
for vm_name in topo['configuration'].keys():
1174+
if vm_name not in topo['topology']['VMs']:
1175+
topo['configuration'].pop(vm_name)
11681176

11691177
is_storage_backend = "backend" in topo_name
11701178

ansible/library/test_facts.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,18 @@ def _read_testbed_topo_from_yaml():
169169
with open(self.testbed_filename) as f:
170170
tb_info = yaml.safe_load(f)
171171
for tb in tb_info:
172-
if tb["ptf_ip"]:
172+
if "ptf_ip" in tb and tb["ptf_ip"]:
173173
tb["ptf_ip"], tb["ptf_netmask"] = \
174174
_cidr_to_ip_mask(tb["ptf_ip"])
175-
if tb["ptf_ipv6"]:
175+
if "ptf_ipv6" in tb and tb["ptf_ipv6"]:
176176
tb["ptf_ipv6"], tb["ptf_netmask_v6"] = \
177177
_cidr_to_ip_mask(tb["ptf_ipv6"])
178178
tb["duts"] = tb.pop("dut")
179179
tb["duts_map"] = \
180180
{dut: i for i, dut in enumerate(tb["duts"])}
181+
if 'servers' in tb:
182+
tb['multi_servers_tacacs_ip'], _ = \
183+
_cidr_to_ip_mask(tb['servers'].values()[0]["ptf_ip"])
181184
self.testbed_topo[tb["conf-name"]] = tb
182185

183186
if self.testbed_filename.endswith(".csv"):

ansible/library/testbed_vm_info.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22

33
from ansible.module_utils.basic import AnsibleModule
4+
from ansible.module_utils.multi_servers_utils import MultiServersUtils
45
import re
56
import yaml
67
import traceback
@@ -49,12 +50,13 @@ class TestbedVMFacts():
4950
5051
"""
5152

52-
def __init__(self, toponame, base_vm, vm_file):
53+
def __init__(self, toponame, base_vm, vm_file, servers_info):
5354
CLET_SUFFIX = "-clet"
5455
self.toponame = re.sub(CLET_SUFFIX + "$", "", toponame)
5556
self.topofile = TOPO_PATH + 'topo_' + self.toponame + '.yml'
5657
self.base_vm = base_vm
5758
self.vm_file = vm_file
59+
self.servers_info = servers_info
5860
if has_dataloader:
5961
self.inv_mgr = InventoryManager(
6062
loader=DataLoader(), sources=self.vm_file)
@@ -65,6 +67,12 @@ def get_neighbor_eos(self):
6567
vm_topology = yaml.safe_load(f)
6668
self.topoall = vm_topology
6769

70+
if self.servers_info:
71+
return MultiServersUtils.generate_vm_name_mapping(
72+
self.servers_info,
73+
vm_topology['topology']['VMs']
74+
)
75+
6876
if len(self.base_vm) > 2:
6977
vm_start_index = int(self.base_vm[2:])
7078
vm_name_fmt = 'VM%0{}d'.format(len(self.base_vm) - 2)
@@ -116,6 +124,7 @@ def main():
116124
argument_spec=dict(
117125
base_vm=dict(required=True, type='str'),
118126
topo=dict(required=True, type='str'),
127+
servers_info=dict(required=False, type='dict', default={}),
119128
vm_file=dict(default=VM_INV_FILE, type='str')
120129
),
121130
supports_check_mode=True
@@ -128,7 +137,7 @@ def main():
128137
vm_mgmt_ip = {}
129138
try:
130139
vm_facts = TestbedVMFacts(
131-
m_args['topo'], m_args['base_vm'], m_args['vm_file'])
140+
m_args['topo'], m_args['base_vm'], m_args['vm_file'], m_args['servers_info'])
132141
neighbor_eos = vm_facts.get_neighbor_eos()
133142
neighbor_eos.update(vm_facts.get_neighbor_dpu())
134143
if has_dataloader:
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
class MultiServersUtils:
2+
@staticmethod
3+
def filter_by_dut_interfaces(values, dut_interfaces):
4+
if not dut_interfaces:
5+
return values
6+
7+
if isinstance(dut_interfaces, str) or isinstance(dut_interfaces, unicode): # noqa F821
8+
dut_interfaces = MultiServersUtils.parse_multi_servers_interface(dut_interfaces)
9+
10+
if isinstance(values, dict):
11+
return {k: v for k, v in values.items() if int(k) in dut_interfaces}
12+
elif isinstance(values, list):
13+
return [v for v in values if int(v) in dut_interfaces]
14+
else:
15+
raise ValueError('Unsupported type "{}"'.format(type(values)))
16+
17+
@staticmethod
18+
def parse_multi_servers_interface(intf_pattern):
19+
intf_pattern = str(intf_pattern)
20+
intfs = []
21+
for intf in iter(map(str.strip, intf_pattern.split(','))):
22+
if intf.isdigit():
23+
intfs.append(int(intf))
24+
elif '-' in intf:
25+
intf_range = list(map(int, map(str.strip, intf.split('-'))))
26+
assert len(intf_range) == 2, 'Invalid interface range "{}"'.format(intf)
27+
intfs.extend(list(range(intf_range[0], intf_range[1]+1)))
28+
else:
29+
raise ValueError('Unsupported format "{}"'.format(intf_pattern))
30+
if len(intfs) != len(set(intfs)):
31+
raise ValueError('There are interface duplication/overlap in "{}"'.format(intf_pattern))
32+
return intfs
33+
34+
@staticmethod
35+
def get_vms_by_dut_interfaces(VMs, dut_interfaces):
36+
if not dut_interfaces:
37+
return VMs
38+
39+
if isinstance(dut_interfaces, str) or isinstance(dut_interfaces, unicode): # noqa F821
40+
dut_interfaces = MultiServersUtils.parse_multi_servers_interface(dut_interfaces)
41+
42+
result = {}
43+
offset = 0
44+
for hostname, attr in VMs.items():
45+
if dut_interfaces and attr['vlans'][0] not in dut_interfaces:
46+
continue
47+
result[hostname] = attr
48+
result[hostname]['vm_offset'] = offset
49+
offset += 1
50+
return result
51+
52+
@staticmethod
53+
def generate_vm_name_mapping(servers_info, topo_vms):
54+
_m = {}
55+
56+
for server_attr in servers_info.values():
57+
if 'dut_interfaces' in server_attr:
58+
filtered_vms = MultiServersUtils.get_vms_by_dut_interfaces(topo_vms, server_attr['dut_interfaces'])
59+
vm_base = server_attr['vm_base']
60+
vm_start_index = int(vm_base[2:])
61+
vm_name_fmt = 'VM%0{}d'.format(len(vm_base) - 2)
62+
63+
for hostname, host_attr in filtered_vms.items():
64+
vm_name = vm_name_fmt % (vm_start_index + host_attr['vm_offset'])
65+
_m[hostname] = vm_name
66+
return _m

ansible/plugins/filter/filters.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
class FilterModule(object):
99
def filters(self):
1010
return {
11+
'filter_by_dut_interfaces': MultiServersUtils.filter_by_dut_interfaces,
12+
'get_vms_by_dut_interfaces': MultiServersUtils.get_vms_by_dut_interfaces,
1113
'extract_by_prefix': extract_by_prefix,
1214
'filter_by_prefix': filter_by_prefix,
1315
'filter_vm_targets': filter_vm_targets,
@@ -88,7 +90,7 @@ def first_n_elements(values, num):
8890
return values[0:int(num)]
8991

9092

91-
def filter_vm_targets(values, topology, vm_base):
93+
def filter_vm_targets(values, topology, vm_base, dut_interfaces=None):
9294
"""
9395
This function takes a list of host VMs as parameter 'values' and then extract a list of host VMs
9496
which starts with 'vm_base' and contains all VMs which mentioned in 'vm_offset' keys inside of 'topology' structure
@@ -114,17 +116,18 @@ def filter_vm_targets(values, topology, vm_base):
114116
if vm_base not in values:
115117
raise errors.AnsibleFilterError('Current vm_base: %s is not found in vm_list' % vm_base)
116118

119+
vms = MultiServersUtils.get_vms_by_dut_interfaces(topology, dut_interfaces) if dut_interfaces else topology
117120
result = []
118121
base = values.index(vm_base)
119-
for hostname, attr in topology.items():
122+
for hostname, attr in vms.items():
120123
if base + attr['vm_offset'] >= len(values):
121124
continue
122125
result.append(values[base + attr['vm_offset']])
123126

124127
return result
125128

126129

127-
def extract_hostname(values, topology, vm_base, inventory_hostname):
130+
def extract_hostname(values, topology, vm_base, inventory_hostname, dut_interfaces=None):
128131
"""
129132
This function takes a list of host VMs as parameter 'values' and then return 'inventory_hostname'
130133
corresponding EOS hostname based on 'topology' structure, 'vm_base' parameters
@@ -156,8 +159,9 @@ def extract_hostname(values, topology, vm_base, inventory_hostname):
156159
if vm_base not in values:
157160
raise errors.AnsibleFilterError('Current vm_base: %s is not found in vm_list' % vm_base)
158161

162+
vms = MultiServersUtils.get_vms_by_dut_interfaces(topology, dut_interfaces) if dut_interfaces else topology
159163
base = values.index(vm_base)
160-
for hostname, attr in topology.items():
164+
for hostname, attr in vms.items():
161165
if base + attr['vm_offset'] >= len(values):
162166
continue
163167
if inventory_hostname == values[base + attr['vm_offset']]:
@@ -209,3 +213,55 @@ def first_ip_of_subnet(value):
209213
def path_join(paths):
210214
"""Join path strings."""
211215
return os.path.join(*paths)
216+
217+
218+
class MultiServersUtils:
219+
@staticmethod
220+
def filter_by_dut_interfaces(values, dut_interfaces):
221+
if not dut_interfaces:
222+
return values
223+
224+
if isinstance(dut_interfaces, str) or isinstance(dut_interfaces, unicode): # noqa F821
225+
dut_interfaces = MultiServersUtils.parse_multi_servers_interface(dut_interfaces)
226+
227+
if isinstance(values, dict):
228+
return {k: v for k, v in values.items() if int(k) in dut_interfaces}
229+
elif isinstance(values, list):
230+
return [v for v in values if int(v) in dut_interfaces]
231+
else:
232+
raise ValueError('Unsupported type "{}"'.format(type(values)))
233+
234+
@staticmethod
235+
def parse_multi_servers_interface(intf_pattern):
236+
intf_pattern = str(intf_pattern)
237+
intfs = []
238+
for intf in iter(map(str.strip, intf_pattern.split(','))):
239+
if intf.isdigit():
240+
intfs.append(int(intf))
241+
elif '-' in intf:
242+
intf_range = list(map(int, map(str.strip, intf.split('-'))))
243+
assert len(intf_range) == 2, 'Invalid interface range "{}"'.format(intf)
244+
intfs.extend(list(range(intf_range[0], intf_range[1]+1)))
245+
else:
246+
raise ValueError('Unsupported format "{}"'.format(intf_pattern))
247+
if len(intfs) != len(set(intfs)):
248+
raise ValueError('There are interface duplication/overlap in "{}"'.format(intf_pattern))
249+
return intfs
250+
251+
@staticmethod
252+
def get_vms_by_dut_interfaces(VMs, dut_interfaces):
253+
if not dut_interfaces:
254+
return VMs
255+
256+
if isinstance(dut_interfaces, str) or isinstance(dut_interfaces, unicode): # noqa F821
257+
dut_interfaces = MultiServersUtils.parse_multi_servers_interface(dut_interfaces)
258+
259+
result = {}
260+
offset = 0
261+
for hostname, attr in VMs.items():
262+
if dut_interfaces and attr['vlans'][0] not in dut_interfaces:
263+
continue
264+
result[hostname] = attr
265+
result[hostname]['vm_offset'] = offset
266+
offset += 1
267+
return result

ansible/roles/eos/tasks/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
set_fact: VM_host={{ groups[current_server] | difference(VM_list) }}
1818

1919
- name: Generate hostname for target VM
20-
set_fact: hostname={{ VM_list | extract_hostname(topology['VMs'], VM_base, inventory_hostname) }}
20+
set_fact: hostname={{ VM_list | extract_hostname(topology['VMs'], VM_base, inventory_hostname, dut_interfaces | default("")) }}
2121
when: topology['VMs'] is defined
2222

2323
- fail:

ansible/roles/fanout/templates/arista_7260_connect.j2

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ vlan {{ dev_vlans | list | join(',') }}
1010
{% set peer_speed = root_conn[intf]['speed'] %}
1111
{% if peer_dev in lab_devices and 'Fanout' not in lab_devices[peer_dev]['Type'] and not deploy_leaf %}
1212
interface {{ intf }}
13+
{% if clean_before_add == 'y' %}
1314
switchport
1415
switchport trunk allowed vlan remove {{ dev_vlans | list | join(',') }}
16+
{% endif %}
1517
{% if peer_dev == server and peer_port == server_port %}
1618
switchport mode trunk
1719
switchport trunk allowed vlan add {{ dev_vlans | list | join(',') }}
@@ -20,8 +22,10 @@ interface {{ intf }}
2022
{% endif %}
2123
{% if peer_dev in lab_devices and 'Fanout' in lab_devices[peer_dev]['Type'] and deploy_leaf %}
2224
interface {{ intf }}
25+
{% if clean_before_add == 'y' %}
2326
switchport
2427
switchport trunk allowed vlan remove {{ dev_vlans | list | join(',') }}
28+
{% endif %}
2529
{% if peer_dev == leaf_name %}
2630
description {{ peer_dev }}-{{ peer_port }}
2731
speed forced {{ peer_speed }}full

0 commit comments

Comments
 (0)