|
16 | 16 | import argparse |
17 | 17 | import jsonpatch |
18 | 18 | import subprocess |
| 19 | +import concurrent.futures |
19 | 20 |
|
20 | 21 | # Add the parent directory to Python path to import sonic-utilities modules |
21 | 22 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) |
22 | 23 |
|
23 | 24 | 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 |
26 | 27 | from sonic_py_common import multi_asic |
27 | 28 | except ImportError as e: |
28 | 29 | print(f"Error importing required modules: {e}", file=sys.stderr) |
@@ -70,6 +71,55 @@ def validate_patch(patch): |
70 | 71 | return False |
71 | 72 |
|
72 | 73 |
|
| 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 | + |
73 | 123 | def create_checkpoint(args): |
74 | 124 | """Create a checkpoint of the current configuration""" |
75 | 125 | try: |
@@ -144,11 +194,76 @@ def apply_patch(args): |
144 | 194 | if not validate_patch(patch_json): |
145 | 195 | raise GenericConfigUpdaterError(f"Invalid patch format in file: {args.patch_file}") |
146 | 196 |
|
147 | | - # Apply patch using GenericUpdater |
148 | 197 | 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) |
152 | 267 |
|
153 | 268 | print_success("Patch applied successfully.") |
154 | 269 |
|
@@ -191,42 +306,25 @@ def save_config(args): |
191 | 306 | if args.verbose: |
192 | 307 | print(f"Saving configuration to: {filename}") |
193 | 308 |
|
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) |
220 | 318 |
|
221 | 319 | # Save to file |
222 | 320 | with open(filename, 'w') as f: |
223 | 321 | json.dump(config_to_save, f, indent=2) |
224 | 322 |
|
225 | 323 | print_success(f"Configuration saved successfully to '{filename}'.") |
226 | 324 |
|
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) |
230 | 328 | except Exception as ex: |
231 | 329 | print_error(f"Failed to save configuration: {ex}") |
232 | 330 | sys.exit(1) |
@@ -338,6 +436,11 @@ def main(): |
338 | 436 | action='store_true', |
339 | 437 | help='Print additional details of what the operation is doing' |
340 | 438 | ) |
| 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 | + ) |
341 | 444 | apply_parser.add_argument( |
342 | 445 | '--ignore-non-yang-tables', |
343 | 446 | action='store_true', |
|
0 commit comments