- Table of Contents
- List of Tables
- Revision
- About this Manual
- Scope
- Definition/Abbreviation
- 1 Feature Overview
- 2 Functionality
- 3 Design
- 4 Flow Diagrams
- 5 Error Handling
- 6 Serviceability and Debug
- 7 Warm Boot Support
- 8 Scalability
- 9 Unit Tests
- Appendix A Examples
Rev | Date | Author | Change Description |
---|---|---|---|
0.1 | 03/01/2021 | Mohamed Ghoneim | Initial version |
This document provides a detailed description on the algorithm to implement patch ordering described in SONiC Generic Configuration Update and Rollback - HLD
This document describes the algorithm of patch ordering described in SONiC Generic Configuration Update and Rollback - HLD using YANG models. This document provides minor implementation details about the proposed solutions.
Term | Meaning |
---|---|
JsonPatch | JSON document structure for expressing a sequence of operations to apply to a JavaScript Object Notation (JSON) document. Check JSON Patch (RFC6902). |
Please make sure you have reviewed SONiC Generic Configuration Update and Rollback design document especially Patch Orderer.
In this design document, we are going to explore using YANG models to order a given JsonPatch of updates. The idea is to make sure the transitions applied to the configurations all result in valid configs according to SONiC YANG models.
YANG models has multiple constrains that can affect ordering (check YANG 1.1 (RFC7950)):
- leafref built-in type
- must statement
- ... other statements
These constrains should be helpful while generating the list of transitions needed.
- The patch of updates should follow a standard notation. The JSON Patch (RFC6902) notation should be used.
- The return of the function is a list of JsonChanges (check JsonChange).
- Each application of a JsonChange should move the configuration from a valid state to another valid state.
- A valid state is a config that is valid according to YANG models.
- Each JsonChange change should assume the applier of the change will have no notion of ordering. i.e. ordering of updates is only guaranteed between different JsonChange, within the same JsonChange order of update is not guaranteed.
- It is assumed that the configuration lock was acquired before starting patch ordering.
N/A
N/A
N/A
From SONiC Generic Configuration Update and Rollback - HLD, this is the target interface:
list<JsonChange> order-patch(JsonPatch jsonPatch)
aspect | item | description |
---|---|---|
inputs | JsonPatch | It represents the changes that needs to applied to the device running config, described in JSON Patch (RFC6902). |
outputs | list<JsonChange> | The list will contain the steps to be followed to apply the input JsonPatch correctly. Each item in the list is assumed to be executed after the previous item, in the order given in the list. |
errors | malformedPatchError | Will be raised if the input JsonPatch is not valid according to JSON Patch (RFC6902). |
other errors | Check SONiC Generic Configuration Update and Rollback - HLD for exact list of errors to expect. | |
side-effects | None | |
assumptions | running-config locked | The implementor of this contract might interact with ConfigDB to get the running-config, it is assumed the running-config is locked for changes for the lifespan of the operation. |
The important constraint in the above interface is the JsonChange, where the application of a single JsonChange does not guarantee any ordering. If ordering is needed, multiple JsonChanges should be returned.
This design document is exploring the option to use YANG models validations in order to verify each config state is valid.
To represent this problem as a graph we will consider the following:
- Nodes are configuration states which should be valid according to YANG models
- Edges are the valid JsonChanges which should ignore any command ordering
The problem now is just finding a path between the current config and the target config. A solution to the above graph might be:
Note:
- Green nodes/edges are valid.
- Red nodes/edges are invalid.
- Grey nodes/edges are not tried.
This problem can be reduced to a constraint satisfaction problem which are usually solved by search in particular backtracking or local search. This means we can have a wide range of algorithms to choose from such as Depth-First Search (DFS), Breadth-First Search (BFS), Dynamic Programming (DP), A*, other search algorithms, greedy approaches, heuristics or a combination of all of these algorithms.
There are at least 2 steps that are needed broadly for any algorithm.
A transition between config state to another config state will be done through JsonChanges.
Each transition should be validated that it does not contain dependent changes. For example: VLAN_MEMBER can contain a reference to PORT table, in that case VLAN_MEMBER cannot be updated together with PORT table in the same JsonChange.
If generating/validating transitions needs additional explicit flags/configs, these flags/configs should be added to YANG models as extensions.
A config state represents how the config looks like after applying a JsonChange. Validation of config state is simply the validation of the generated config using YANG models.
This is going to be part of the apply-patch
functionality described in SONiC Generic Configuration Update and Rollback - HLD
Let's discuss how a simple DFS solution will look like.
def order-patch(patch):
current_config = get_current_config()
target_config = simulate_patch(patch, current_config)
return dfs(current_config, target_config)
visited = {}
def dfs(current_config, target_config):
if current_config == target_config:
return []
if current_config in visited:
return None
visited[curr_config] = True
moves = get_all_moves(current_config)
for move in moves:
if is_valid(move, current_config):
new_config = apply_move(move, current_config)
new_moves = dfs(new_config, target_config)
if new_moves != None:
return [move] + [new_moves]
return None
In the above approach, we are trying all possible moves and just returning the first successful list of moves. Use of Visited
dictionary help cut down states that were already visited.
Gets the current config from ConfigDB, e.g. show runningconfiguration all
Can use jsonpatch python library to do that, e.g. patch.apply(current_config)
This means to include all moves at the lowest level of the JSON tree. For example showing the diff between Target Config and Current Config:
{
"ACL_RULE": {
"SNMP_ACL|RULE_1": {
"PRIORITY": "9999",
- "SRC_IP": "2.2.2.2/21",
+ "SRC_IP": "1.1.1.1/21",
"IP_PROTOCOL": "17",
"PACKET_ACTION": "ACCEPT"
},
- "SNMP_ACL|RULE_2": {
- "PRIORITY": "9998",
- "SRC_IP": "2.2.2.2/20",
- "IP_PROTOCOL": "17",
- "PACKET_ACTION": "ACCEPT"
- },
+ "SNMP_ACL|RULE_3": {
+ "PRIORITY": "9997",
+ "SRC_IP": "3.3.3.3/20",
+ "IP_PROTOCOL": "17",
+ "PACKET_ACTION": "ACCEPT"
+ },
"SNMP_ACL|DEFAULT_RULE": {
"PRIORITY": "1",
"ETHER_TYPE": "2048",
"PACKET_ACTION": "DROP"
}
},
"ACL_TABLE": {
"SNMP_ACL": {
"type": "CTRLPLANE",
"policy_desc": "SNMP_ACL",
"services": [
"SNMP"
]
}
}
}
The lowest levels steps to try in the first run will be: (expressed as JsonPatch operations)
Moves generated for /ACL_RULE/SNMP_ACL|RULE_1
, which corresponds to modifying a leaf key/value pair.
[
{ "op": "replace", "path": "/ACL_RULE/SNMP_ACL|RULE_1/SRC_IP", "value": "1.1.1.1/21" }
]
Moves generated for /ACL_RULE/SNMP_ACL|RULE_2
, which is getting completely removed:
[
{ "op": "remove", "path": "/ACL_RULE/SNMP_ACL|RULE_2/PRIORITY" },
{ "op": "remove", "path": "/ACL_RULE/SNMP_ACL|RULE_2/SRC_IP" },
{ "op": "remove", "path": "/ACL_RULE/SNMP_ACL|RULE_2/IP_PROTOCOL" },
{ "op": "remove", "path": "/ACL_RULE/SNMP_ACL|RULE_2/PACKET_ACTION" }
]
Moves generated for /ACL_RULE/SNMP_ACL|RULE_3
, which is getting completely added:
[
{ "op": "add", "path": "/ACL_RULE/SNMP_ACL|RULE_3", "value": { "PRIORITY": "9997" } },
{ "op": "add", "path": "/ACL_RULE/SNMP_ACL|RULE_3", "value": { "SRC_IP": "2.2.2.2/20" } },
{ "op": "add", "path": "/ACL_RULE/SNMP_ACL|RULE_3", "value": { "IP_PROTOCOL": "17" } },
{ "op": "add", "path": "/ACL_RULE/SNMP_ACL|RULE_3", "value": { "PACKET_ACTION": "ACCEPT" } }
]
Recursively go through the the parent of the moves in the lower level, and try the following:
- If the parent is still the same,
replace
the parent with its children - If the parent is remove,
remove
the parent - If the parent is added,
add
the parent with its children
Move generated for /ACL_RULE/SNMP_ACL|RULE_1
, will be replace
since /ACL_RULE/SNMP_ACL|RULE_1
itself is still the same.
[
{ "op": "replace", "path": "/ACL_RULE/SNMP_ACL|RULE_1", "value": { "PRIORITY": "9999", "SRC_IP": "1.1.1.1/21", "IP_PROTOCOL": "17","PACKET_ACTION": "ACCEPT" } }
]
Move generated for /ACL_RULE/SNMP_ACL|RULE_2
, will be remove
since /ACL_RULE/SNMP_ACL|RULE_2
itself is removed.
[
{ "op": "remove", "path": "/ACL_RULE/SNMP_ACL|RULE_2"}
]
Move generated for /ACL_RULE/SNMP_ACL|RULE_3
, will be add
since /ACL_RULE/SNMP_ACL|RULE_3
itself is added.
[
{ "op": "add", "path": "/ACL_RULE/SNMP_ACL|RULE_3", "value": { "PRIORITY": "9997", "SRC_IP": "3.3.3.3/20", "IP_PROTOCOL": "17","PACKET_ACTION": "ACCEPT" } }
]
Next we recursively go up the tree, this includes at the level of /ACL_RULE
table, and the whole config. Since both already exist the operation would be replace
. Note that "path": ""
where the path is empty represents the whole config.
[
{ "op": "replace", "path": "/ACL_RULE", "value": { "SNMP_ACL|RULE_1": { "PRIORITY": "9999", "SRC_IP": "1.1.1.1/21", "IP_PROTOCOL": "17", "PACKET_ACTION": "ACCEPT" }, "SNMP_ACL|RULE_3": { "PRIORITY": "9997", "SRC_IP": "3.3.3.3/20", "IP_PROTOCOL": "17", "PACKET_ACTION": "ACCEPT"}, "SNMP_ACL|DEFAULT_RULE": { "PRIORITY": "1", "ETHER_TYPE": "2048", "PACKET_ACTION": "DROP" } }},
{ "op": "replace", "path": "", "value": { "ACL_RULE": { "SNMP_ACL|RULE_1": { "PRIORITY": "9999", "SRC_IP": "1.1.1.1/21", "IP_PROTOCOL": "17", "PACKET_ACTION": "ACCEPT" }, "SNMP_ACL|RULE_3": { "PRIORITY": "9997", "SRC_IP": "3.3.3.3/20", "IP_PROTOCOL": "17", "PACKET_ACTION": "ACCEPT"}, "SNMP_ACL|DEFAULT_RULE": { "PRIORITY": "1", "ETHER_TYPE": "2048", "PACKET_ACTION": "DROP" } }, "ACL_TABLE": { "SNMP_ACL": { "type": "CTRLPLANE", "policy_desc": "SNMP_ACL", "services": [ "SNMP" ] } } } }
]
replace
operation can be substituted byremove
, which should be put back in the next level of recursion of the DFS, for example:Will generate:[ { "op": "replace", "path": "/ACL_RULE/SNMP_ACL|RULE_1", "value": { "PRIORITY": "9999", "SRC_IP": "1.1.1.1/21", "IP_PROTOCOL": "17","PACKET_ACTION": "ACCEPT" } } ]
[ { "op": "remove", "path": "/ACL_RULE/SNMP_ACL|RULE_1" } ]
- Lines getting removed which have references, can be substituted by deleting their references first as it is required anyway, for example:
Will generate the following assuming PORT Ethernet2 is consumed by ACL_TABLE and VLAN_MEMBER:
[ { "op": "remove", "path": "/PORT/Ethernet2" } ]
[ { "op": "remove", "path": "/ACL_TABLE/NO-NSW-PACL-V4/ports/0" }, { "op": "remove", "path": "/VLAN_MEMBER/Vlan100|Ethernet2" } ]
- There can be other moves as the quality of the moves will determine if there is a path to get to the Target Config from the Current Config.
- Validates result config after applying the move against SONiC YANG models.
- Validates the operation itself
- If we don't validate the operation, then we can always just replace the whole current_config with the whole target_config in one operation. This will always produce a valid result config.
- Validating the operation makes sure it does not contain 2 changes that have dependency as JsonChange appliers do not take of ordering.
- If ordering is necessary, the operation needs to be split into multiple JsonChanges.
A small modification to the DFS algorithm listed before is introducing memoization, where the function is optimizing for the shortest number of moves.
def order-patch(patch):
current_config = get_current_config()
target_config = simulate_patch(patch, current_config)
return rec(current_config, target_config)
visited = {}
mem = {}
def rec(current_config, target_config):
if current_config == target_config:
return []
if current_config in mem: # If already calculated, just return the result form memory
return mem[current_config]
if current_config in visited:
return None
visited[curr_config] = True
moves = get_all_moves(current_config)
best_moves = None
for move in moves:
if is_valid(move, current_config):
new_config = apply_move(move, current_config)
new_moves = rec(new_config, target_config)
if new_moves != None:
# Minimize on the number of moves
if best_moves == None or len(best_moves) > 1 + len(new_moves):
best_moves = [move] + [new_moves]
mem[current_config] = best_moves
return best_moves
This optimization can help reduce the number of result JsonChanges, and ultimately reduce the time needed to completely apply all the JsonChanges.
SONiC YANG models are going to be used for the following:
- Validating config state after applying a move
- Generating other moves
- Host flags/configs for move validation
This is a straightforward task and can be done using SONiC Libynag wrapper, specifically validate_data_tree
.
Check 2.2.3 Generating all possible moves to learn more about what are the possible moves.
YANG models can help with:
- Lines getting removed which have references, can be substituted by deleting their references first as it is required anyway (check 2.2.3.3 Generating Other Moves for details). Getting the dependency of some data can be done using SONiC Libynag wrapper, specifically
find_data_dependencies
. - Any other moves should be generated using YANG models, if YANG models data are not enough an extension to YANG models can be added to help with generating new moves.
Some fields are not allowed to be modified and can only be created, such as /PORT/Ethernet0/lanes
which is marked as CREATE_ONLY
in opencomputeproject/SAI/blob/master/inc/saiport.h:
/**
* @brief Hardware Lane list
*
* @type sai_u32_list_t
* @flags MANDATORY_ON_CREATE | CREATE_ONLY | KEY
*/
SAI_PORT_ATTR_HW_LANE_LIST,
The new extension create-only
will be added to YANG models SONiC extensions:
extension create-only {
description "During apply-patch operation the field can only be created (i.e. added) or re-created (i.e. removed then added) but cannot be modified (i.e. replaced).";
}
create-only
field moves will be evaluated as follows:
- Fail validation for
replace
move that is updating a field marked withcreate-only
. - Fail validation for
replace
move that is updating parent of a field that is marked withcreate-only
and thecreate-only
field is itself different. - Pass validation for
replace
move that is updating parent of a field that is marked withcreate-only
and thecreate-only
field is itself the same. - Rule 2,3 above will be also applied to the grand-parent, grand-grand-parent ...etc
Let's take the example A.2 Dynamic Port Breakout 1 to 4 which has the following:
[
{
"op": "replace",
"path": "/PORT/Ethernet0/lanes",
"value": "65"
}
]
But the field lanes
is create-only
and cannot be modified i.e. replaced. The only way to update the lanes
field is through deleting then adding its parent the /PORT/Ethernet0
.
In port YANG model, let's mark lanes
with create-only
:
leaf lanes {
mandatory true;
+ ext:create-only;
type string {
length 1..128;
}
}
Now following the DFS algorithm described above:
-
Generating all possible moves which includes low level moves, upper level moves and other moves. We will end up with:
[ { "op": "replace", "path": "/PORT/Ethernet0/lanes", "value": "65" }, { "op": "delete", "path": "/PORT/Ethernet0/lanes" }, { "op": "replace", "path": "/PORT/Ethernet0", "value": {"alias": "Eth1/1", "lanes": "65", "description": "", "speed": "10000"} }, { "op": "delete", "path": "/PORT/Ethernet0" } ]
-
Validating the moves and result config states after applying the moves
A.
{ "op": "replace", "path": "/PORT/Ethernet0/lanes", "value": "65" }
the move validation will fail because a
replace
move is applied to a the fieldlanes
which iscreate-only
B.
{ "op": "delete", "path": "/PORT/Ethernet0/lanes" }
the generated config validation will fail because the field
lanes
ismandatory
and cannot be deleted.C.
{ "op": "replace", "path": "/PORT/Ethernet0", "value": {"alias": "Eth1/1", "lanes": "65", "description": "", "speed": "10000"} },
the move validation will fail because a
replace
move is applied the/PORT/Ethernet0
which is a parent tolanes
which is acreate-only
field, and alsolanes
field is differentD.
{ "op": "delete", "path": "/PORT/Ethernet0" }
the move validation will pass.
N/A
check JsonChange
N/A
N/A
Check here for formal list of errors defined for the interface order-patch
.
All commands logs are stored in systemd-journal and syslog.
N/A
N/A
Test Case | Description |
---|---|
1 | Add a new table. |
2 | Remove an existing table. |
3 | Modify values of an existing table entry. |
4 | Modify value of an existing item an array value. |
5 | Add a new item to an array value. |
6 | Remove an item form an array value. |
7 | Add a new key to an existing table . |
8 | Remove a key from an existing table. |
9 | Remove 2 items that depends on each other but in different tables e.g. /PORT/Ethernet2 and /VLAN_MEMBER/Vlan101 |
10 | Add 2 items that depends on each other but in different tables e.g. /PORT/Ethernet2 and /VLAN_MEMBER/Vlan101 |
11 | Remove 2 items that depends on each other in the same table e.g. /INTERFACE/INTERFACE_LIST and /INTERFACE/INTERFACE_PREFIX_LIST. |
12 | Add 2 items that depends on each other in the same table e.g. /INTERFACE/INTERFACE_LIST and /INTERFACE/INTERFACE_PREFIX_LIST. |
13 | Replace a mandatory item e.g. type under ACL_TABLE. |
14 | Dynamic port breakout as described here. |
15 | Remove an item that has a default value. |
16 | Modifying items that rely depends on each other based on a must condition rather than direct connection such as leafref e.g. /CRM/acl_counter_high_threshold (check here). |
Current running config
{
"DHCP_SERVER": {
"192.0.0.1": {},
"192.0.0.2": {},
"192.0.0.3": {},
"192.0.0.4": {}
}
}
JsonPatch
[
{
"path": "/DHCP_SERVER/192.0.0.5",
"op": "add",
"value": {}
}
]
Result Jsonchanges
[
[{"op": "add", "path": "/DHCP_SERVER/192.0.0.5", "value": {}}]
]
Current running config
{
"PORT": {
"Ethernet0": {
"alias": "Eth1",
"lanes": "65, 66, 67, 68",
"description": "Ethernet0 100G link",
"speed": "100000"
}
},
"ACL_TABLE": {
"NO-NSW-PACL-V4": {
"type": "L3",
"policy_desc": "NO-NSW-PACL-V4",
"ports": [
"Ethernet0"
]
}
},
"VLAN_MEMBER": {
"Vlan100|Ethernet0": {
"tagging_mode": "untagged"
}
}
}
JsonPatch
[
{
"op": "add",
"path": "/PORT/Ethernet3",
"value": {
"alias": "Eth1/4",
"lanes": "68",
"description": "",
"speed": "10000"
}
},
{
"op": "add",
"path": "/PORT/Ethernet1",
"value": {
"alias": "Eth1/2",
"lanes": "66",
"description": "",
"speed": "10000"
}
},
{
"op": "add",
"path": "/PORT/Ethernet2",
"value": {
"alias": "Eth1/3",
"lanes": "67",
"description": "",
"speed": "10000"
}
},
{
"op": "replace",
"path": "/PORT/Ethernet0/lanes",
"value": "65"
},
{
"op": "replace",
"path": "/PORT/Ethernet0/alias",
"value": "Eth1/1"
},
{
"op": "replace",
"path": "/PORT/Ethernet0/description",
"value": ""
},
{
"op": "replace",
"path": "/PORT/Ethernet0/speed",
"value": "10000"
},
{
"op": "add",
"path": "/VLAN_MEMBER/Vlan100|Ethernet2",
"value": {
"tagging_mode": "untagged"
}
},
{
"op": "add",
"path": "/VLAN_MEMBER/Vlan100|Ethernet3",
"value": {
"tagging_mode": "untagged"
}
},
{
"op": "add",
"path": "/VLAN_MEMBER/Vlan100|Ethernet1",
"value": {
"tagging_mode": "untagged"
}
},
{
"op": "add",
"path": "/ACL_TABLE/NO-NSW-PACL-V4/ports/1",
"value": "Ethernet1"
},
{
"op": "add",
"path": "/ACL_TABLE/NO-NSW-PACL-V4/ports/2",
"value": "Ethernet2"
},
{
"op": "add",
"path": "/ACL_TABLE/NO-NSW-PACL-V4/ports/3",
"value": "Ethernet3"
}
]
Result Jsonchanges
[
[{"op": "remove", "path": "/ACL_TABLE/NO-NSW-PACL-V4/ports"}],
[{"op": "remove", "path": "/VLAN_MEMBER/Vlan100|Ethernet0"}],
[{"op": "remove", "path": "/PORT/Ethernet0"}],
[{"op": "add", "path": "/PORT/Ethernet1", "value": {"alias": "Eth1/2", "lanes": "66", "description": "", "speed": "10000"}}],
[{"op": "add", "path": "/PORT/Ethernet2", "value": {"alias": "Eth1/3", "lanes": "67", "description": "", "speed": "10000"}}],
[{"op": "add", "path": "/PORT/Ethernet0", "value": {"alias": "Eth1/1", "lanes": "65", "description": "", "speed": "10000"}}],
[{"op": "add", "path": "/PORT/Ethernet3", "value": {"alias": "Eth1/4", "lanes": "68", "description": "", "speed": "10000"}}],
[{"op": "add", "path": "/ACL_TABLE/NO-NSW-PACL-V4/ports", "value": ["Ethernet0", "Ethernet1", "Ethernet2", "Ethernet3"]}],
[{"op": "add", "path": "/VLAN_MEMBER/Vlan100|Ethernet1", "value": {"tagging_mode": "untagged"}}],
[{"op": "add", "path": "/VLAN_MEMBER/Vlan100|Ethernet2", "value": {"tagging_mode": "untagged"}}],
[{"op": "add", "path": "/VLAN_MEMBER/Vlan100|Ethernet3", "value": {"tagging_mode": "untagged"}}],
[{"op": "add", "path": "/VLAN_MEMBER/Vlan100|Ethernet0", "value": {"tagging_mode": "untagged"}}],
]