diff --git a/generic_config_updater/gu_common.py b/generic_config_updater/gu_common.py index 6ed9699f26..e3b866814d 100644 --- a/generic_config_updater/gu_common.py +++ b/generic_config_updater/gu_common.py @@ -273,7 +273,13 @@ def create_path(self, tokens): return JsonPointer.from_parts(tokens).path def has_path(self, doc, path): - return JsonPointer(path).get(doc, default=None) is not None + return self.get_from_path(doc, path) is not None + + def get_from_path(self, doc, path): + return JsonPointer(path).get(doc, default=None) + + def is_config_different(self, path, current, target): + return self.get_from_path(current, path) != self.get_from_path(target, path) def get_xpath_tokens(self, xpath): """ @@ -548,7 +554,8 @@ def _get_xpath_tokens_from_leaf(self, model, token_index, path_tokens, config): # Source: Check examples in https://netopeer.liberouter.org/doc/libyang/master/html/howto_x_path.html return [f"{token}[.='{value}']"] - raise ValueError("Token not found") + raise ValueError(f"Path token not found.\n model: {model}\n token_index: {token_index}\n " + \ + f"path_tokens: {path_tokens}\n config: {config}") def _extractKey(self, tableKey, keys): keyList = keys.split() @@ -712,7 +719,8 @@ def _get_path_tokens_from_leaf(self, model, token_index, xpath_tokens, config): list_idx = list_config.index(leaf_list_value) return [leaf_list_name, list_idx] - raise Exception("no leaf") + raise ValueError(f"Xpath token not found.\n model: {model}\n token_index: {token_index}\n " + \ + f"xpath_tokens: {xpath_tokens}\n config: {config}") def _extract_key_dict(self, list_token): # Example: VLAN_MEMBER_LIST[name='Vlan1000'][port='Ethernet8'] diff --git a/generic_config_updater/patch_sorter.py b/generic_config_updater/patch_sorter.py index f7a5b37efe..3ed1eb3a37 100644 --- a/generic_config_updater/patch_sorter.py +++ b/generic_config_updater/patch_sorter.py @@ -1,14 +1,14 @@ import copy import json import jsonpatch -from collections import deque +from collections import deque, OrderedDict from enum import Enum from .gu_common import OperationWrapper, OperationType, GenericConfigUpdaterError, \ JsonChange, PathAddressing, genericUpdaterLogging class Diff: """ - A class that contains the diff info between current and target configs. + A class that contains the diff info between current and target configs. """ def __init__(self, current_config, target_config): self.current_config = current_config @@ -114,7 +114,7 @@ def _to_jsonpatch_add_operation(diff, current_config_tokens, target_config_token } Assume JsonMove: op_type=add, current_config_tokens=[dict1, key11], target_config_tokens=[dict1, key11] - + Converting this to operation directly would result in: {"op":"add", "path":"/dict1/key11", "value":"value11"} BUT this is not correct since 'dict1' which does not exist in Current Config. @@ -333,6 +333,189 @@ def _extend_moves(self, move, diff): for newmove in extender.extend(move, diff): yield newmove +class JsonPointerFilter: + """ + A filtering class to get the paths matching the filter from the given config. + The patterns: + - Each pattern consist of multiple tokens + - Tokens are matched with the config from the root level + - Each token can be: + - '*' Will match all keys at the current level + - '@' Will be replaced by 'common_key' passed in 'get_paths' + - Will match other strings that are not * nor @ + - Token advanced options + - '*|@' Will match keys that end with '|common_key' + - '@|*' Will match keys that start with 'common_key|' + - '*|' Will match keys that end with '|' + - '|*' Will match keys that start with '|' + """ + def __init__(self, patterns, path_addressing): + self.patterns = patterns + self.path_addressing = path_addressing + + def get_paths(self, config, common_key=None): + for pattern in self.patterns: + for path in self._get_paths_recursive(config, pattern, [], 0, common_key): + yield path + + def _get_paths_recursive(self, config, pattern_tokens, matching_tokens, idx, common_key): + if idx == len(pattern_tokens): + yield self.path_addressing.create_path(matching_tokens) + return + + token = pattern_tokens[idx] + if common_key: + token = token.replace("@", common_key) + + matching_keys = [] + if token == "*": + matching_keys = config.keys() + elif token.startswith("*|"): + suffix = token[2:] + matching_keys = [key for key in config.keys() if key.endswith(suffix)] + elif token.endswith("|*"): + prefix = token[:-2] + matching_keys = [key for key in config.keys() if key.startswith(prefix)] + elif token in config: + matching_keys = [token] + + for key in matching_keys: + matching_tokens.append(key) + for path in self._get_paths_recursive(config[key], pattern_tokens, matching_tokens, idx+1, common_key): + yield path + matching_tokens.pop() + + def is_match(self, path): + tokens = self.path_addressing.get_path_tokens(path) + for pattern in self.patterns: + if len(pattern) != len(tokens): + return False + + for idx in range(len(pattern)): + pattern_token = pattern[idx] + token = tokens[idx] + + if not self._is_token_match(pattern_token, token): + return False + + return True + + def _is_token_match(self, pattern_token, token): + if "|" in pattern_token: + pattern_token_parts = pattern_token.split("|", 1) + token_parts = token.split("|", 1) + if len(pattern_token_parts) != len(token_parts): + return False + + return self._is_simple_token_match(pattern_token_parts[0], token_part[0]) and \ + self._is_simple_token_match(pattern_token_parts[1], token_part[1]) + + return self._is_simple_token_match(pattern_token, token) + + + def _is_simple_token_match(self, pattern_token, token): + if pattern_token == "*" or pattern_token == "@": + return True + + return pattern_token == token + +class RequiredValueIdentifier: + """ + A class that identifies the config that requires other fields to be of specific value + The "requiring" config is the config that requires other fields to be of specific value. + The "required" config is the confing that needs to be of specific value. + E.g. Changes to "QUEUE" table requires the corresponding "PORT" to be admin down. + """ + def __init__(self, path_addressing): + # TODO: port-critical fields are hard-coded for now, it should be moved to YANG models + # settings format, each setting consist of: + # [ + # "required_pattern": the list of tokens, where there is a single token with value '@' which is the common key + # with the requiring patterns + # "required_value": the required value + # "default_value": the default value of the required paths + # "requiring_patterns": the patterns matching paths that requires the given value, each pattern can have '@' + # which will be replaced with the common key, '*' will match any symbol + # } + self.settings = [ + { + "required_pattern": ["PORT", "@", "admin_status"], + "required_value": "down", + "default_value": "down", + "requiring_patterns": [ + ["BUFFER_PG", "@|*"], + ["BUFFER_PORT_EGRESS_PROFILE_LIST", "@"], + ["BUFFER_PORT_INGRESS_PROFILE_LIST", "@"], + ["BUFFER_QUEUE", "@|*"], + ["PORT_QOS_MAP", "@"], + ["QUEUE", "@|*"], + ] + }, + ] + self.path_addressing = path_addressing + for setting in self.settings: + required_pattern = setting["required_pattern"] + required_parent_pattern = required_pattern[:-1] + # replace the '@' with '*' so it can be used as a JsonPointerFilter + required_parent_pattern_with_asterisk = [token.replace("@", "*") for token in required_parent_pattern] + setting["required_parent_filter"] = JsonPointerFilter([required_parent_pattern_with_asterisk], path_addressing) + setting["required_field_name"] = required_pattern[-1] + for index, token in enumerate(required_pattern): + if token == "@": + setting["common_key_index"] = index + setting["requiring_filter"] = JsonPointerFilter(setting["requiring_patterns"], path_addressing) + + + def get_required_value_data(self, configs): + data = {} + for setting in self.settings: + required_parent_filter = setting["required_parent_filter"] + required_field_name = setting["required_field_name"] + common_key_index = setting["common_key_index"] + required_value = setting["required_value"] + requiring_filter = setting["requiring_filter"] + for config in configs: + for required_parent_path in required_parent_filter.get_paths(config): + parent_tokens = self.path_addressing.get_path_tokens(required_parent_path) + required_path = self.path_addressing.create_path(parent_tokens+[required_field_name]) + common_key = parent_tokens[common_key_index] + requires_paths = requiring_filter.get_paths(config, common_key) + for requires_path in requires_paths: + if requires_path not in data: + data[requires_path] = set() + data[requires_path].add((required_path, required_value)) + + sorted_paths = sorted(data.keys()) + sorted_data = OrderedDict() + for path in sorted_paths: + sorted_data[path] = sorted(data[path]) + + return sorted_data + + def get_value_or_default(self, config, path): + value = self.path_addressing.get_from_path(config, path) + if value is not None: + return value + + # Check if parent exist + tokens = self.path_addressing.get_path_tokens(path) + parent_tokens = tokens[:-1] + field_name = tokens[-1] + parent_path = self.path_addressing.create_path(parent_tokens) + parent_value = self.path_addressing.get_from_path(config, parent_path) + + if parent_value is None: + return None + + return self._get_default_value_from_settings(parent_path, field_name) + + def _get_default_value_from_settings(self, parent_path, field_name): + for setting in self.settings: + if setting["required_parent_filter"].is_match(parent_path) and field_name == setting["required_field_name"]: + return setting["default_value"] + + return None + class DeleteWholeConfigMoveValidator: """ A class to validate not deleting whole config as it is not supported by JsonPatch lib. @@ -388,19 +571,18 @@ def __init__(self, path_addressing): self.path_addressing = path_addressing # TODO: create-only fields are hard-coded for now, it should be moved to YANG models - # Each pattern consist of a list of tokens. Token matching starts from the root level of the config. - # Each token is either a specific key or '*' to match all keys. - self.create_only_patterns = [ - ["PORT", "*", "lanes"], - ["LOOPBACK_INTERFACE", "*", "vrf_name"], - ["BGP_NEIGHBOR", "*", "holdtime"], - ["BGP_NEIGHBOR", "*", "keepalive"], - ["BGP_NEIGHBOR", "*", "name"], - ["BGP_NEIGHBOR", "*", "asn"], - ["BGP_NEIGHBOR", "*", "local_addr"], - ["BGP_NEIGHBOR", "*", "nhopself"], - ["BGP_NEIGHBOR", "*", "rrclient"], - ] + self.create_only_filter = JsonPointerFilter([ + ["PORT", "*", "lanes"], + ["LOOPBACK_INTERFACE", "*", "vrf_name"], + ["BGP_NEIGHBOR", "*", "holdtime"], + ["BGP_NEIGHBOR", "*", "keepalive"], + ["BGP_NEIGHBOR", "*", "name"], + ["BGP_NEIGHBOR", "*", "asn"], + ["BGP_NEIGHBOR", "*", "local_addr"], + ["BGP_NEIGHBOR", "*", "nhopself"], + ["BGP_NEIGHBOR", "*", "rrclient"], + ], + path_addressing) def validate(self, move, diff): simulated_config = move.apply(diff.current_config) @@ -443,26 +625,8 @@ def _parent_added_child_not_as_target(self, tokens, current_config, simulated_co return self.path_addressing.has_path(simulated_config, child_path) def _get_create_only_paths(self, config): - for pattern in self.create_only_patterns: - for create_only_path in self._get_create_only_path_recursive(config, pattern, [], 0): - yield create_only_path - - def _get_create_only_path_recursive(self, config, pattern_tokens, matching_tokens, idx): - if idx == len(pattern_tokens): - yield '/' + '/'.join(matching_tokens) - return - - matching_keys = [] - if pattern_tokens[idx] == "*": - matching_keys = config.keys() - elif pattern_tokens[idx] in config: - matching_keys = [pattern_tokens[idx]] - - for key in matching_keys: - matching_tokens.append(key) - for create_only_path in self._get_create_only_path_recursive(config[key], pattern_tokens, matching_tokens, idx+1): - yield create_only_path - matching_tokens.pop() + for path in self.create_only_filter.get_paths(config): + yield path def _value_exist_but_different(self, tokens, current_config_ptr, simulated_config_ptr): for token in tokens: @@ -610,7 +774,7 @@ def _get_paths(self, current_ptr, target_ptr, tokens): tokens.pop() return deleted_paths, added_paths - + # current/target configs are not dict nor list, so handle them as string, int, bool, float if current_ptr != target_ptr: # tokens.append(token) @@ -691,6 +855,63 @@ def _validate_table(self, table, config): # the only invalid case is if table exists and is empty return table not in config or config[table] +class RequiredValueMoveValidator: + """ + Check RequiredValueIdentifier class description first. + + The validator checks the following: + - A move that is changing a requiring config, while the required path is not equal to the required value is rejected + E.g. A move that is changing "QUEUE" table while the corresponding "PORT" is not admin down is rejected + - A move that is changing the required path value to something other than the required value, while there are + requiring changes left is rejected + E.g. A move is changing "PORT" to admin up from down, while "QUEUE" table still have changes left is rejected. + """ + def __init__(self, path_addressing): + self.path_addressing = path_addressing + self.identifier = RequiredValueIdentifier(path_addressing) + + def validate(self, move, diff): + # ignore full config removal because it is not possible by JsonPatch lib + if move.op_type == OperationType.REMOVE and move.path == "": + return + + current_config = diff.current_config + simulated_config = move.apply(current_config) # Config after applying just this move + target_config = diff.target_config # Final config after applying whole patch + + # data dictionary: + # { + # : [(required_path, required_value), ...], + # ... + # } + data = self.identifier.get_required_value_data([current_config, simulated_config, target_config]) + + # If move is changing a requiring path while the required path does not have the required value, reject the move + # E.g. if the move is changing port-critical configs while the port is up, reject the move + for path in data: + if self.path_addressing.is_config_different(path, current_config, simulated_config): + for required_path, required_value in data[path]: + actual_value = self.identifier.get_value_or_default(current_config, required_path) + if actual_value is None: # current config does not have this value at all + continue + if actual_value != required_value: + return False + + # If some changes to the requiring paths are still to take place and the move has changes + # to the required path, reject the move + # E.g. if there are still port-critical changes left and the move has changes to the port + # admin status, reject the move + # This makes sure we don't change the required path unnecessarily. + for path in data: + if self.path_addressing.is_config_different(path, current_config, target_config): + for required_path, required_value in data[path]: + current_value = self.identifier.get_value_or_default(current_config, required_path) + simulated_value = self.identifier.get_value_or_default(simulated_config, required_path) + if current_value != simulated_value and simulated_value != required_value: + return False + + return True + class LowLevelMoveGenerator: """ A class to generate the low level moves i.e. moves corresponding to differences between current/target config @@ -759,7 +980,7 @@ def _traverse(self, current_ptr, target_ptr, current_tokens, target_tokens): yield move current_tokens.pop() target_tokens.pop() - + return # The current/target ptr are neither dict nor list, so they might be string, int, float, bool @@ -911,6 +1132,99 @@ def _list_to_dict_with_count(self, items): return counts +class RequiredValueMoveExtender: + """ + Check RequiredValueIdentifier class description first. + + The extender does the following: + - If the move that is changing a requiring config, while the required path is not equal to the required value, then + generate a move to turn the required path to the required value. + E.g. A move that is changing "QUEUE" table while the corresponding "PORT" is not admin down, then generate + a move to turn the "PORT" to admin down. + - If a move that is changing the required path value to something other than the required value, while there are + requiring changes left, then flip all the required paths in the move to the required value. + E.g. A move is changing "PORT" to admin up from down, while "QUEUE" table still have changes left, then flip + the "PORT" to admin down in the move. + """ + def __init__(self, path_addressing, operation_wrapper): + self.path_addressing = path_addressing + self.identifier = RequiredValueIdentifier(path_addressing) + self.operation_wrapper = operation_wrapper + + def extend(self, move, diff): + # ignore full config removal because it is not possible by JsonPatch lib + if move.op_type == OperationType.REMOVE and move.path == "": + return + + current_config = diff.current_config + simulated_config = move.apply(current_config) # Config after applying just this move + target_config = diff.target_config # Final config after applying whole patch + + # data dictionary: + # { + # : [(required_path, required_value), ...], + # ... + # } + data = self.identifier.get_required_value_data([current_config, simulated_config, target_config]) + + # If move is changing a requiring path while the required path does not have the required value, + # flip the required path to the required value + # E.g. if the move is changing port-critical config while the port is admin up, create a move to + # turn the port admin down + processed_moves = set() + for path in data: + if self.path_addressing.is_config_different(path, current_config, simulated_config): + for required_path, required_value in data[path]: + actual_value = self.identifier.get_value_or_default(current_config, required_path) + if actual_value is None: # current config does not have this value at all + continue + if actual_value != required_value: + extended_move = JsonMove.from_operation({"op":"replace", "path":required_path, "value":required_value}) + if extended_move not in processed_moves: + processed_moves.add(extended_move) + yield extended_move + + # If some changes to the requiring paths are still to take place and the move has changes + # to the required path, flip the required path to the required value. + # E.g. if there are still port-critical changes left and the move has changes to the port + # admin status, flip the port to admin down in the move + # This makes sure we don't change the required path unnecessarily. + flip_path_value_tuples = set() + for path in data: + if self.path_addressing.is_config_different(path, current_config, target_config): + for required_path, required_value in data[path]: + current_value = self.identifier.get_value_or_default(current_config, required_path) + simulated_value = self.identifier.get_value_or_default(simulated_config, required_path) + if current_value != simulated_value and simulated_value != required_value: + flip_path_value_tuples.add((required_path, required_value)) + + if flip_path_value_tuples: + extended_move = self._flip(move, flip_path_value_tuples) + yield extended_move + + def _flip(self, move, flip_path_value_tuples): + new_value = copy.deepcopy(move.value) + move_tokens = self.path_addressing.get_path_tokens(move.path) + for field_path, field_value in flip_path_value_tuples: + field_tokens = self.path_addressing.get_path_tokens(field_path) + new_value = self._change_value(field_tokens, field_value, move_tokens, new_value) + + operation = self.operation_wrapper.create(move.op_type, move.path, new_value) + return JsonMove.from_operation(operation) + + def _change_value(self, field_tokens, field_value, move_tokens, move_value): + rem_tokens = field_tokens[len(move_tokens):] + if not rem_tokens: + return field_value + + move_value_ptr = move_value + for token in rem_tokens[:-1]: + move_value_ptr = move_value_ptr[token] + + last_token = rem_tokens[-1] + move_value_ptr[last_token] = field_value + return move_value + class UpperLevelMoveExtender: """ A class to extend the given move by including its parent. It has 3 cases: @@ -1084,7 +1398,8 @@ def __init__(self, operation_wrapper, config_wrapper, path_addressing): def create(self, algorithm=Algorithm.DFS): move_generators = [LowLevelMoveGenerator(self.path_addressing)] - move_extenders = [UpperLevelMoveExtender(), + move_extenders = [RequiredValueMoveExtender(self.path_addressing, self.operation_wrapper), + UpperLevelMoveExtender(), DeleteInsteadOfReplaceMoveExtender(), DeleteRefsMoveExtender(self.path_addressing)] move_validators = [DeleteWholeConfigMoveValidator(), @@ -1092,6 +1407,7 @@ def create(self, algorithm=Algorithm.DFS): NoDependencyMoveValidator(self.path_addressing, self.config_wrapper), UniqueLanesMoveValidator(), CreateOnlyMoveValidator(self.path_addressing), + RequiredValueMoveValidator(self.path_addressing), NoEmptyTableMoveValidator(self.path_addressing)] move_wrapper = MoveWrapper(move_generators, move_extenders, move_validators) diff --git a/tests/generic_config_updater/files/config_db_with_port_critical.json b/tests/generic_config_updater/files/config_db_with_port_critical.json new file mode 100644 index 0000000000..5853bfe5ea --- /dev/null +++ b/tests/generic_config_updater/files/config_db_with_port_critical.json @@ -0,0 +1,49 @@ +{ + "PORT": { + "Ethernet4": { + "admin_status": "up", + "alias": "fortyGigE0/4", + "description": "Servers0:eth0", + "index": "1", + "lanes": "29,30,31,32", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000" + }, + "Ethernet8": { + "admin_status": "up", + "alias": "fortyGigE0/8", + "description": "Servers1:eth0", + "index": "2", + "lanes": "33,34,35,36", + "pfc_asym": "off", + "speed": "40000" + }, + "Ethernet12": { + "admin_status": "down", + "alias": "fortyGigE0/12", + "description": "Servers2:eth0", + "index": "3", + "lanes": "37,38,39,40", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000" + }, + "Ethernet16": { + "alias": "fortyGigE0/16", + "description": "Servers3:eth0", + "index": "4", + "lanes": "41,42,43,44", + "pfc_asym": "off", + "speed": "40000" + } + }, + "BUFFER_PG": { + "Ethernet4|0": { + "profile": "ingress_lossy_profile" + }, + "Ethernet12|0": { + "profile": "ingress_lossy_profile" + } + } +} diff --git a/tests/generic_config_updater/files/patch_sorter_test_success.json b/tests/generic_config_updater/files/patch_sorter_test_success.json index 11e061d091..894f68896c 100644 --- a/tests/generic_config_updater/files/patch_sorter_test_success.json +++ b/tests/generic_config_updater/files/patch_sorter_test_success.json @@ -2974,5 +2974,1800 @@ } ] ] + }, + "PORT_CRITICAL_CHANGE_AND_PORT_IS_ADMIN_UP": { + "desc": "Port critical change and the port is already admin up", + "current_config": { + "PORT": { + "Ethernet4": { + "admin_status": "up", + "alias": "fortyGigE0/4", + "description": "Servers0:eth0", + "index": "1", + "lanes": "29,30,31,32", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000" + } + }, + "BUFFER_PROFILE": { + "egress_lossy_profile": { + "dynamic_th": "3", + "pool": "egress_lossy_pool", + "size": "1518" + } + }, + "BUFFER_POOL": { + "egress_lossy_pool": { + "mode": "dynamic", + "size": "7326924", + "type": "egress" + } + } + }, + "patch": [ + { + "op": "add", + "path": "/BUFFER_PG", + "value": { + "Ethernet4|0": { + "profile": "egress_lossy_profile" + } + } + } + ], + "expected_changes": [ + [ + { + "op": "replace", + "path": "/PORT/Ethernet4/admin_status", + "value": "down" + } + ], + [ + { + "op": "add", + "path": "/BUFFER_PG", + "value": { + "Ethernet4|0": { + "profile": "egress_lossy_profile" + } + } + } + ], + [ + { + "op": "replace", + "path": "/PORT/Ethernet4/admin_status", + "value": "up" + } + ] + ] + }, + "PORT_CRITICAL_CONFIG_AND_ADDING_ADMIN_UP_PORT": { + "desc": "Port critical change and the port with admin up is added in the same patch.", + "current_config": { + "BUFFER_PROFILE": { + "egress_lossy_profile": { + "dynamic_th": "3", + "pool": "egress_lossy_pool", + "size": "1518" + } + }, + "BUFFER_POOL": { + "egress_lossy_pool": { + "mode": "dynamic", + "size": "7326924", + "type": "egress" + } + } + }, + "patch": [ + { + "op": "add", + "path": "/PORT", + "value": { + "Ethernet4": { + "admin_status": "up", + "alias": "fortyGigE0/4", + "description": "Servers0:eth0", + "index": "1", + "lanes": "29,30,31,32", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000" + } + } + }, + { + "op": "add", + "path": "/BUFFER_PG", + "value": { + "Ethernet4|0": { + "profile": "egress_lossy_profile" + } + } + } + ], + "expected_changes": [ + [ + { + "op": "add", + "path": "/PORT", + "value": { + "Ethernet4": { + "admin_status": "down", + "alias": "fortyGigE0/4", + "description": "Servers0:eth0", + "index": "1", + "lanes": "29,30,31,32", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000" + } + } + } + ], + [ + { + "op": "add", + "path": "/BUFFER_PG", + "value": { + "Ethernet4|0": { + "profile": "egress_lossy_profile" + } + } + } + ], + [ + { + "op": "replace", + "path": "/PORT/Ethernet4/admin_status", + "value": "up" + } + ] + ] + }, + "ADD_RACK": { + "desc": "Add a rack and all its related settings", + "current_config": { + "ACL_TABLE": { + "EVERFLOW": { + "policy_desc": "EVERFLOW", + "ports": [ + "Ethernet68", + "Ethernet72" + ], + "stage": "ingress", + "type": "MIRROR" + }, + "EVERFLOWV6": { + "policy_desc": "EVERFLOWV6", + "ports": [ + "Ethernet68", + "Ethernet72" + ], + "stage": "ingress", + "type": "MIRRORV6" + } + }, + "BGP_NEIGHBOR": { + "10.0.0.1": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.0", + "name": "ARISTA01T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.5": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.4", + "name": "ARISTA03T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.9": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.8", + "name": "ARISTA05T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.13": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.12", + "name": "ARISTA07T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.17": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.16", + "name": "ARISTA09T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.21": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.20", + "name": "ARISTA11T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.25": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.24", + "name": "ARISTA13T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.29": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.28", + "name": "ARISTA15T2", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.35": { + "asn": "64002", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.34", + "name": "ARISTA02T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.37": { + "asn": "64003", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.36", + "name": "ARISTA03T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.39": { + "asn": "64004", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.38", + "name": "ARISTA04T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.41": { + "asn": "64005", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.40", + "name": "ARISTA05T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.43": { + "asn": "64006", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.42", + "name": "ARISTA06T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.45": { + "asn": "64007", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.44", + "name": "ARISTA07T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.47": { + "asn": "64008", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.46", + "name": "ARISTA08T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.49": { + "asn": "64009", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.48", + "name": "ARISTA09T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.51": { + "asn": "64010", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.50", + "name": "ARISTA10T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.53": { + "asn": "64011", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.52", + "name": "ARISTA11T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.55": { + "asn": "64012", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.54", + "name": "ARISTA12T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.57": { + "asn": "64013", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.56", + "name": "ARISTA13T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.59": { + "asn": "64014", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.58", + "name": "ARISTA14T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.61": { + "asn": "64015", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.60", + "name": "ARISTA15T0", + "nhopself": "0", + "rrclient": "0" + }, + "10.0.0.63": { + "asn": "64016", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.62", + "name": "ARISTA16T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::1a": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::19", + "name": "ARISTA07T2", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::2": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::1", + "name": "ARISTA01T2", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::2a": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::29", + "name": "ARISTA11T2", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::3a": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::39", + "name": "ARISTA15T2", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::4a": { + "asn": "64003", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::49", + "name": "ARISTA03T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::4e": { + "asn": "64004", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::4d", + "name": "ARISTA04T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::5a": { + "asn": "64007", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::59", + "name": "ARISTA07T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::5e": { + "asn": "64008", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::5d", + "name": "ARISTA08T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::6a": { + "asn": "64011", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::69", + "name": "ARISTA11T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::6e": { + "asn": "64012", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::6d", + "name": "ARISTA12T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::7a": { + "asn": "64015", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::79", + "name": "ARISTA15T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::7e": { + "asn": "64016", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::7d", + "name": "ARISTA16T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::12": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::11", + "name": "ARISTA05T2", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::22": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::21", + "name": "ARISTA09T2", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::32": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::31", + "name": "ARISTA13T2", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::46": { + "asn": "64002", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::45", + "name": "ARISTA02T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::52": { + "asn": "64005", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::51", + "name": "ARISTA05T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::56": { + "asn": "64006", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::55", + "name": "ARISTA06T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::62": { + "asn": "64009", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::61", + "name": "ARISTA09T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::66": { + "asn": "64010", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::65", + "name": "ARISTA10T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::72": { + "asn": "64013", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::71", + "name": "ARISTA13T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::76": { + "asn": "64014", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::75", + "name": "ARISTA14T0", + "nhopself": "0", + "rrclient": "0" + }, + "fc00::a": { + "asn": "65200", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::9", + "name": "ARISTA03T2", + "nhopself": "0", + "rrclient": "0" + } + }, + "BUFFER_PG": { + "Ethernet68|0": { + "profile": "ingress_lossy_profile" + }, + "Ethernet68|3-4": { + "profile": "pg_lossless_40000_40m_profile" + }, + "Ethernet72|0": { + "profile": "ingress_lossy_profile" + }, + "Ethernet72|3-4": { + "profile": "pg_lossless_40000_40m_profile" + } + }, + "BUFFER_POOL": { + "egress_lossless_pool": { + "mode": "static", + "size": "12766208", + "type": "egress" + }, + "egress_lossy_pool": { + "mode": "dynamic", + "size": "7326924", + "type": "egress" + }, + "ingress_lossless_pool": { + "mode": "dynamic", + "size": "12766208", + "type": "ingress" + } + }, + "BUFFER_PROFILE": { + "egress_lossless_profile": { + "pool": "egress_lossless_pool", + "size": "0", + "static_th": "12766208" + }, + "egress_lossy_profile": { + "dynamic_th": "3", + "pool": "egress_lossy_pool", + "size": "1518" + }, + "ingress_lossy_profile": { + "dynamic_th": "3", + "pool": "ingress_lossless_pool", + "size": "0" + }, + "pg_lossless_40000_40m_profile": { + "dynamic_th": "-3", + "pool": "ingress_lossless_pool", + "size": "56368", + "xoff": "55120", + "xon": "18432", + "xon_offset": "2496" + }, + "pg_lossless_40000_300m_profile": { + "dynamic_th": "-3", + "pool": "ingress_lossless_pool", + "size": "56368", + "xoff": "55120", + "xon": "18432", + "xon_offset": "2496" + } + }, + "BUFFER_QUEUE": { + "Ethernet52|0-2": { + "profile": "egress_lossy_profile" + }, + "Ethernet52|3-4": { + "profile": "egress_lossless_profile" + }, + "Ethernet52|5-6": { + "profile": "egress_lossy_profile" + }, + "Ethernet56|0-2": { + "profile": "egress_lossy_profile" + }, + "Ethernet56|3-4": { + "profile": "egress_lossless_profile" + }, + "Ethernet56|5-6": { + "profile": "egress_lossy_profile" + }, + "Ethernet60|0-2": { + "profile": "egress_lossy_profile" + }, + "Ethernet60|3-4": { + "profile": "egress_lossless_profile" + }, + "Ethernet60|5-6": { + "profile": "egress_lossy_profile" + }, + "Ethernet68|0-2": { + "profile": "egress_lossy_profile" + }, + "Ethernet68|3-4": { + "profile": "egress_lossless_profile" + }, + "Ethernet68|5-6": { + "profile": "egress_lossy_profile" + }, + "Ethernet72|0-2": { + "profile": "egress_lossy_profile" + }, + "Ethernet72|3-4": { + "profile": "egress_lossless_profile" + }, + "Ethernet72|5-6": { + "profile": "egress_lossy_profile" + } + }, + "DEVICE_NEIGHBOR": { + "Ethernet52": { + "name": "ARISTA13T2", + "port": "Ethernet2" + }, + "Ethernet56": { + "name": "ARISTA15T2", + "port": "Ethernet1" + }, + "Ethernet60": { + "name": "ARISTA15T2", + "port": "Ethernet2" + }, + "Ethernet68": { + "name": "ARISTA02T0", + "port": "Ethernet1" + }, + "Ethernet72": { + "name": "ARISTA03T0", + "port": "Ethernet1" + } + }, + "DSCP_TO_TC_MAP": { + "AZURE": { + "0": "1", + "1": "1", + "10": "1", + "11": "1", + "12": "1", + "13": "1", + "14": "1", + "15": "1", + "16": "1", + "17": "1", + "18": "1", + "19": "1", + "2": "1", + "20": "1", + "21": "1", + "22": "1", + "23": "1", + "24": "1", + "25": "1", + "26": "1", + "27": "1", + "28": "1", + "29": "1", + "3": "3", + "30": "1", + "31": "1", + "32": "1", + "33": "1", + "34": "1", + "35": "1", + "36": "1", + "37": "1", + "38": "1", + "39": "1", + "4": "4", + "40": "1", + "41": "1", + "42": "1", + "43": "1", + "44": "1", + "45": "1", + "46": "5", + "47": "1", + "48": "6", + "49": "1", + "5": "2", + "50": "1", + "51": "1", + "52": "1", + "53": "1", + "54": "1", + "55": "1", + "56": "1", + "57": "1", + "58": "1", + "59": "1", + "6": "1", + "60": "1", + "61": "1", + "62": "1", + "63": "1", + "7": "1", + "8": "0", + "9": "1" + } + }, + "INTERFACE": { + "Ethernet68": {}, + "Ethernet68|10.0.0.34/31": {}, + "Ethernet68|FC00::45/126": {}, + "Ethernet72": {}, + "Ethernet72|10.0.0.36/31": {}, + "Ethernet72|FC00::49/126": {} + }, + "MAP_PFC_PRIORITY_TO_QUEUE": { + "AZURE": { + "0": "0", + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7" + } + }, + "PORT": { + "Ethernet52": { + "admin_status": "up", + "alias": "fortyGigE0/52", + "description": "ARISTA13T2:Ethernet2", + "index": "13", + "lanes": "49,50,51,52", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000", + "tpid": "0x8100" + }, + "Ethernet56": { + "admin_status": "up", + "alias": "fortyGigE0/56", + "description": "ARISTA15T2:Ethernet1", + "index": "14", + "lanes": "57,58,59,60", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000", + "tpid": "0x8100" + }, + "Ethernet60": { + "admin_status": "up", + "alias": "fortyGigE0/60", + "description": "ARISTA15T2:Ethernet2", + "index": "15", + "lanes": "61,62,63,64", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000", + "tpid": "0x8100" + }, + "Ethernet64": { + "alias": "fortyGigE0/64", + "description": "fortyGigE0/64", + "index": "16", + "lanes": "69,70,71,72", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000", + "tpid": "0x8100" + }, + "Ethernet68": { + "admin_status": "up", + "alias": "fortyGigE0/68", + "description": "ARISTA02T0:Ethernet1", + "index": "17", + "lanes": "65,66,67,68", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000", + "tpid": "0x8100" + }, + "Ethernet72": { + "admin_status": "up", + "alias": "fortyGigE0/72", + "description": "ARISTA03T0:Ethernet1", + "index": "18", + "lanes": "73,74,75,76", + "mtu": "9100", + "pfc_asym": "off", + "speed": "40000", + "tpid": "0x8100" + } + }, + "PORT_QOS_MAP": { + "Ethernet52": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + }, + "Ethernet56": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + }, + "Ethernet60": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + }, + "Ethernet68": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + }, + "Ethernet72": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + } + }, + "TC_TO_PRIORITY_GROUP_MAP": { + "AZURE": { + "0": "0", + "1": "0", + "2": "0", + "3": "3", + "4": "4", + "5": "0", + "6": "0", + "7": "7" + } + }, + "TC_TO_QUEUE_MAP": { + "AZURE": { + "0": "0", + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7" + } + } + }, + "patch": [ + { + "op": "add", + "path": "/PORT/Ethernet64/admin_status", + "value": "up" + }, + { + "op": "replace", + "path": "/PORT/Ethernet64/description", + "value": "ARISTA01T0:Ethernet1" + }, + { + "op": "add", + "path": "/BUFFER_QUEUE/Ethernet64|0-2", + "value": { + "profile": "egress_lossy_profile" + } + }, + { + "op": "add", + "path": "/BUFFER_QUEUE/Ethernet64|3-4", + "value": { + "profile": "egress_lossless_profile" + } + }, + { + "op": "add", + "path": "/BUFFER_QUEUE/Ethernet64|5-6", + "value": { + "profile": "egress_lossy_profile" + } + }, + { + "op": "add", + "path": "/ACL_TABLE/EVERFLOWV6/ports/0", + "value": "Ethernet64" + }, + { + "op": "add", + "path": "/ACL_TABLE/EVERFLOW/ports/0", + "value": "Ethernet64" + }, + { + "op": "add", + "path": "/INTERFACE/Ethernet64", + "value": {} + }, + { + "op": "add", + "path": "/INTERFACE/Ethernet64|10.0.0.32~131", + "value": {} + }, + { + "op": "add", + "path": "/INTERFACE/Ethernet64|FC00::41~1126", + "value": {} + }, + { + "op": "add", + "path": "/PORT_QOS_MAP/Ethernet64", + "value": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + } + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.33", + "value": { + "admin_status": "up", + "asn": "64001", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.32", + "name": "ARISTA01T0", + "nhopself": "0", + "rrclient": "0" + } + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::42", + "value": { + "admin_status": "up", + "asn": "64001", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::41", + "name": "ARISTA01T0", + "nhopself": "0", + "rrclient": "0" + } + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::6a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.1/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.17/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::56/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::62/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::76/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.41/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.25/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::5e/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::52/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::7a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.35/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.63/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.53/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::32/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::22/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.9/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::6e/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.29/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.47/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.21/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::72/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.13/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.5/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.39/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.61/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.55/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.37/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::5a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::66/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::46/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::4e/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::2a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::2/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.49/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.59/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::1a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::12/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::4a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.45/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::7e/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.43/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::3a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.51/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.57/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::a/admin_status", + "value": "up" + }, + { + "op": "add", + "path": "/DEVICE_NEIGHBOR/Ethernet64", + "value": { + "name": "ARISTA01T0", + "port": "Ethernet1" + } + }, + { + "op": "add", + "path": "/BUFFER_PG/Ethernet64|3-4", + "value": { + "profile": "pg_lossless_40000_40m_profile" + } + }, + { + "op": "add", + "path": "/BUFFER_PG/Ethernet64|0", + "value": { + "profile": "ingress_lossy_profile" + } + } + ], + "expected_changes": [ + [ + { + "op": "add", + "path": "/ACL_TABLE/EVERFLOW/ports/0", + "value": "Ethernet64" + } + ], + [ + { + "op": "add", + "path": "/ACL_TABLE/EVERFLOWV6/ports/0", + "value": "Ethernet64" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.1/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.5/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.9/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.13/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.17/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.21/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.25/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.29/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.35/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.37/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.39/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.41/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.43/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.45/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.47/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.49/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.51/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.53/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.55/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.57/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.59/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.61/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.63/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::1a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::2/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::2a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::3a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::4a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::4e/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::5a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::5e/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::6a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::6e/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::7a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::7e/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::12/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::22/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::32/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::46/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::52/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::56/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::62/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::66/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::72/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::76/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::a/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BUFFER_PG/Ethernet64|3-4", + "value": { + "profile": "pg_lossless_40000_40m_profile" + } + } + ], + [ + { + "op": "add", + "path": "/BUFFER_PG/Ethernet64|0", + "value": { + "profile": "ingress_lossy_profile" + } + } + ], + [ + { + "op": "add", + "path": "/BUFFER_QUEUE/Ethernet64|0-2", + "value": { + "profile": "egress_lossy_profile" + } + } + ], + [ + { + "op": "add", + "path": "/BUFFER_QUEUE/Ethernet64|3-4", + "value": { + "profile": "egress_lossless_profile" + } + } + ], + [ + { + "op": "add", + "path": "/BUFFER_QUEUE/Ethernet64|5-6", + "value": { + "profile": "egress_lossy_profile" + } + } + ], + [ + { + "op": "add", + "path": "/DEVICE_NEIGHBOR/Ethernet64", + "value": { + "name": "ARISTA01T0" + } + } + ], + [ + { + "op": "add", + "path": "/DEVICE_NEIGHBOR/Ethernet64/port", + "value": "Ethernet1" + } + ], + [ + { + "op": "add", + "path": "/INTERFACE/Ethernet64", + "value": {} + } + ], + [ + { + "op": "add", + "path": "/INTERFACE/Ethernet64|10.0.0.32~131", + "value": {} + } + ], + [ + { + "op": "add", + "path": "/INTERFACE/Ethernet64|FC00::41~1126", + "value": {} + } + ], + [ + { + "op": "replace", + "path": "/PORT/Ethernet64/description", + "value": "ARISTA01T0:Ethernet1" + } + ], + [ + { + "op": "add", + "path": "/PORT_QOS_MAP/Ethernet64", + "value": { + "dscp_to_tc_map": "AZURE" + } + } + ], + [ + { + "op": "add", + "path": "/PORT_QOS_MAP/Ethernet64/pfc_enable", + "value": "3,4" + } + ], + [ + { + "op": "add", + "path": "/PORT_QOS_MAP/Ethernet64/pfc_to_queue_map", + "value": "AZURE" + } + ], + [ + { + "op": "add", + "path": "/PORT_QOS_MAP/Ethernet64/tc_to_pg_map", + "value": "AZURE" + } + ], + [ + { + "op": "add", + "path": "/PORT_QOS_MAP/Ethernet64/tc_to_queue_map", + "value": "AZURE" + } + ], + [ + { + "op": "add", + "path": "/PORT/Ethernet64/admin_status", + "value": "up" + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/10.0.0.33", + "value": { + "admin_status": "up", + "asn": "64001", + "holdtime": "10", + "keepalive": "3", + "local_addr": "10.0.0.32", + "name": "ARISTA01T0", + "nhopself": "0", + "rrclient": "0" + } + } + ], + [ + { + "op": "add", + "path": "/BGP_NEIGHBOR/fc00::42", + "value": { + "admin_status": "up", + "asn": "64001", + "holdtime": "10", + "keepalive": "3", + "local_addr": "fc00::41", + "name": "ARISTA01T0", + "nhopself": "0", + "rrclient": "0" + } + } + ] + ] } } \ No newline at end of file diff --git a/tests/generic_config_updater/patch_sorter_test.py b/tests/generic_config_updater/patch_sorter_test.py index 41805b698f..3b6fcfe691 100644 --- a/tests/generic_config_updater/patch_sorter_test.py +++ b/tests/generic_config_updater/patch_sorter_test.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import jsonpatch import unittest from unittest.mock import MagicMock, Mock @@ -669,6 +670,84 @@ def test_simulate__applies_move(self): # Assert self.assertIs(self.any_diff, actual) +class TestRequiredValueIdentifier(unittest.TestCase): + def test_hard_coded_required_value_data(self): + identifier = ps.RequiredValueIdentifier(PathAddressing()) + config = { + "BUFFER_PG": { + "Ethernet4|0": { + "profile": "ingress_lossy_profile" + }, + "Ethernet8|3-4": { + "profile": "pg_lossless_40000_40m_profile" + } + }, + "BUFFER_QUEUE": { + "Ethernet0|5-6": { + "profile": "egress_lossless_profile" + }, + "Ethernet4|1": { + "profile": "egress_lossy_profile" + } + }, + "QUEUE": { + "Ethernet4|2": { + "scheduler": "scheduler.0" + }, + "Ethernet4|7-8": { + "scheduler": "scheduler.0" + } + }, + "BUFFER_PORT_INGRESS_PROFILE_LIST": { + "Ethernet0": { + "profile_list": ["ingress_lossy_profile"] + }, + "Ethernet4": { + "profile_list": ["ingress_lossy_profile"] + }, + }, + "BUFFER_PORT_EGRESS_PROFILE_LIST": { + "Ethernet4": { + "profile_list": ["egress_lossless_profile", "egress_lossy_profile"] + }, + "Ethernet8": { + "profile_list": ["ingress_lossy_profile"] + }, + }, + "PORT_QOS_MAP": { + "Ethernet4": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + }, + "Ethernet12": { + "dscp_to_tc_map": "AZURE", + "pfc_enable": "3,4", + "pfc_to_queue_map": "AZURE", + "tc_to_pg_map": "AZURE", + "tc_to_queue_map": "AZURE" + }, + }, + "PORT": { + "Ethernet4": {} + } + } + expected = OrderedDict([ + ('/BUFFER_PG/Ethernet4|0', [('/PORT/Ethernet4/admin_status', 'down')]), + ('/BUFFER_PORT_EGRESS_PROFILE_LIST/Ethernet4', [('/PORT/Ethernet4/admin_status', 'down')]), + ('/BUFFER_PORT_INGRESS_PROFILE_LIST/Ethernet4', [('/PORT/Ethernet4/admin_status', 'down')]), + ('/BUFFER_QUEUE/Ethernet4|1', [('/PORT/Ethernet4/admin_status', 'down')]), + ('/PORT_QOS_MAP/Ethernet4', [('/PORT/Ethernet4/admin_status', 'down')]), + ('/QUEUE/Ethernet4|2', [('/PORT/Ethernet4/admin_status', 'down')]), + ('/QUEUE/Ethernet4|7-8', [('/PORT/Ethernet4/admin_status', 'down')]), + ]) + + actual = identifier.get_required_value_data([config]) + + self.assertEqual(expected, actual) + class TestDeleteWholeConfigMoveValidator(unittest.TestCase): def setUp(self): self.operation_wrapper = OperationWrapper() @@ -1270,6 +1349,472 @@ def test_validate__add_non_empty_table__success(self): # Act and assert self.assertTrue(self.validator.validate(move, diff)) +class TestRequiredValueMoveValidator(unittest.TestCase): + def setUp(self): + self.operation_wrapper = OperationWrapper() + path_addressing = PathAddressing() + self.validator = ps.RequiredValueMoveValidator(path_addressing) + self.validator.identifier.settings[0]["requiring_filter"] = ps.JsonPointerFilter([ + ["BUFFER_PG", "@|*"], + ["PORT", "@", "mtu"] + ], + path_addressing) + + def test_validate__critical_port_change(self): + # Each test format: + # "": { + # "expected": "", + # "config": , + # "move": , + # "target_config": + # } + # Each test is testing different flag of: + # - port-up: if port is up or not in current_config + # - status-changing: if admin_status is changing from any state to another + # - under-port: if the port-critical config is under port + # - port-exist: if the port already exist in current_config + test_cases = self._get_critical_port_change_test_cases() + for test_case_name in test_cases: + with self.subTest(name=test_case_name): + self._run_single_test(test_cases[test_case_name]) + + def _run_single_test(self, test_case): + # Arrange + expected = test_case['expected'] + current_config = test_case['config'] + move = test_case['move'] + target_config = test_case.get('target_config', move.apply(current_config)) + diff = ps.Diff(current_config, target_config) + + # Act and Assert + self.assertEqual(expected, self.validator.validate(move, diff)) + + def _get_critical_port_change_test_cases(self): + # port-up status-changing under-port port-exist verdict + # 1 1 1 1 0 + # 1 1 1 0 invalid - port cannot be up while it does not exist + # 1 1 0 1 0 + # 1 1 0 0 invalid - port cannot be up while it does not exist + # 1 0 1 1 0 + # 1 0 1 0 invalid - port cannot be up while it does not exist + # 1 0 0 1 0 + # 1 0 0 0 invalid - port cannot be up while it does not exist + # 0 1 1 1 0 + # 0 1 1 0 0 can be 1? + # 0 1 0 1 0 + # 0 1 0 0 0 + # 0 0 1 1 1 + # 0 0 1 0 1 + # 0 0 0 1 1 + # 0 0 0 0 invalid - port does not exist anyway + return { + "PORT_UP__STATUS_CHANGING__UNDER_PORT__PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "replace", + "path": "/PORT/Ethernet4", + "value": { # only admin_status and mtu are different + "admin_status": "down", # <== status changing + "alias": "fortyGigE0/4", + "description": "Servers0:eth0", + "index": "1", + "lanes": "29,30,31,32", + "mtu": "9000", # <== critical config under port + "pfc_asym": "off", + "speed": "40000" + } + }) + }, + # cannot test "port-up, status-changing, under-port, not port-exist" + # because if port does not exist, it cannot be admin up + "PORT_UP__STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "replace", + "path": "", + "value": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, + [ + # status-changing + {"op":"replace", "path":"/PORT/Ethernet4/admin_status", "value": "down"}, + # port-critical config is not under port + {"op":"replace", "path":"/BUFFER_PG/Ethernet4|0/profile", "value": "egress_lossy_profile"}, + ]) + }) + }, + # cannot test "port-up, status-changing, not under-port, not port-exist" + # because if port does not exist, it cannot be admin up + "PORT_UP__NOT_STATUS_CHANGING__UNDER_PORT__PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "replace", + "path": "/PORT/Ethernet4/mtu", + "value": "9000" + }) + }, + # cannot test "port-up, not status-changing, under-port, not port-exist" + # because if port does not exist, it cannot be admin up + "PORT_UP__NOT_STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op":"replace", + "path":"/BUFFER_PG/Ethernet4|0/profile", + "value": "egress_lossy_profile" + }) + }, + # cannot test "port-up, not status-changing, not under-port, not port-exist" + # because if port does not exist, it cannot be admin up + "NOT_PORT_UP__STATUS_CHANGING__UNDER_PORT__PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet12", + "value": { + "admin_status": "up", # <== status changing + "alias": "fortyGigE0/12", + "description": "Servers2:eth0", + "index": "3", + "lanes": "37,38,39,40", + "mtu": "9000", # <== critical config under port + "pfc_asym": "off", + "speed": "40000" + } + }) + }, + "NOT_PORT_UP__STATUS_CHANGING__UNDER_PORT__NOT_PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet20", + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "mtu": "9100", # <== critical config under port + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }) + }, + "NOT_PORT_UP__STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "replace", + "path": "", + "value": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, + [ + # status-changing + {"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value": "up"}, + # port-critical config is not under port + {"op":"replace", "path":"/BUFFER_PG/Ethernet4|0/profile", "value": "egress_lossy_profile"}, + ] + ) + }) + }, + "NOT_PORT_UP__STATUS_CHANGING__NOT_UNDER_PORT__NOT_PORT_EXIST": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "replace", + "path": "", + "value": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, + [ + # status-changing + { + "op":"add", + "path":"/PORT/Ethernet20", + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e. "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }, + # port-critical config is not under port + { + "op":"add", + "path":"/BUFFER_PG/Ethernet20|0", + "value": { + "profile": "ingress_lossy_profile" + } + }, + ] + ) + }) + }, + "NOT_PORT_UP__NOT_STATUS_CHANGING__UNDER_PORT__PORT_EXIST": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "replace", + "path": "/PORT/Ethernet12/mtu", + "value": "9000" + }) + }, + "NOT_PORT_UP__NOT_STATUS_CHANGING__UNDER_PORT__NOT_PORT_EXIST": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet20", + "value": { + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "mtu": "9100", # <== critical config under port + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }) + }, + "NOT_PORT_UP__NOT_STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op":"replace", + "path":"/BUFFER_PG/Ethernet12|0/profile", + "value": "egress_lossy_profile" + }) + }, + # cannot test "not port-up, not status-changing, not under-port, not port-exist" + # because if port does not exist, it cannot be admin up + + # The following set of cases check validation failure when a move is turning a port admin up, while still + # some critical changes not included at all in the move + "NOT_PORT_UP__STATUS_CHANGING__UNDER_PORT__PORT_EXIST__NOT_ALL_CRITICAL_CHANGES_INCLUDED": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "target_config": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, + [ + # The following critical change is not part of the move below. + {"op": "replace", "path": "/PORT/Ethernet12/mtu", "value": "9000"} + ]), + "move": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value": "up"}) + }, + "NOT_PORT_UP__STATUS_CHANGING__UNDER_PORT__NOT_PORT_EXIST__NOT_ALL_CRITICAL_CHANGES_INCLUDED": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "target_config": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, + [ + { + "op": "add", + "path": "/PORT/Ethernet20", + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "mtu": "9100", # <== critical config under port which is not part of the move below + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + } + ]), + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet20", + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + # "mtu": "9100", # <== critical change is left out + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }) + }, + "NOT_PORT_UP__STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST__NOT_ALL_CRITICAL_CHANGES_INCLUDED": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "target_config": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, + [ + # The following critical change is not part of the move below + {"op":"replace", "path":"/BUFFER_PG/Ethernet12|0/profile", "value": "egress_lossy_profile"}, + ]), + "move": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value": "up"}) + }, + "NOT_PORT_UP__STATUS_CHANGING__NOT_UNDER_PORT__NOT_PORT_EXIST__NOT_ALL_CRITICAL_CHANGES_INCLUDED": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "target_config": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, + [ + { + "op":"add", + "path":"/BUFFER_PG/Ethernet20|0", + "value": { + "profile": "ingress_lossy_profile" + } + }, + { + "op": "add", + "path": "/PORT/Ethernet20", + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + } + ]), + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet20", + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + # "mtu": "9100", # <== critical change is left out + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }) + }, + # Additional cases trying different operation to port-critical config i.e. REPLACE, REMOVE, ADD + "PORT_UP__NOT_STATUS_CHANGING__UNDER_PORT__PORT_EXIST__REMOVE": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "remove", + "path": "/PORT/Ethernet4/mtu" + }) + }, + "PORT_UP__NOT_STATUS_CHANGING__UNDER_PORT__PORT_EXIST__ADD": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet8/mtu", + "value": "9000" + }) + }, + "PORT_UP__NOT_STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST__REMOVE": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op":"remove", + "path":"/BUFFER_PG/Ethernet4|0/profile" + }) + }, + "PORT_UP__NOT_STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST__ADD": { + "expected": False, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op":"add", + "path":"/BUFFER_PG/Ethernet8|0", + "value":{ + "profile": "ingress_lossy_profile" + } + }) + }, + "NOT_PORT_UP__NOT_STATUS_CHANGING__UNDER_PORT__PORT_EXIST__REMOVE": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "remove", + "path": "/PORT/Ethernet12/mtu" + }) + }, + "NOT_PORT_UP__NOT_STATUS_CHANGING__UNDER_PORT__PORT_EXIST__ADD": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet16/mtu", + "value": "9000" + }) + }, + "NOT_PORT_UP__NOT_STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST__REMOVE": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op":"remove", + "path":"/BUFFER_PG/Ethernet12|0" + }) + }, + "NOT_PORT_UP__NOT_STATUS_CHANGING__NOT_UNDER_PORT__PORT_EXIST__ADD": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op":"add", + "path":"/BUFFER_PG/Ethernet16|0", + "value": { + "profile": "ingress_lossy_profile" + } + }) + }, + } + + def test_validate__no_critical_port_changes(self): + # Each test format: + # "": { + # "expected": "", + # "config": , + # "move": , + # "target_config": + # } + # Each test is testing different flag of: + # - port-up: if port is up or not in current_config + # - status-changing: if admin_status is changing from any state to another + # - under-port: if the port-critical config is under port + # - port-exist: if the port already exist in current_config + test_cases = self._get_no_critical_port_change_test_cases() + for test_case_name in test_cases: + with self.subTest(name=test_case_name): + self._run_single_test(test_cases[test_case_name]) + + def _get_no_critical_port_change_test_cases(self): + return { + "REPLACE_NON_CRITICAL_CONFIG": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "replace", + "path": "/PORT/Ethernet4/description", + "value": "desc4" + }) + }, + "REMOVE_NON_CRITICAL_CONFIG": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "remove", + "path": "/PORT/Ethernet4/description" + }) + }, + "ADD_NON_CRITICAL_CONFIG": { + "expected": True, + "config": Files.CONFIG_DB_WITH_PORT_CRITICAL, + "move": ps.JsonMove.from_operation({ + "op": "add", + "path": "/PORT/Ethernet8/description", + "value": "desc8" + }) + }, + } + + def _apply_operations(self, config, operations): + return jsonpatch.JsonPatch(operations).apply(config) + class TestLowLevelMoveGenerator(unittest.TestCase): def setUp(self): path_addressing = PathAddressing() @@ -1522,6 +2067,405 @@ def get_diff(self, target_config_ops = None, current_config_ops = None): return ps.Diff(current_config, target_config) +class TestRequiredValueMoveExtender(unittest.TestCase): + def setUp(self): + path_addressing = PathAddressing() + self.extender = ps.RequiredValueMoveExtender(path_addressing, OperationWrapper()) + self.extender.identifier.settings[0]["requiring_filter"] = ps.JsonPointerFilter([ + ["BUFFER_PG", "@|*"], + ["PORT", "@", "mtu"] + ], + path_addressing) + + def test_extend__remove_whole_config__no_extended_moves(self): + # Arrange + move = ps.JsonMove.from_operation({"op":"remove", "path":""}) + diff = ps.Diff(Files.ANY_CONFIG_DB, Files.ANY_OTHER_CONFIG_DB) + expected = [] + + # Act + actual = self.extender.extend(move, diff) + + # Assert + self._verify_moves(expected, actual) + + def test_extend__port_up_and_no_critical_move__no_extended_moves(self): + # Arrange + move = ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet4/description", "value":"desc4"}) + current_config = Files.CONFIG_DB_WITH_PORT_CRITICAL + target_config = move.apply(current_config) + diff = ps.Diff(current_config, target_config) + expected = [] + + # Act + actual = self.extender.extend(move, diff) + + # Assert + self._verify_moves(expected, actual) + + def test_extend__port_up_and_critical_move__turn_admin_status_down(self): + # Arrange + move = ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet4/mtu", "value":"9000"}) + current_config = Files.CONFIG_DB_WITH_PORT_CRITICAL + target_config = move.apply(current_config) + diff = ps.Diff(current_config, target_config) + expected = [{"op":"replace", "path":"/PORT/Ethernet4/admin_status", "value":"down"}] + + # Act + actual = self.extender.extend(move, diff) + + # Assert + self._verify_moves(expected, actual) + + def test_extend__port_turn_up_and_no_critical_move__no_extended_moves(self): + # Arrange + move = ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value":"up"}) + current_config = Files.CONFIG_DB_WITH_PORT_CRITICAL + target_config = move.apply(current_config) + diff = ps.Diff(current_config, target_config) + expected = [] + + # Act + actual = self.extender.extend(move, diff) + + # Assert + self._verify_moves(expected, actual) + + def test_extend__port_turn_up_and_critical_move__flip_to_turn_down(self): + # Arrange + move = ps.JsonMove.from_operation({ + "op":"replace", + "path":"/PORT/Ethernet12", + "value":{ + "admin_status": "up", # <== Turn admin_status up + "alias": "fortyGigE0/12", + "description": "Servers2:eth0", + "index": "3", + "lanes": "37,38,39,40", + "mtu": "9000", # <== Critical move + "pfc_asym": "off", + "speed": "40000" + }, + }) + current_config = Files.CONFIG_DB_WITH_PORT_CRITICAL + target_config = move.apply(current_config) + diff = ps.Diff(current_config, target_config) + expected = [{ + "op":"replace", + "path":"/PORT/Ethernet12", + "value":{ + "admin_status": "down", # <== Leave admin_status as down + "alias": "fortyGigE0/12", + "description": "Servers2:eth0", + "index": "3", + "lanes": "37,38,39,40", + "mtu": "9000", # <== Critical move + "pfc_asym": "off", + "speed": "40000" + }, + }] + + # Act + actual = self.extender.extend(move, diff) + + # Assert + self._verify_moves(expected, actual) + + def test_extend__multi_port_turn_up_and_critical_move__multi_flip_to_turn_down(self): + # Arrange + current_config = Files.CONFIG_DB_WITH_PORT_CRITICAL + target_config = self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, [ + {"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value":"up"}, + {"op":"add", "path":"/PORT/Ethernet16/admin_status", "value":"up"}, + {"op":"add", "path":"/PORT/Ethernet12/mtu", "value":"9000"}, + # Will not be part of the move, only in the final target config + {"op":"add", "path":"/PORT/Ethernet16/mtu", "value":"9000"}, + ]) + move = ps.JsonMove.from_operation({ + "op":"replace", + "path":"/PORT", + # Following value is for the PORT part of the config + "value": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL["PORT"], [ + {"op":"replace", "path":"/Ethernet12/admin_status", "value":"up"}, + {"op":"add", "path":"/Ethernet16/admin_status", "value":"up"}, + {"op":"add", "path":"/Ethernet12/mtu", "value":"9000"}, + ]) + }) + diff = ps.Diff(current_config, target_config) + expected = [{ + "op":"replace", + "path":"/PORT", + # Following value is for the PORT part of the config + "value": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL["PORT"], [ + {"op":"replace", "path":"/Ethernet12/admin_status", "value":"down"}, + {"op":"add", "path":"/Ethernet16/admin_status", "value":"down"}, + {"op":"add", "path":"/Ethernet12/mtu", "value":"9000"}, + ]) + }] + + # Act + actual = self.extender.extend(move, diff) + + # Assert + self._verify_moves(expected, actual) + + def test_extend__multiple_changes__multiple_extend_moves(self): + # Arrange + current_config = Files.CONFIG_DB_WITH_PORT_CRITICAL + target_config = self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, [ + {"op":"replace", "path":"/BUFFER_PG/Ethernet4|0/profile", "value": "egress_lossy_profile"}, + {"op": "add", "path": "/PORT/Ethernet8/mtu", "value": "9000"}, + { + "op": "add", + "path": "/PORT/Ethernet20", # <== adding a non-existing port + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "mtu": "9100", # <== critical config under port + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }, + {"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value":"up"}, + {"op":"add", "path":"/PORT/Ethernet16/admin_status", "value":"up"}, + {"op":"add", "path":"/PORT/Ethernet12/mtu", "value":"9000"}, + # Will not be part of the move, only in the final target config + {"op":"add", "path":"/PORT/Ethernet16/mtu", "value":"9000"}, + ]) + move = ps.JsonMove.from_operation({ + "op":"replace", + "path":"", + "value": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, [ + {"op":"replace", "path":"/BUFFER_PG/Ethernet4|0/profile", "value": "egress_lossy_profile"}, + {"op": "add", "path": "/PORT/Ethernet8/mtu", "value": "9000"}, + { + "op": "add", + "path": "/PORT/Ethernet20", # <== adding a non-existing port + "value": { + "admin_status": "up", # <== status-changing from not-existing i.e "down" to "up" + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "mtu": "9100", # <== critical config under port + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }, + {"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value":"up"}, + {"op":"add", "path":"/PORT/Ethernet16/admin_status", "value":"up"}, + {"op":"add", "path":"/PORT/Ethernet12/mtu", "value":"9000"}, + ]) + }) + diff = ps.Diff(current_config, target_config) + expected = [ + {"op":"replace", "path":"/PORT/Ethernet4/admin_status", "value":"down"}, + {"op":"replace", "path":"/PORT/Ethernet8/admin_status", "value":"down"}, + { + "op":"replace", + "path":"", + "value": self._apply_operations(Files.CONFIG_DB_WITH_PORT_CRITICAL, [ + {"op":"replace", "path":"/BUFFER_PG/Ethernet4|0/profile", "value": "egress_lossy_profile"}, + {"op": "add", "path": "/PORT/Ethernet8/mtu", "value": "9000"}, + { + "op": "add", + "path": "/PORT/Ethernet20", # <== adding a non-existing port + "value": { + "admin_status": "down", # <== flipping to down admin_status + "alias": "fortyGigE0/20", + "description": "Servers4:eth0", + "index": "5", + "mtu": "9100", # <== critical config under port + "lanes": "45,46,47,48", + "pfc_asym": "off", + "speed": "40000" + } + }, + {"op":"replace", "path":"/PORT/Ethernet12/admin_status", "value":"down"}, + {"op":"add", "path":"/PORT/Ethernet16/admin_status", "value":"down"}, + {"op":"add", "path":"/PORT/Ethernet12/mtu", "value":"9000"}, + ]) + } + ] + + # Act + actual = self.extender.extend(move, diff) + + # Assert + self._verify_moves(expected, actual) + + def _verify_moves(self, ex_ops, moves): + moves_ops = [list(move.patch)[0] for move in moves] + self.assertCountEqual(ex_ops, moves_ops) + + def _apply_operations(self, config, operations): + return jsonpatch.JsonPatch(operations).apply(config) + + def test_flip(self): + test_cases = { + "ADD_ADMIN_STATUS": { + "move": ps.JsonMove.from_operation({"op":"add", "path":"/PORT/Ethernet200/admin_status", "value": "up"}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"add", "path":"/PORT/Ethernet200/admin_status", "value": "down"}) + }, + "ADD_ETHERNET": { + "move": ps.JsonMove.from_operation({"op":"add", "path":"/PORT/Ethernet200", "value": { + "admin_status": "up", + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"add", "path":"/PORT/Ethernet200", "value": { + "admin_status": "down", + }}) + }, + "ADD_ETHERNET_NO_ADMIN_STATUS": { + "move": ps.JsonMove.from_operation({"op":"add", "path":"/PORT/Ethernet200", "value": { + "admin_status": "up", + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"add", "path":"/PORT/Ethernet200", "value": { + "admin_status": "down", + }}) + }, + "ADD_PORT": { + "move": ps.JsonMove.from_operation({"op":"add", "path":"/PORT", "value": { + "Ethernet200":{ + "admin_status": "up", + } + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"add", "path":"/PORT", "value": { + "Ethernet200":{ + "admin_status": "down", + } + }}) + }, + "ADD_WHOLE_CONFIG": { + "move": ps.JsonMove.from_operation({"op":"add", "path":"", "value": { + "PORT": { + "Ethernet200": { + "admin_status": "up", + } + } + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"add", "path":"", "value": { + "PORT": { + "Ethernet200":{ + "admin_status": "down", + } + } + }}), + }, + "ADD_WHOLE_CONFIG_NO_ADMIN_STATUS": { + "move": ps.JsonMove.from_operation({"op":"add", "path":"", "value": { + "PORT": { + "Ethernet200": { + } + } + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"add", "path":"", "value": { + "PORT": { + "Ethernet200":{ + "admin_status": "down", + } + } + }}), + }, + "REPLACE_ADMIN_STATUS": { + "move": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet200/admin_status", "value": "up"}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet200/admin_status", "value": "down"}) + }, + "REPLACE_ETHERNET": { + "move": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet200", "value": { + "admin_status": "up", + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT/Ethernet200", "value": { + "admin_status": "down", + }}) + }, + "REPLACE_PORT": { + "move": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT", "value": { + "Ethernet200":{ + "admin_status": "up", + } + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"replace", "path":"/PORT", "value": { + "Ethernet200":{ + "admin_status": "down", + } + }}) + }, + "REPLACE_WHOLE_CONFIG": { + "move": ps.JsonMove.from_operation({"op":"replace", "path":"", "value": { + "PORT": { + "Ethernet200": { + "admin_status": "up", + } + } + }}), + "port_names": ["Ethernet200"], + "expected": ps.JsonMove.from_operation({"op":"replace", "path":"", "value": { + "PORT": { + "Ethernet200":{ + "admin_status": "down", + } + } + }}), + }, + "MULTIPLE_PORTS" :{ + "move": ps.JsonMove.from_operation({"op":"replace", "path":"", "value": { + "PORT": { + "Ethernet200": { + "admin_status": "up", + }, + "Ethernet300": { + "admin_status": "down", + }, + "Ethernet400": { + "admin_status": "up", + }, + "Ethernet500": { + }, + } + }}), + "port_names": ["Ethernet200", "Ethernet300", "Ethernet400", "Ethernet500"], + "expected": ps.JsonMove.from_operation({"op":"replace", "path":"", "value": { + "PORT": { + "Ethernet200": { + "admin_status": "down", + }, + "Ethernet300": { + "admin_status": "down", + }, + "Ethernet400": { + "admin_status": "down", + }, + "Ethernet500": { + "admin_status": "down", + }, + } + }}), + }, + } + + for test_case_name, test_case in test_cases.items(): + with self.subTest(name=test_case_name): + move = test_case["move"] + port_names = test_case["port_names"] + expected = test_case["expected"] + + path_value_tuples = [(f"/PORT/{port_name}/admin_status", "down") for port_name in port_names] + + actual = self.extender._flip(move, path_value_tuples) + self.assertEqual(expected, actual) + class TestUpperLevelMoveExtender(unittest.TestCase): def setUp(self): self.extender = ps.UpperLevelMoveExtender() @@ -1832,12 +2776,16 @@ def verify(self, algo, algo_class): config_wrapper = ConfigWrapper() factory = ps.SortAlgorithmFactory(OperationWrapper(), config_wrapper, PathAddressing(config_wrapper)) expected_generators = [ps.LowLevelMoveGenerator] - expected_extenders = [ps.UpperLevelMoveExtender, ps.DeleteInsteadOfReplaceMoveExtender, ps.DeleteRefsMoveExtender] + expected_extenders = [ps.RequiredValueMoveExtender, + ps.UpperLevelMoveExtender, + ps.DeleteInsteadOfReplaceMoveExtender, + ps.DeleteRefsMoveExtender] expected_validator = [ps.DeleteWholeConfigMoveValidator, ps.FullConfigMoveValidator, ps.NoDependencyMoveValidator, ps.UniqueLanesMoveValidator, ps.CreateOnlyMoveValidator, + ps.RequiredValueMoveValidator, ps.NoEmptyTableMoveValidator] # Act @@ -1873,6 +2821,9 @@ def test_patch_sorter_success(self): data = Files.PATCH_SORTER_TEST_SUCCESS skip_exact_change_list_match = False for test_case_name in data: + # Skipping ADD RACK case until fixing issue https://github.com/Azure/sonic-utilities/issues/2034 + if test_case_name == "ADD_RACK": + continue with self.subTest(name=test_case_name): self.run_single_success_case(data[test_case_name], skip_exact_change_list_match)