Skip to content

Commit 794cc01

Browse files
authored
feat: support trimming lab inv file (#17348)
Description of PR Support trimming the inventory files such as ansible/lab, ansible/t2_lab etc when passing --trim_inv option. Summary: Fixes # (issue) Microsoft ADO 30056122 Approach What is the motivation for this PR? When we enable inventory trimming by passing the --trim_inv option, the current logic is to only trim the ansible/veos file, but we noticed that the other inventory file (such as ansible/lab) should also be trimmed because it contains the configs of all the devices in that lab, but we only need the configs related to the current test run. Therefore, we decided to support trimming these inventory files as well. Please note that the PDU & Fanout hosts trimming is not supported in this PR as it's currently blocked by #17347 How did you do it? How did you verify/test it? I ran the new trimming logic on various lab files and can confirm it's working well: https://elastictest.org/scheduler/testplan/67c7ad505048655bf9cf8a58 https://elastictest.org/scheduler/testplan/67c78be48dcac0cdc64a3998 https://elastictest.org/scheduler/testplan/67c78cc7f60a7a79ff1ae585 https://elastictest.org/scheduler/testplan/67c78c9c8dcac0cdc64a399c https://elastictest.org/scheduler/testplan/67c7b419d0bae94c81d8a9d6 https://elastictest.org/scheduler/testplan/67ca846a5048655bf9cf8f7b Any platform specific information? co-authorized by: jianquanye@microsoft.com
1 parent 6cc333b commit 794cc01

2 files changed

Lines changed: 113 additions & 35 deletions

File tree

tests/common/helpers/inventory_utils.py

Lines changed: 111 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
logger = logging.getLogger(__name__)
99

1010

11-
def trim_inventory(inv_files, tbinfo):
11+
def trim_inventory(inv_files, tbinfo, target_hostname):
1212
"""
1313
Trim the useless topology neighbor the inv_files according to testbed to speed up ansible inventory initialization.
1414
@@ -21,21 +21,30 @@ def trim_inventory(inv_files, tbinfo):
2121
The useless ansible_host extremely slow down the initialization.
2222
2323
Hence, will trim and generate a temporary inventory file, for example:
24-
['../ansible/veos', '../ansible/inv1'] -> ['../ansible/veos_kvm-t0_trim_tmp', '../ansible/inv1']
25-
Then pytest will use the light inventory file '../ansible/veos_kvm-t0_trim_tmp' to initialize the ansible_hosts.
24+
['../ansible/veos', '../ansible/lab1'] -> ['../ansible/veos_kvm-t0_trim_tmp', '../ansible/lab1_kvm-t0_trim_tmp']
25+
Then pytest will use the light inventory files '../ansible/veos_kvm-t0_trim_tmp' and
26+
'../ansible/lab1_kvm-t0_trim_tmp' to initialize the ansible_hosts.
2627
2728
Args:
28-
inv_files: inventory file list, sample: ['../ansible/veos', '../ansible/inv1']
29+
inv_files: inventory file list, sample: ['../ansible/veos', '../ansible/lab1']
2930
tbinfo: tbinfo
31+
target_hostname: target_hostname, will be None if parallel run is disabled
3032
3133
Returns:
3234
Direct update inv_files and return nothing
3335
"""
3436

3537
vm_base = tbinfo.get("vm_base", None)
3638
tb_name = tbinfo.get("conf-name", None)
39+
ptf = tbinfo.get("ptf", None)
40+
inv_name = tbinfo.get("inv_name", None)
41+
duts = tbinfo.get("duts", None)
42+
3743
pattern = r'VM\d{3,7}'
38-
logger.info(f"Start to trim inventory, tb[{tb_name}] vm_base [{vm_base}], inv file {inv_files}")
44+
45+
# TODO: Trim fanout hosts and PDU hosts. Currently blocked by https://github.com/sonic-net/sonic-mgmt/issues/17347
46+
logger.info(f"Start to trim inventory, inv_files: {inv_files}, vm_base: {vm_base}, tb_name: {tb_name}, "
47+
f"ptf: {ptf}, inv_name: {inv_name}, duts: {duts}")
3948

4049
# Find a key in all the levels of a dict,
4150
# Return the val of the key and the vm_base_path of the key
@@ -55,36 +64,104 @@ def find_key(d: dict, target_key: str = None, regex: str = None):
5564
stack.append((value, path + [key]))
5665
return None, []
5766

58-
# Remove all the matched
67+
def get_value_by_key_path(d: dict, key_path: list):
68+
for k in key_path:
69+
d = d[k]
70+
71+
return d
72+
73+
def dump_trimmed_inv_to_file(trimmed_inv, trimmed_inventory_file_name):
74+
with BlankNone(), open(trimmed_inventory_file_name, 'w') as f:
75+
yaml.dump(trimmed_inv, f)
76+
77+
# Remove all the unnecessary nodes in the inventory file(s)
5978
for idx in range(len(inv_files)):
6079
inv_file = inv_files[idx]
80+
if target_hostname is not None:
81+
trimmed_inv_file_name = f"{inv_file}_{tb_name}_{target_hostname}_trim_tmp"
82+
else:
83+
trimmed_inv_file_name = f"{inv_file}_{tb_name}_trim_tmp"
84+
6185
with open(inv_file, 'r') as file:
6286
inv = yaml.safe_load(file)
63-
# If the vm_base is found in the inv_file,
64-
# then other useless topology neighbor definition are in it,
65-
# that's the inventory to be trimmed
66-
_, vm_base_path = find_key(d=inv, target_key=vm_base)
67-
if vm_base_path:
68-
keys_to_del = set()
69-
logger.info(f"Find vm_base {vm_base} in inv file {inv_file}, path: {vm_base_path}")
70-
71-
for root_key in inv.keys():
72-
neighbor_val, neighbor_path = find_key(d=inv[root_key], regex=pattern)
73-
if neighbor_path:
74-
# Keep the neighbor server for the testbed
75-
if root_key == vm_base_path[0]:
76-
logger.info(f"vm_base[{vm_base}] located in {root_key}, inv file {inv_file}, keep it")
77-
# Remove all the useless neighbor server ansible_hosts
78-
else:
79-
logger.info(f"Attempt to remove {root_key} in inv file {inv_file}")
80-
keys_to_del.add(root_key)
81-
82-
for key_to_del in keys_to_del:
83-
del inv[key_to_del]
84-
85-
# dump and replace trimmed inventory file
86-
trimmed_inventory_file_name = f"{inv_file}_{tb_name}_trim_tmp"
87-
with BlankNone(), open(trimmed_inventory_file_name, 'w') as f:
88-
yaml.dump(inv, f)
89-
90-
inv_files[idx] = trimmed_inventory_file_name
87+
if inv_file.split('/')[-1] == inv_name:
88+
89+
def trim_value_of_key_path(key_path, keys_to_keep):
90+
original_val = get_value_by_key_path(d=inv, key_path=key_path)
91+
all_keys = set(original_val.keys())
92+
for key in all_keys:
93+
if key not in keys_to_keep:
94+
del original_val[key]
95+
96+
vars_to_trim = ["ptf", "hwsku"]
97+
for var in vars_to_trim:
98+
if var == "ptf" and ptf:
99+
_, ptf_key_path = find_key(d=inv, target_key=ptf)
100+
if ptf_key_path:
101+
logger.info(f"Keep PTF host {ptf} and trim the rest PTF hosts")
102+
trim_value_of_key_path(ptf_key_path[:-1], {ptf})
103+
elif var == "hwsku" and duts:
104+
# We have all the user-defined HwSKUs as the root keys of this inv file, and these
105+
# HwSKUs combined should be a subset of the keys under the sonic.children node.
106+
# We want to keep the active HwSKU(s) based on the DUTs info and trim the rest.
107+
# Besides, we should also trim the DUT hosts under active HwSKU(s) based on DUTs info.
108+
# Note: if we find an active DUT's HwSKU is somehow not a value under sonic.children node,
109+
# we will skip the HwSKU trimming to avoid potential data loss.
110+
all_hwskus = set(inv["sonic"]["children"].keys())
111+
should_trim_hwsku = True
112+
hwskus_to_keep = dict()
113+
for dut in duts:
114+
_, dut_key_path = find_key(d=inv, target_key=dut)
115+
curr_hwksu = dut_key_path[0] if dut_key_path else None
116+
if not curr_hwksu or curr_hwksu in hwskus_to_keep:
117+
continue
118+
119+
if curr_hwksu in all_hwskus:
120+
hwskus_to_keep[curr_hwksu] = dut_key_path
121+
else:
122+
logger.warning(
123+
f"DUT {dut}'s hwsku is not a value under sonic.children node, skip trimming hwsku"
124+
)
125+
126+
should_trim_hwsku = False
127+
break
128+
129+
if should_trim_hwsku:
130+
for hwsku in all_hwskus:
131+
if hwsku in hwskus_to_keep:
132+
trim_value_of_key_path(hwskus_to_keep[hwsku][:-1], set(duts))
133+
else:
134+
del inv["sonic"]["children"][hwsku]
135+
if hwsku in inv:
136+
logger.info(f"Attempt to delete {hwsku} in inv file {inv_file}")
137+
del inv[hwsku]
138+
else:
139+
logger.warning(f"Unknown or invalid var {var} to trim")
140+
141+
dump_trimmed_inv_to_file(inv, trimmed_inv_file_name)
142+
inv_files[idx] = trimmed_inv_file_name
143+
else:
144+
# If the vm_base is found in the inv_file,
145+
# then other useless topology neighbor definition are in it,
146+
# that's the inventory to be trimmed
147+
_, vm_base_path = find_key(d=inv, target_key=vm_base)
148+
if vm_base_path:
149+
keys_to_del = set()
150+
logger.info(f"Find vm_base {vm_base} in inv file {inv_file}, path: {vm_base_path}")
151+
152+
for root_key in inv.keys():
153+
neighbor_val, neighbor_path = find_key(d=inv[root_key], regex=pattern)
154+
if neighbor_path:
155+
# Keep the neighbor server for the testbed
156+
if root_key == vm_base_path[0]:
157+
logger.info(f"vm_base[{vm_base}] located in {root_key}, inv file {inv_file}, keep it")
158+
# Remove all the useless neighbor server ansible_hosts
159+
else:
160+
logger.info(f"Attempt to remove {root_key} in inv file {inv_file}")
161+
keys_to_del.add(root_key)
162+
163+
for key_to_del in keys_to_del:
164+
del inv[key_to_del]
165+
166+
dump_trimmed_inv_to_file(inv, trimmed_inv_file_name)
167+
inv_files[idx] = trimmed_inv_file_name

tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,8 @@ def enhance_inventory(request, tbinfo):
278278
inv_files = [inv_file.strip() for inv_file in inv_opt.split(",")]
279279

280280
if request.config.getoption("trim_inv"):
281-
trim_inventory(inv_files, tbinfo)
281+
target_hostname = get_target_hostname(request)
282+
trim_inventory(inv_files, tbinfo, target_hostname)
282283

283284
try:
284285
logger.info(f"Inventory file: {inv_files}")

0 commit comments

Comments
 (0)