Skip to content

[code sync] Merge code from sonic-net/sonic-utilities:202511 to 202603 - #461

Merged
mssonicbld merged 8 commits into
Azure:202603from
mssonicbld:sonicbld/202603-merge
Sep 14, 2026
Merged

mssonicbld merged 8 commits into
Azure:202603from
mssonicbld:sonicbld/202603-merge

Conversation

@mssonicbld

Copy link
Copy Markdown
Collaborator
* 2fe029a1 - (origin/202511) [generic_config_updater]: Remove empty leaf-lists from the simulated ConfigDB target (#4731) (#4817) (2026-09-11) [rimunagala]
* befb0eb1 - fix GCU patch sorter crash on create-only field change referenced by a leaf-list (#4668) (#4816) (2026-09-11) [rimunagala]
* 6c5a4fc1 - [generic_config_updater] Cache loadData() calls to reduce redundant YANG parsing (#4476) (#4815) (2026-09-11) [rimunagala]
* 5962f5cf - GCU sort: batch leaf-list changes into single REPLACE move (#4478) (#4814) (2026-09-11) [rimunagala]
* be04b07f - GCU generates suboptimal plan for CreateOnly paths (#4335) (#4813) (2026-09-11) [rimunagala]
* f27fda0c - GCU: Add path tracing support (#4317) (#4812) (2026-09-10) [rimunagala]
* d508fcff - Fix JsonMove._get_value to Support Both String and Integer List Indices (#4237) (#4811) (2026-09-10) [rimunagala]<br>```

rimunagala and others added 8 commits September 10, 2026 16:40
…es (#4237) (#4811)

What I did:
Issue: #4221

Updated JsonMove._get_value to handle both string and integer indices when traversing lists in config data.
Adjusted related unit tests to reflect the new behavior.
How I did it:
Modified the traversal logic to convert string tokens to integers when accessing lists, allowing both "1" and 1 as valid indices.
Removed the test expecting a TypeError for integer indices and added assertions for both string and integer index access.
How to verify it:
Patched change in lab device, confirmed.

admin@STR-SN5640-RDMA-1:~$ cat /usr/local/lib/python3.11/dist-packages/generic_config_updater/patch_sorter.py | grep -C 2 "int(token)"
        for token in tokens:
            if isinstance(config, list):
                token = int(token)
            config = config[token]

admin@STR-SN5640-RDMA-1:~$ cat t_tc_to_queue_map_modify.json
[
  {
    "op": "replace",
    "path": "/TC_TO_QUEUE_MAP/AZURE/8",
    "value": "8"
  },
  {
    "op": "add",
    "path": "/TC_TO_QUEUE_MAP/AZURE/7",
    "value": "7"
  }
]

admin@STR-SN5640-RDMA-1:~$ sudo config apply-patch -v t_tc_to_queue_map_modify.json
Patch Applier: localhost: Patch application starting.
Patch Applier: localhost: Patch: [{"op": "replace", "path": "/TC_TO_QUEUE_MAP/AZURE/8", "value": "8"}, {"op": "add", "path": "/TC_TO_QUEUE_MAP/AZURE/7", "value": "7"}]
Patch Applier: localhost getting current config db.
Patch Applier: localhost: simulating the target full config after applying the patch.
Patch Applier: localhost: validating all JsonPatch operations are permitted on the specified fields
Patch Applier: localhost: validating target config does not have empty tables,
                            since they do not show up in ConfigDb.
Patch Applier: localhost: sorting patch updates.
Patch Sorter - Strict: Validating patch is not making changes to tables without YANG models.
Patch Sorter - Strict: Validating target config according to YANG models.
Patch Sorter - Strict: Sorting patch updates.
Patch Applier: The localhost patch was converted into 1 change:
Patch Applier: localhost: applying 1 change in order:
Patch Applier:   * [{"op": "replace", "path": "/TC_TO_QUEUE_MAP/AZURE/7", "value": "7"}, {"op": "replace", "path": "/TC_TO_QUEUE_MAP/AZURE/8", "value": "8"}]
Patch Applier: localhost: verifying patch updates are reflected on ConfigDB.
Patch Applier: localhost patch application completed.
Patch applied successfully.
Also run the updated unit tests and all tests should pass, confirming the fix.



(cherry picked from commit 40260d5)

Signed-off-by: rimunagala <rimunagala@microsoft.com>
Co-authored-by: Xincun Li <147451452+xincunli-sonic@users.noreply.github.com>
GCU uses a very complex set of generators and validators.  When a suboptimal
plan is created, it can be hard to determine why the path was generated the
way it was and how best to optimize the generators.

This patch adds the ability to pass an IO object to all attempted paths
with annotations such as `valid`, `invalid`, `recurse_reject` (source and
target were already attempted), and `path_issue` (patch was good, but a
generator further down the hierarchy failed).  It will also list the
generator used for each patch, and if `invalid` it will list the validator
which failed it with a possibly extended error message.

For callers to `config replace` or `config apply-patch` a new command line
option of `--path-trace` which takes a filename to dump the json output.

This patch also increases the diff output shown during test run failures
to make it easier to debug issues based on logging generated via CI/CD.



(cherry picked from commit 369e703)

Signed-off-by: rimunagala <rimunagala@microsoft.com>
Co-authored-by: Brad House - Nexthop <bhouse@nexthop.ai>
* GCU generates suboptimal plan for CreateOnly paths

When GCU hits a CreateOnly entry that has changed, it generates a suboptimal
plan.  One example is a simple change of:
```
[{"op": "replace", "path": "/MIRROR_SESSION/EVERFLOW_TUNNEL/dst_ip", "value": "200.1.1.203"}]
```

Should generate an optimal plan of:
```
[
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION"}],
[{"op": "remove", "path": "/MIRROR_SESSION"}],
[{"op": "add", "path": "/MIRROR_SESSION", "value": {"EVERFLOW_TUNNEL": {"dscp": "8", "dst_ip": "200.1.1.203", "src_ip": "100.1.1.1", "ttl": "255", "type": "ERSPAN"}}}]
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION", "value": "EVERFLOW_TUNNEL"}]
]
```

But instead generates this plan (which removes all ACLs):
```
[
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION"}],
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1"}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1", "value": {"PRIORITY": "1000"}}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION", "value": "EVERFLOW_TUNNEL"}],
[{"op": "remove", "path": "/ACL_RULE"}],
[{"op": "add", "path": "/ACL_RULE", "value": {"EVERFLOW|RULE_1": {"PRIORITY": "1000", "IP_TYPE": "IP", "MIRROR_INGRESS_ACTION": "EVERFLOW_TUNNEL"}}}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1", "value": {"PRIORITY": "10"}}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/DST_IP", "value": "192.168.1.1/32"}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/IP_TYPE", "value": "IP"}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/L4_DST_PORT", "value": "22"}],
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION"}],
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1"}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1", "value": {"PRIORITY": "1000"}}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION", "value": "EVERFLOW_TUNNEL"}],
[{"op": "remove", "path": "/ACL_RULE/DATAACL|RULE_1"}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1", "value": {"PRIORITY": "10"}}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/DST_IP", "value": "192.168.1.1/32"}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/IP_TYPE", "value": "IP"}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/PACKET_ACTION", "value": "DROP"}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1/IP_TYPE", "value": "IP"}],
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION"}],
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1"}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1", "value": {"PRIORITY": "1000"}}],
[{"op": "remove", "path": "/ACL_RULE/DATAACL|RULE_1"}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1/IP_TYPE", "value": "IP"}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1", "value": {"PRIORITY": "10"}}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/DST_IP", "value": "192.168.1.1/32"}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/IP_TYPE", "value": "IP"}],
[{"op": "remove", "path": "/ACL_RULE/EVERFLOW|RULE_1"}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1", "value": {"PRIORITY": "1000"}}],
[{"op": "remove", "path": "/MIRROR_SESSION"}],
[{"op": "add", "path": "/MIRROR_SESSION", "value": {"EVERFLOW_TUNNEL": {"dscp": "8", "dst_ip": "200.1.1.203", "src_ip": "100.1.1.1", "ttl": "255", "type": "ERSPAN"}}}],
[{"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/L4_DST_PORT", "value": "22"}, {"op": "add", "path": "/ACL_RULE/DATAACL|RULE_1/PACKET_ACTION", "value": "DROP"}],
[{"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1/IP_TYPE", "value": "IP"}, {"op": "add", "path": "/ACL_RULE/EVERFLOW|RULE_1/MIRROR_INGRESS_ACTION", "value": "EVERFLOW_TUNNEL"}]
]
```

Modified`RemoveCreateOnlyDependencyMoveGenerator`:
 * it would previously short-circuit early due to only processing one child
   leaf in the same table.
 * it would previously attempt to iterate across all members of the table even
   though there was a complete path list.
 * it was missing logic to remove the create only path itself (and was relying
   on extenders to do that which was inefficient and wouldn't generate the
   right plan)
 * when removing dependents it wasn't recursing to ensure it would remove
   dependents of dependents

Since this generator is now full and doesn't rely on any extenders, it has
been moved to a non-extendable generator.

These changes caused some existing (suboptimal) plans that got generated to
change so those test cases have also been updated.

Added test case to validate this behavior and ensure it does not regress.



* Update generic_config_updater/patch_sorter.py

Prevent possible infinite loop scenario Copilot identified,
however it shouldn't be possible given self.__get_path_count()
can't return 1 in that scenario to allow the loop to continue.
But hardening isn't a bad practice.




* Update generic_config_updater/patch_sorter.py

Fix spelling / gramatical error caught by Copilot.




* Code Review Comments

1. Add recursion depth protection in removing dependents because of
   a concern of recursive dependencies, which isn't actually possible
   with yang.  None-the-less, implemented.
2. Remove a duplicate move that gets generated as when using it with a
   Depth-first sorter its not necessary though could be on other sorters.
3. The old code depended on 3 levels of depth for the create only
   leafs.  Reworked the logic to not be dependent on depth.
4. __get_path_count() can no longer return a KeyError even though the
   caller paths would make that impossible, but future use cases
   may need it to not throw an exception when the path is not found.



---------




(cherry picked from commit 1580ccc)

Signed-off-by: rimunagala <rimunagala@microsoft.com>
Co-authored-by: Brad House - Nexthop <bhouse@nexthop.ai>
…4814)

Add BulkLeafListMoveGenerator that produces a single REPLACE move for
leaf-list fields whose items differ between current and target configs,
instead of decomposing into N individual REMOVE/ADD moves.

This is registered as a non-extendable generator (tried before individual
moves in DFS). If validation fails, DFS falls through to per-item moves.

Impact: For a 512-port ACL table where half the ports are removed, this
reduces ~256 individual moves (each triggering 2 loadData calls at ~1.4s
each = ~717s) to 1 move (1 loadData = ~1.4s).

Conservative scope:
- Only handles leaf-lists (lists of scalars, not lists of dicts)
- Only replaces lists that exist in both current and target
- Falls through to individual moves if the bulk replace fails validation




(cherry picked from commit bfc67f5)

Signed-off-by: rimunagala <rimunagala@microsoft.com>
Co-authored-by: rookie-who <vaibhavcall@gmail.com>
…ANG parsing (#4476) (#4815)

The two caches in this PR target different layers:

1. _currently_loaded_hash in SonicYangCfg.loadData() — skips re-parsing when the same config (by content hash) is loaded consecutively. This helps when multiple validators call loadData() with identical config within a single move validation.

2. _validate_config_cache in ConfigWrapper.validate_config_db() — caches the validation result for a given config hash, so if the same config state is validated again later, it returns the cached pass/fail without calling loadData() at all.

Per-operation analysis

Operation	Helps?	Why
REMOVE (individual)	❌ No	Each DFS step removes one item → unique config at each step. Neither cache hits because every state is different.
ADD	⚠️ Marginal	Typically 1 move → few loadData calls total. Cache might save 1 call if FullConfigMoveValidator and NoDependencyMoveValidator validate the same state.
REPLACE (scalar)	⚠️ Marginal	Same as ADD — few moves, small absolute savings.
REMOVE (batched via #4478)	✅ Yes	#4478 collapses N individual REMOVEs into 1 bulk REPLACE move. That single move still triggers multiple validator calls with the same config. Cache deduplicates those, reducing loads/move from ~10.6x to ~7.7

---------



(cherry picked from commit 5d54e44)

Signed-off-by: rimunagala <rimunagala@microsoft.com>
…a leaf-list (#4668) (#4816)

#### Why I did it

The GCU (`generic_config_updater`) patch sorter aborted with an unhandled `ValueError ("'<x>' is not in list")` when a `config replace` / apply-patch changed a create-only PORT field (e.g. `lanes` during a breakout) while that port was a member of a multi-member leaf-list such as `ACL_TABLE.ports`.

During move validation the sorter transiently removes the port from the leaf-list in the simulated intermediate config, but a still-present leafref to it then fails to resolve (`list.index` raises `ValueError`). The exception propagated out of `RemoveCreateOnlyDependencyMoveValidator._validate_member` and aborted the whole sort, so any GCU/config-replace-based breakout failed on a realistic switch config (auto-rollback). The native `config interface breakout` path avoids GCU and is unaffected.

#### How I did it

Treat an unresolvable reference in a simulated intermediate config as an *invalid move*: catch the error in `_validate_member` and return `False` so the DFS backtracks to a valid ordering instead of letting the exception propagate and abort the whole sort.

#### How to verify it

Added a regression test that reproduces the create-only PORT field change against a multi-member `ACL_TABLE.ports` leaf-list and asserts the move is rejected rather than raising:

```
python3 -m pytest tests/generic_config_updater/patch_sorter_test.py \
  -k test_validate__unresolvable_ref_in_simulated_config__move_rejected
```

Verified the new test **fails** with the original code (raising the reported `ValueError`) and **passes** with the fix. 



(cherry picked from commit 0552d0d24f67b287d74ef82ef7922f389f715c5f)

Signed-off-by: rimunagala <rimunagala@microsoft.com>
Co-authored-by: Brad House - Nexthop <bhouse@nexthop.ai>
…ConfigDB target (#4731) (#4817)

Emptying a leaf-list through `config apply-patch` left an invalid value in
CONFIG_DB and made the patch application fail. Removing the last item of e.g.
BGP_ALLOWED_PREFIXES|DEPLOYMENT_ID|0 prefixes_v4 wrote `prefixes_v4@ = ""` and
the command reported "after applying patch to config, there are still some
parts not updated". Every later patch touching that field then failed YANG
validation with `"" does not conform`.

Root cause is that CONFIG_DB has no representation for an empty leaf-list.
set_entry serializes [] to an empty string, which is read back as [""], so a
target config asking for [] can never be satisfied and the final
verify_same_json in PatchApplier.apply always mismatches. The canonical way to
express "this leaf-list has no items" is for the field to be absent.

Add PatchWrapper.simulate_config_db_patch, which simulates the patch and then
drops empty leaf-lists, and use it everywhere the result is a ConfigDB target:
PatchApplier.apply, all three sorters (StrictPatchSorter.sort,
NonStrictPatchSorter.sort and PatchSorter.sort), and
convert_config_db_patch_to_sonic_yang_patch. The sorter now produces a field
level remove instead of a replace with an empty list, the change applier writes
the entry without the field, and the readback matches the target.

simulate_patch stays shape agnostic so the SonicYang format path is unaffected.
In SonicYang json a list holds yang list entries rather than leaf-list items, so
ConfigDB canonicalization must not be applied there.

The guard in BulkLeafListMoveGenerator is kept as defence in depth so the
generator never emits a bulk replace with an empty list.

This intentionally reverses one assertion added by #4478.
test_generate__leaf_list_all_items_removed__single_replace_move expected the
generator to emit REPLACE with an empty list for an emptied leaf-list. That is
the value CONFIG_DB cannot store, so the test is renamed to
test_generate__leaf_list_all_items_removed__no_bulk_replace_move and now expects
no bulk move, leaving the transition to the granular removal generators. The
batching optimisation from #4478 is untouched for every non-empty leaf-list.

Verified on docker-sonic-vs built from master using the reproduction steps from
sonic-net/sonic-buildimage#27818. Before the change CONFIG_DB ended up with
prefixes_v4@ = "" and apply-patch failed; after the change the field is removed
and the patch applies successfully, including re-adding the leaf-list
afterwards. The full generic_config_updater suite passes in the same container,
558 passed against a 554 passed baseline.



(cherry picked from commit 4464c12241d3f87f8d43c71e60a57a438484aa0b)

Signed-off-by: rimunagala <rimunagala@microsoft.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator Author

/azp run

@mssonicbld
mssonicbld merged commit ee5e882 into Azure:202603 Sep 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants