Skip to content

Commit e276765

Browse files
authored
Support multi-asic in gcu.py (sonic-net#4057)
What I did Microsoft ADO: 33497730 Update gcu.py to support multi-asic How I did it apply-patch and save api are different for multi-asic. How to verify it Run build pipeline.
1 parent d2c697f commit e276765

1 file changed

Lines changed: 138 additions & 35 deletions

File tree

scripts/gcu.py

Lines changed: 138 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
import argparse
1717
import jsonpatch
1818
import subprocess
19+
import concurrent.futures
1920

2021
# Add the parent directory to Python path to import sonic-utilities modules
2122
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
2223

2324
try:
24-
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat
25-
from generic_config_updater.gu_common import GenericConfigUpdaterError
25+
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat, extract_scope
26+
from generic_config_updater.gu_common import GenericConfigUpdaterError, HOST_NAMESPACE
2627
from sonic_py_common import multi_asic
2728
except ImportError as e:
2829
print(f"Error importing required modules: {e}", file=sys.stderr)
@@ -70,6 +71,55 @@ def validate_patch(patch):
7071
return False
7172

7273

74+
def multiasic_save_to_singlefile(filename):
75+
"""Save all ASIC configurations to a single file in multi-asic mode"""
76+
all_configs = {}
77+
78+
# Get host configuration
79+
cmd = ["sonic-cfggen", "-d", "--print-data"]
80+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
81+
host_config = json.loads(result.stdout)
82+
all_configs['localhost'] = host_config
83+
84+
# Get each ASIC configuration
85+
for namespace in multi_asic.get_namespace_list():
86+
cmd = ["sonic-cfggen", "-d", "--print-data", "-n", namespace]
87+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
88+
asic_config = json.loads(result.stdout)
89+
all_configs[namespace] = asic_config
90+
91+
# Save to file
92+
with open(filename, 'w') as f:
93+
json.dump(all_configs, f, indent=2)
94+
95+
96+
def apply_patch_for_scope(scope_changes, results, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path):
97+
"""Apply patch for a single ASIC scope"""
98+
scope, changes = scope_changes
99+
# Replace localhost to DEFAULT_NAMESPACE which is db definition of Host
100+
if scope.lower() == HOST_NAMESPACE or scope == "":
101+
scope = multi_asic.DEFAULT_NAMESPACE
102+
103+
scope_for_log = scope if scope else HOST_NAMESPACE
104+
105+
try:
106+
# Call apply_patch with the ASIC-specific changes
107+
GenericUpdater(scope=scope).apply_patch(jsonpatch.JsonPatch(changes),
108+
config_format,
109+
verbose,
110+
dry_run,
111+
ignore_non_yang_tables,
112+
ignore_path)
113+
results[scope_for_log] = {"success": True, "message": "Success"}
114+
except Exception as e:
115+
results[scope_for_log] = {"success": False, "message": str(e)}
116+
117+
118+
def apply_patch_wrapper(args):
119+
"""Wrapper for apply_patch_for_scope to support ThreadPoolExecutor"""
120+
return apply_patch_for_scope(*args)
121+
122+
73123
def create_checkpoint(args):
74124
"""Create a checkpoint of the current configuration"""
75125
try:
@@ -144,11 +194,76 @@ def apply_patch(args):
144194
if not validate_patch(patch_json):
145195
raise GenericConfigUpdaterError(f"Invalid patch format in file: {args.patch_file}")
146196

147-
# Apply patch using GenericUpdater
148197
config_format = ConfigFormat[args.format.upper()]
149-
updater = GenericUpdater()
150-
updater.apply_patch(patch, config_format, args.verbose, False,
151-
args.ignore_non_yang_tables, args.ignore_path)
198+
199+
# For multi-asic, extract scope and apply patches per ASIC
200+
if multi_asic.is_multi_asic():
201+
results = {}
202+
changes_by_scope = {}
203+
204+
# Iterate over each change in the JSON Patch
205+
for change in patch:
206+
scope, modified_path = extract_scope(change["path"])
207+
208+
# Modify the 'path' in the change to remove the scope
209+
change["path"] = modified_path
210+
211+
# Check if the scope is already in our dictionary, if not, initialize it
212+
if scope not in changes_by_scope:
213+
changes_by_scope[scope] = []
214+
215+
# Add the modified change to the appropriate list based on scope
216+
changes_by_scope[scope].append(change)
217+
218+
# Empty case to force validate YANG model
219+
if not changes_by_scope:
220+
asic_list = [multi_asic.DEFAULT_NAMESPACE]
221+
asic_list.extend(multi_asic.get_namespace_list())
222+
for asic in asic_list:
223+
changes_by_scope[asic] = []
224+
225+
# Apply changes for each scope
226+
if args.parallel:
227+
with concurrent.futures.ThreadPoolExecutor() as executor:
228+
# Prepare the argument tuples
229+
arguments = [
230+
(scope_changes, results, config_format,
231+
args.verbose, False, args.ignore_non_yang_tables, args.ignore_path)
232+
for scope_changes in changes_by_scope.items()
233+
]
234+
235+
# Submit all tasks and wait for them to complete
236+
futures = [executor.submit(apply_patch_wrapper, arguments)
237+
for arguments in arguments]
238+
239+
# Wait for all tasks to complete
240+
concurrent.futures.wait(futures)
241+
else:
242+
# Apply changes for each scope sequentially
243+
for scope_changes in changes_by_scope.items():
244+
apply_patch_for_scope(scope_changes,
245+
results,
246+
config_format,
247+
args.verbose, False,
248+
args.ignore_non_yang_tables,
249+
args.ignore_path)
250+
251+
# Check if any updates failed
252+
failures = [scope for scope, result in results.items() if not result['success']]
253+
254+
if failures:
255+
failure_messages = '\n'.join([
256+
f"- {failed_scope}: {results[failed_scope]['message']}"
257+
for failed_scope in failures
258+
])
259+
raise GenericConfigUpdaterError(
260+
f"Failed to apply patch on the following scopes:\n{failure_messages}"
261+
)
262+
else:
263+
# Single ASIC mode - use traditional approach
264+
updater = GenericUpdater()
265+
updater.apply_patch(patch, config_format, args.verbose, False,
266+
args.ignore_non_yang_tables, args.ignore_path)
152267

153268
print_success("Patch applied successfully.")
154269

@@ -191,42 +306,25 @@ def save_config(args):
191306
if args.verbose:
192307
print(f"Saving configuration to: {filename}")
193308

194-
# Get current configuration using sonic-cfggen
195-
try:
196-
# Handle multi-ASIC configurations
197-
if multi_asic.is_multi_asic():
198-
# Save all ASIC configurations to a single file
199-
all_configs = {}
200-
201-
# Get host configuration
202-
cmd = ["sonic-cfggen", "-d", "--print-data"]
203-
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
204-
host_config = json.loads(result.stdout)
205-
all_configs['localhost'] = host_config
206-
207-
# Get each ASIC configuration
208-
for namespace in multi_asic.get_namespace_list():
209-
cmd = ["sonic-cfggen", "-d", "--print-data", "-n", namespace]
210-
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
211-
asic_config = json.loads(result.stdout)
212-
all_configs[namespace] = asic_config
213-
214-
config_to_save = all_configs
215-
else:
216-
# Single ASIC configuration
217-
cmd = ["sonic-cfggen", "-d", "--print-data"]
218-
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
219-
config_to_save = json.loads(result.stdout)
309+
# In multi-asic mode, save all ASIC configurations to single file
310+
if multi_asic.is_multi_asic():
311+
multiasic_save_to_singlefile(filename)
312+
print_success(f"Configuration saved successfully to '{filename}'.")
313+
else:
314+
# Single ASIC configuration
315+
cmd = ["sonic-cfggen", "-d", "--print-data"]
316+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
317+
config_to_save = json.loads(result.stdout)
220318

221319
# Save to file
222320
with open(filename, 'w') as f:
223321
json.dump(config_to_save, f, indent=2)
224322

225323
print_success(f"Configuration saved successfully to '{filename}'.")
226324

227-
except subprocess.CalledProcessError as e:
228-
raise Exception(f"Failed to get current configuration: {e}")
229-
325+
except subprocess.CalledProcessError as e:
326+
print_error(f"Failed to get current configuration: {e}")
327+
sys.exit(1)
230328
except Exception as ex:
231329
print_error(f"Failed to save configuration: {ex}")
232330
sys.exit(1)
@@ -338,6 +436,11 @@ def main():
338436
action='store_true',
339437
help='Print additional details of what the operation is doing'
340438
)
439+
apply_parser.add_argument(
440+
'-p', '--parallel',
441+
action='store_true',
442+
help='Apply changes to all ASICs in parallel (multi-asic only)'
443+
)
341444
apply_parser.add_argument(
342445
'--ignore-non-yang-tables',
343446
action='store_true',

0 commit comments

Comments
 (0)