diff --git a/generic_config_updater/field_operation_validators.py b/generic_config_updater/field_operation_validators.py index 84cc48547..72af9c8bd 100644 --- a/generic_config_updater/field_operation_validators.py +++ b/generic_config_updater/field_operation_validators.py @@ -1,10 +1,117 @@ -from sonic_py_common import device_info +import os import re +import json +import jsonpointer +import subprocess +from sonic_py_common import device_info +from .gu_common import GenericConfigUpdaterError + + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +GCU_TABLE_MOD_CONF_FILE = f"{SCRIPT_DIR}/gcu_field_operation_validators.conf.json" +GET_HWSKU_CMD = "sonic-cfggen -d -v DEVICE_METADATA.localhost.hwsku" + +def get_asic_name(): + asic = "unknown" + + if os.path.exists(GCU_TABLE_MOD_CONF_FILE): + with open(GCU_TABLE_MOD_CONF_FILE, "r") as s: + gcu_field_operation_conf = json.load(s) + else: + raise GenericConfigUpdaterError("GCU table modification validators config file not found") + + asic_mapping = gcu_field_operation_conf["helper_data"]["rdma_config_update_validator"] + asic_type = device_info.get_sonic_version_info()['asic_type'] + + if asic_type == 'cisco-8000': + asic = "cisco-8000" + elif asic_type == 'mellanox' or asic_type == 'vs' or asic_type == 'broadcom': + proc = subprocess.Popen(GET_HWSKU_CMD, shell=True, universal_newlines=True, stdout=subprocess.PIPE) + output, err = proc.communicate() + hwsku = output.rstrip('\n') + if asic_type == 'mellanox' or asic_type == 'vs': + spc1_hwskus = asic_mapping["mellanox_asics"]["spc1"] + if hwsku.lower() in [spc1_hwsku.lower() for spc1_hwsku in spc1_hwskus]: + asic = "spc1" + return asic + if asic_type == 'broadcom' or asic_type == 'vs': + broadcom_asics = asic_mapping["broadcom_asics"] + for asic_shorthand, hwskus in broadcom_asics.items(): + if asic != "unknown": + break + for hwsku_cur in hwskus: + if hwsku_cur.lower() in hwsku.lower(): + asic = asic_shorthand + break + + return asic -def rdma_config_update_validator(): - version_info = device_info.get_sonic_version_info() - asic_type = version_info.get('asic_type') - if (asic_type != 'mellanox' and asic_type != 'broadcom' and asic_type != 'cisco-8000'): +def rdma_config_update_validator(patch_element): + asic = get_asic_name() + if asic == "unknown": return False + version_info = device_info.get_sonic_version_info() + build_version = version_info.get('build_version') + version_substrings = build_version.split('.') + branch_version = None + + for substring in version_substrings: + if substring.isdigit() and re.match(r'^\d{8}$', substring): + branch_version = substring + + path = patch_element["path"] + table = jsonpointer.JsonPointer(path).parts[0] + + # Helper function to return relevant cleaned paths, consdiers case where the jsonpatch value is a dict + # For paths like /PFC_WD/Ethernet112/action, remove Ethernet112 from the path so that we can clearly determine the relevant field (i.e. action, not Ethernet112) + def _get_fields_in_patch(): + cleaned_fields = [] + + field_elements = jsonpointer.JsonPointer(path).parts[1:] + cleaned_field_elements = [elem for elem in field_elements if not any(char.isdigit() for char in elem)] + cleaned_field = '/'.join(cleaned_field_elements).lower() + + + if 'value' in patch_element.keys() and isinstance(patch_element['value'], dict): + for key in patch_element['value']: + cleaned_fields.append(cleaned_field+ '/' + key) + else: + cleaned_fields.append(cleaned_field) + + return cleaned_fields + + if os.path.exists(GCU_TABLE_MOD_CONF_FILE): + with open(GCU_TABLE_MOD_CONF_FILE, "r") as s: + gcu_field_operation_conf = json.load(s) + else: + raise GenericConfigUpdaterError("GCU table modification validators config file not found") + + tables = gcu_field_operation_conf["tables"] + scenarios = tables[table]["validator_data"]["rdma_config_update_validator"] + + cleaned_fields = _get_fields_in_patch() + for cleaned_field in cleaned_fields: + scenario = None + for key in scenarios.keys(): + if cleaned_field in scenarios[key]["fields"]: + scenario = scenarios[key] + break + + if scenario is None: + return False + + if scenario["platforms"][asic] == "": + return False + + if patch_element['op'] not in scenario["operations"]: + return False + + if branch_version is not None: + if asic in scenario["platforms"]: + if branch_version < scenario["platforms"][asic]: + return False + else: + return False + return True diff --git a/generic_config_updater/gcu_field_operation_validators.conf.json b/generic_config_updater/gcu_field_operation_validators.conf.json index f12a14d8e..2dcf1649b 100644 --- a/generic_config_updater/gcu_field_operation_validators.conf.json +++ b/generic_config_updater/gcu_field_operation_validators.conf.json @@ -10,11 +10,128 @@ "e.g. 'show.acl.test_acl'", "", "field_operation_validators for a given table defines a list of validators that all must pass for modification to the specified field and table to be allowed", + "", + "validator_data provides data relevant to each validator", "" ], + "helper_data": { + "rdma_config_update_validator": { + "mellanox_asics": { + "spc1": [ "ACS-MSN2700", "ACS-MSN2740", "ACS-MSN2100", "ACS-MSN2410", "ACS-MSN2010", "Mellanox-SN2700", "Mellanox-SN2700-D48C8" ] + }, + "broadcom_asics": { + "th": [ "Force10-S6100", "Arista-7060CX-32S-C32", "Arista-7060CX-32S-C32-T1", "Arista-7060CX-32S-D48C8", "Celestica-DX010-C32", "Seastone-DX010" ], + "th2": [ "Arista-7260CX3-D108C8", "Arista-7260CX3-C64", "Arista-7260CX3-Q64" ], + "td2": [ "Force10-S6000", "Force10-S6000-Q24S32", "Arista-7050-QX32", "Arista-7050-QX-32S", "Nexus-3164", "Arista-7050QX32S-Q32" ], + "td3": [ "Arista-7050CX3-32S-C32", "Arista-7050CX3-32S-D48C8" ] + } + } + }, "tables": { "PFC_WD": { - "field_operation_validators": [ "generic_config_updater.field_operation_validators.rdma_config_update_validator" ] + "field_operation_validators": [ "generic_config_updater.field_operation_validators.rdma_config_update_validator" ], + "validator_data": { + "rdma_config_update_validator": { + "PFCWD enable/disable": { + "fields": [ + "restoration_time", + "detection_time", + "action", + "global/poll_interval" + ], + "operations": ["remove", "add", "replace"], + "platforms": { + "spc1": "20181100", + "td2": "20181100", + "th": "20181100", + "th2": "20181100", + "td3": "20201200", + "cisco-8000": "20201200" + } + } + } + } + }, + "BUFFER_POOL": { + "field_operation_validators": [ "generic_config_updater.field_operation_validators.rdma_config_update_validator" ], + "validator_data": { + "rdma_config_update_validator": { + "Shared/headroom pool size changes": { + "fields": [ + "ingress_lossless_pool/xoff", + "ingress_lossless_pool/size", + "egress_lossy_pool/size" + ], + "operations": ["replace"], + "platforms": { + "spc1": "20191100", + "td2": "", + "th": "20221100", + "th2": "20221100", + "td3": "20221100", + "cisco-8000": "" + } + } + } + } + }, + "BUFFER_PROFILE": { + "field_operation_validators": [ "generic_config_updater.field_operation_validators.rdma_config_update_validator" ], + "validator_data": { + "rdma_config_update_validator": { + "Dynamic threshold tuning": { + "fields": [ + "dynamic_th" + ], + "operations": ["replace"], + "platforms": { + "spc1": "20181100", + "td2": "20181100", + "th": "20181100", + "th2": "20181100", + "td3": "20201200", + "cisco-8000": "" + } + }, + "PG headroom modification": { + "fields": [ + "xoff" + ], + "operations": ["replace"], + "platforms": { + "spc1": "20191100", + "td2": "", + "th": "20221100", + "th2": "20221100", + "td3": "20221100", + "cisco-8000": "" + } + } + } + } + }, + "WRED_PROFILE": { + "field_operation_validators": [ "generic_config_updater.field_operation_validators.rdma_config_update_validator" ], + "validator_data": { + "rdma_config_update_validator": { + "ECN tuning": { + "fields": [ + "azure_lossless/green_min_threshold", + "azure_lossless/green_max_threshold", + "azure_lossless/green_drop_probability" + ], + "operations": ["replace"], + "platforms": { + "spc1": "20181100", + "td2": "20181100", + "th": "20181100", + "th2": "20181100", + "td3": "20201200", + "cisco-8000": "" + } + } + } + } } } } diff --git a/generic_config_updater/gu_common.py b/generic_config_updater/gu_common.py index 77952f6dd..d5146ba93 100644 --- a/generic_config_updater/gu_common.py +++ b/generic_config_updater/gu_common.py @@ -166,7 +166,7 @@ def validate_field_operation(self, old_config, target_config): if any(op['op'] == operation and field == op['path'] for op in patch): raise IllegalPatchOperationError("Given patch operation is invalid. Operation: {} is illegal on field: {}".format(operation, field)) - def _invoke_validating_function(cmd): + def _invoke_validating_function(cmd, jsonpatch_element): # cmd is in the format as . method_name = cmd.split(".")[-1] module_name = ".".join(cmd.split(".")[0:-1]) @@ -174,7 +174,7 @@ def _invoke_validating_function(cmd): raise GenericConfigUpdaterError("Attempting to call invalid method {} in module {}. Module must be generic_config_updater.field_operation_validators, and method must be a defined validator".format(method_name, module_name)) module = importlib.import_module(module_name, package=None) method_to_call = getattr(module, method_name) - return method_to_call() + return method_to_call(jsonpatch_element) if os.path.exists(GCU_FIELD_OP_CONF_FILE): with open(GCU_FIELD_OP_CONF_FILE, "r") as s: @@ -194,7 +194,7 @@ def _invoke_validating_function(cmd): validating_functions.update(tables.get(table, {}).get("field_operation_validators", [])) for function in validating_functions: - if not _invoke_validating_function(function): + if not _invoke_validating_function(function, element): raise IllegalPatchOperationError("Modification of {} table is illegal- validating function {} returned False".format(table, function)) def validate_lanes(self, config_db): diff --git a/tests/generic_config_updater/field_operation_validator_test.py b/tests/generic_config_updater/field_operation_validator_test.py new file mode 100644 index 000000000..4ffe11d5b --- /dev/null +++ b/tests/generic_config_updater/field_operation_validator_test.py @@ -0,0 +1,142 @@ +import io +import unittest +import mock +import json +import subprocess +import generic_config_updater +import generic_config_updater.field_operation_validators as fov +import generic_config_updater.gu_common as gu_common + +from unittest.mock import MagicMock, Mock, mock_open +from mock import patch +from sonic_py_common.device_info import get_hwsku, get_sonic_version_info + + +class TestValidateFieldOperation(unittest.TestCase): + + @patch("generic_config_updater.field_operation_validators.get_asic_name", mock.Mock(return_value="unknown")) + def test_rdma_config_update_validator_unknown_asic(self): + patch_element = {"path": "/PFC_WD/Ethernet4/restoration_time", "op": "replace", "value": "234234"} + assert generic_config_updater.field_operation_validators.rdma_config_update_validator(patch_element) == False + + @patch("sonic_py_common.device_info.get_sonic_version_info", mock.Mock(return_value={"build_version": "SONiC.20220530"})) + @patch("generic_config_updater.field_operation_validators.get_asic_name", mock.Mock(return_value="td3")) + @patch("os.path.exists", mock.Mock(return_value=True)) + @patch("builtins.open", mock_open(read_data='{"tables": {"BUFFER_POOL": {"validator_data": {"rdma_config_update_validator": {"Shared/headroom pool size changes": {"fields": ["ingress_lossless_pool/xoff", "ingress_lossless_pool/size", "egress_lossy_pool/size"], "operations": ["replace"], "platforms": {"td3": "20221100"}}}}}}}')) + def test_rdma_config_update_validator_td3_asic_invalid_version(self): + patch_element = {"path": "/BUFFER_POOL/ingress_lossless_pool/xoff", "op": "replace", "value": "234234"} + assert generic_config_updater.field_operation_validators.rdma_config_update_validator(patch_element) == False + + @patch("sonic_py_common.device_info.get_sonic_version_info", mock.Mock(return_value={"build_version": "SONiC.20220530"})) + @patch("generic_config_updater.field_operation_validators.get_asic_name", mock.Mock(return_value="spc1")) + @patch("os.path.exists", mock.Mock(return_value=True)) + @patch("builtins.open", mock_open(read_data='{"tables": {"PFC_WD": {"validator_data": {"rdma_config_update_validator": {"PFCWD enable/disable": {"fields": ["detection_time", "action"], "operations": ["remove", "replace", "add"], "platforms": {"spc1": "20181100"}}}}}}}')) + def test_rdma_config_update_validator_spc_asic_valid_version(self): + patch_element = {"path": "/PFC_WD/Ethernet8/detection_time", "op": "remove"} + assert generic_config_updater.field_operation_validators.rdma_config_update_validator(patch_element) == True + + @patch("sonic_py_common.device_info.get_sonic_version_info", mock.Mock(return_value={"build_version": "SONiC.20220530"})) + @patch("generic_config_updater.field_operation_validators.get_asic_name", mock.Mock(return_value="spc1")) + @patch("os.path.exists", mock.Mock(return_value=True)) + @patch("builtins.open", mock_open(read_data='{"tables": {"BUFFER_POOL": {"validator_data": {"rdma_config_update_validator": {"Shared/headroom pool size changes": {"fields": ["ingress_lossless_pool/xoff", "egress_lossy_pool/size"], "operations": ["replace"], "platforms": {"spc1": "20181100"}}}}}}}')) + def test_rdma_config_update_validator_spc_asic_invalid_op(self): + patch_element = {"path": "/BUFFER_POOL/ingress_lossless_pool/xoff", "op": "remove"} + assert generic_config_updater.field_operation_validators.rdma_config_update_validator(patch_element) == False + + @patch("sonic_py_common.device_info.get_sonic_version_info", mock.Mock(return_value={"build_version": "SONiC.20220530"})) + @patch("generic_config_updater.field_operation_validators.get_asic_name", mock.Mock(return_value="spc1")) + @patch("os.path.exists", mock.Mock(return_value=True)) + @patch("builtins.open", mock_open(read_data='{"tables": {"PFC_WD": {"validator_data": {"rdma_config_update_validator": {"PFCWD enable/disable": {"fields": ["detection_time", "action"], "operations": ["remove", "replace", "add"], "platforms": {"spc1": "20181100"}}}}}}}')) + def test_rdma_config_update_validator_spc_asic_other_field(self): + patch_element = {"path": "/PFC_WD/Ethernet8/other_field", "op": "add", "value": "sample_value"} + assert generic_config_updater.field_operation_validators.rdma_config_update_validator(patch_element) == False + + def test_validate_field_operation_illegal__pfcwd(self): + old_config = {"PFC_WD": {"GLOBAL": {"POLL_INTERVAL": "60"}}} + target_config = {"PFC_WD": {"GLOBAL": {}}} + config_wrapper = gu_common.ConfigWrapper() + self.assertRaises(gu_common.IllegalPatchOperationError, config_wrapper.validate_field_operation, old_config, target_config) + + def test_validate_field_operation_legal__rm_loopback1(self): + old_config = { + "LOOPBACK_INTERFACE": { + "Loopback0": {}, + "Loopback0|10.1.0.32/32": {}, + "Loopback1": {}, + "Loopback1|10.1.0.33/32": {} + } + } + target_config = { + "LOOPBACK_INTERFACE": { + "Loopback0": {}, + "Loopback0|10.1.0.32/32": {} + } + } + config_wrapper = gu_common.ConfigWrapper() + config_wrapper.validate_field_operation(old_config, target_config) + + def test_validate_field_operation_illegal__rm_loopback0(self): + old_config = { + "LOOPBACK_INTERFACE": { + "Loopback0": {}, + "Loopback0|10.1.0.32/32": {}, + "Loopback1": {}, + "Loopback1|10.1.0.33/32": {} + } + } + target_config = { + "LOOPBACK_INTERFACE": { + "Loopback1": {}, + "Loopback1|10.1.0.33/32": {} + } + } + config_wrapper = gu_common.ConfigWrapper() + self.assertRaises(gu_common.IllegalPatchOperationError, config_wrapper.validate_field_operation, old_config, target_config) + +class TestGetAsicName(unittest.TestCase): + + @patch('sonic_py_common.device_info.get_sonic_version_info') + @patch('subprocess.Popen') + def test_get_asic_spc1(self, mock_popen, mock_get_sonic_version_info): + mock_get_sonic_version_info.return_value = {'asic_type': 'mellanox'} + mock_popen.return_value = mock.Mock() + mock_popen.return_value.communicate.return_value = ["Mellanox-SN2700-D48C8", 0] + self.assertEqual(fov.get_asic_name(), "spc1") + + @patch('sonic_py_common.device_info.get_sonic_version_info') + @patch('subprocess.Popen') + def test_get_asic_th(self, mock_popen, mock_get_sonic_version_info): + mock_get_sonic_version_info.return_value = {'asic_type': 'broadcom'} + mock_popen.return_value = mock.Mock() + mock_popen.return_value.communicate.return_value = ["Force10-S6100", 0] + self.assertEqual(fov.get_asic_name(), "th") + + @patch('sonic_py_common.device_info.get_sonic_version_info') + @patch('subprocess.Popen') + def test_get_asic_th2(self, mock_popen, mock_get_sonic_version_info): + mock_get_sonic_version_info.return_value = {'asic_type': 'broadcom'} + mock_popen.return_value = mock.Mock() + mock_popen.return_value.communicate.return_value = ["Arista-7260CX3-D108C8", 0] + self.assertEqual(fov.get_asic_name(), "th2") + + @patch('sonic_py_common.device_info.get_sonic_version_info') + @patch('subprocess.Popen') + def test_get_asic_td2(self, mock_popen, mock_get_sonic_version_info): + mock_get_sonic_version_info.return_value = {'asic_type': 'broadcom'} + mock_popen.return_value = mock.Mock() + mock_popen.return_value.communicate.return_value = ["Force10-S6000", 0] + self.assertEqual(fov.get_asic_name(), "td2") + + @patch('sonic_py_common.device_info.get_sonic_version_info') + @patch('subprocess.Popen') + def test_get_asic_td3(self, mock_popen, mock_get_sonic_version_info): + mock_get_sonic_version_info.return_value = {'asic_type': 'broadcom'} + mock_popen.return_value = mock.Mock() + mock_popen.return_value.communicate.return_value = ["Arista-7050CX3-32S-C32", 0] + self.assertEqual(fov.get_asic_name(), "td3") + + @patch('sonic_py_common.device_info.get_sonic_version_info') + @patch('subprocess.Popen') + def test_get_asic_cisco(self, mock_popen, mock_get_sonic_version_info): + mock_get_sonic_version_info.return_value = {'asic_type': 'cisco-8000'} + self.assertEqual(fov.get_asic_name(), "cisco-8000") diff --git a/tests/generic_config_updater/files/feature_patch_application_test_failure.json b/tests/generic_config_updater/files/feature_patch_application_test_failure.json new file mode 100644 index 000000000..80c523ddf --- /dev/null +++ b/tests/generic_config_updater/files/feature_patch_application_test_failure.json @@ -0,0 +1,35 @@ +{ + "RDMA_SHARED_POOL_SIZE_CHANGE__FAILURE": { + "desc": "For RDMA shared pool size tuning- adjust both shared pool and headroom pool", + "current_config": { + "BUFFER_POOL": { + "ingress_lossless_pool": { + "xoff": "4194112", + "type": "ingress", + "mode": "dynamic", + "size": "10875072" + }, + "egress_lossless_pool": { + "type": "egress", + "mode": "static", + "size": "15982720" + }, + "egress_lossy_pool": { + "type": "egress", + "mode": "dynamic", + "size": "9243812" + } + } + }, + "patch": [ + { + "op": "replace", + "path": "/BUFFER_POOL/ingress_lossless_pool/xoff", + "value": "invalid_xoff" + } + ], + "expected_error_substrings": [ + "Given patch will produce invalid config" + ] + } +} diff --git a/tests/generic_config_updater/files/feature_patch_application_test_success.json b/tests/generic_config_updater/files/feature_patch_application_test_success.json new file mode 100644 index 000000000..7ca6cab4b --- /dev/null +++ b/tests/generic_config_updater/files/feature_patch_application_test_success.json @@ -0,0 +1,62 @@ +{ + "RDMA_SHARED_POOL_SIZE_CHANGE__SUCCESS": { + "desc": "For RDMA shared pool size tuning- adjust both shared pool and headroom pool", + "current_config": { + "BUFFER_POOL": { + "ingress_lossless_pool": { + "xoff": "4194112", + "type": "ingress", + "mode": "dynamic", + "size": "10875072" + }, + "egress_lossless_pool": { + "type": "egress", + "mode": "static", + "size": "15982720" + }, + "egress_lossy_pool": { + "type": "egress", + "mode": "dynamic", + "size": "9243812" + } + } + }, + "patch": [ + { + "op": "replace", + "path": "/BUFFER_POOL/ingress_lossless_pool/xoff", + "value": "2155712" + }, + { + "op": "replace", + "path": "/BUFFER_POOL/ingress_lossless_pool/size", + "value": "12913472" + }, + { + "op": "replace", + "path": "/BUFFER_POOL/egress_lossy_pool/size", + "value": "5200000" + } + ], + "expected_config": { + "BUFFER_POOL": { + "ingress_lossless_pool": { + "xoff": "2155712", + "type": "ingress", + "mode": "dynamic", + "size": "12913472" + }, + "egress_lossless_pool": { + "type": "egress", + "mode": "static", + "size": "15982720" + }, + "egress_lossy_pool": { + "type": "egress", + "mode": "dynamic", + "size": "5200000" + } + } + } + } +} diff --git a/tests/generic_config_updater/gcu_feature_patch_application_test.py b/tests/generic_config_updater/gcu_feature_patch_application_test.py new file mode 100644 index 000000000..db625e8cd --- /dev/null +++ b/tests/generic_config_updater/gcu_feature_patch_application_test.py @@ -0,0 +1,142 @@ +import jsonpatch +import unittest +import copy +import mock +from unittest.mock import MagicMock, Mock +from mock import patch + +import generic_config_updater.change_applier +import generic_config_updater.patch_sorter as ps +import generic_config_updater.generic_updater as gu +from .gutest_helpers import Files +from generic_config_updater.gu_common import ConfigWrapper, PatchWrapper + +running_config = {} + +def set_entry(config_db, tbl, key, data): + global running_config + if data != None: + if tbl not in running_config: + running_config[tbl] = {} + running_config[tbl][key] = data + else: + assert tbl in running_config + assert key in running_config[tbl] + running_config[tbl].pop(key) + if not running_config[tbl]: + running_config.pop(tbl) + +def get_running_config(): + return running_config + +class TestFeaturePatchApplication(unittest.TestCase): + def setUp(self): + self.config_wrapper = ConfigWrapper() + + @patch("generic_config_updater.field_operation_validators.rdma_config_update_validator", mock.Mock(return_value=True)) + def test_feature_patch_application_success(self): + # Format of the JSON file containing the test-cases: + # + # { + # "":{ + # "desc":"", + # "current_config":, + # "patch":, + # "expected_config": + # }, + # . + # . + # . + # } + data = Files.FEATURE_PATCH_APPLICATION_TEST_SUCCESS + + for test_case_name in data: + with self.subTest(name=test_case_name): + self.run_single_success_case_applier(data[test_case_name]) + + @patch("generic_config_updater.field_operation_validators.rdma_config_update_validator", mock.Mock(return_value=True)) + def test_feature_patch_application_failure(self): + # Fromat of the JSON file containing the test-cases: + # + # { + # "":{ + # "desc":"", + # "current_config":, + # "patch":, + # "expected_error_substrings": + # }, + # . + # . + # . + # } + data = Files.FEATURE_PATCH_APPLICATION_TEST_FAILURE + + for test_case_name in data: + with self.subTest(name=test_case_name): + self.run_single_failure_case_applier(data[test_case_name]) + + def create_strict_patch_sorter(self, config): + config_wrapper = self.config_wrapper + config_wrapper.get_config_db_as_json = MagicMock(return_value=config) + patch_wrapper = PatchWrapper(config_wrapper) + return ps.StrictPatchSorter(config_wrapper, patch_wrapper) + + def create_patch_applier(self, config): + global running_config + running_config = copy.deepcopy(config) + config_wrapper = self.config_wrapper + config_wrapper.get_config_db_as_json = MagicMock(side_effect=get_running_config) + change_applier = generic_config_updater.change_applier.ChangeApplier() + change_applier._get_running_config = MagicMock(side_effect=get_running_config) + patch_wrapper = PatchWrapper(config_wrapper) + return gu.PatchApplier(config_wrapper=config_wrapper, patch_wrapper=patch_wrapper, changeapplier=change_applier) + + @patch("generic_config_updater.change_applier.get_config_db") + @patch("generic_config_updater.change_applier.set_config") + def run_single_success_case_applier(self, data, mock_set, mock_db): + current_config = data["current_config"] + expected_config = data["expected_config"] + patch = jsonpatch.JsonPatch(data["patch"]) + + # Test patch applier + mock_set.side_effect = set_entry + patch_applier = self.create_patch_applier(current_config) + patch_applier.apply(patch) + result_config = patch_applier.config_wrapper.get_config_db_as_json() + + self.assertEqual(expected_config, result_config) + + # Test steps in change applier + sorter = self.create_strict_patch_sorter(current_config) + actual_changes = sorter.sort(patch) + target_config = patch.apply(current_config) + simulated_config = current_config + + for change in actual_changes: + simulated_config = change.apply(simulated_config) + is_valid, error = self.config_wrapper.validate_config_db_config(simulated_config) + self.assertTrue(is_valid, f"Change will produce invalid config. Error: {error}") + + self.assertEqual(target_config, simulated_config) + self.assertEqual(simulated_config, expected_config) + + @patch("generic_config_updater.change_applier.get_config_db") + def run_single_failure_case_applier(self, data, mock_db): + current_config = data["current_config"] + patch = jsonpatch.JsonPatch(data["patch"]) + expected_error_substrings = data["expected_error_substrings"] + + try: + patch_applier = self.create_patch_applier(current_config) + patch_applier.apply(patch) + self.fail("An exception was supposed to be thrown") + except Exception as ex: + notfound_substrings = [] + error = str(ex) + + for substring in expected_error_substrings: + if substring not in error: + notfound_substrings.append(substring) + + if notfound_substrings: + self.fail(f"Did not find the expected substrings {notfound_substrings} in the error: '{error}'") diff --git a/tests/generic_config_updater/gu_common_test.py b/tests/generic_config_updater/gu_common_test.py index d33d448d6..a2a776c0b 100644 --- a/tests/generic_config_updater/gu_common_test.py +++ b/tests/generic_config_updater/gu_common_test.py @@ -71,62 +71,6 @@ def setUp(self): self.config_wrapper_mock = gu_common.ConfigWrapper() self.config_wrapper_mock.get_config_db_as_json=MagicMock(return_value=Files.CONFIG_DB_AS_JSON) - @patch("sonic_py_common.device_info.get_sonic_version_info", mock.Mock(return_value={"asic_type": "mellanox", "build_version": "SONiC.20181131"})) - def test_validate_field_operation_legal__pfcwd(self): - old_config = {"PFC_WD": {"GLOBAL": {"POLL_INTERVAL": "60"}}} - target_config = {"PFC_WD": {"GLOBAL": {"POLL_INTERVAL": "40"}}} - config_wrapper = gu_common.ConfigWrapper() - config_wrapper.validate_field_operation(old_config, target_config) - - def test_validate_field_operation_legal__rm_loopback1(self): - old_config = { - "LOOPBACK_INTERFACE": { - "Loopback0": {}, - "Loopback0|10.1.0.32/32": {}, - "Loopback1": {}, - "Loopback1|10.1.0.33/32": {} - } - } - target_config = { - "LOOPBACK_INTERFACE": { - "Loopback0": {}, - "Loopback0|10.1.0.32/32": {} - } - } - config_wrapper = gu_common.ConfigWrapper() - config_wrapper.validate_field_operation(old_config, target_config) - - def test_validate_field_operation_illegal__pfcwd(self): - old_config = {"PFC_WD": {"GLOBAL": {"POLL_INTERVAL": "60"}}} - target_config = {"PFC_WD": {"GLOBAL": {}}} - config_wrapper = gu_common.ConfigWrapper() - self.assertRaises(gu_common.IllegalPatchOperationError, config_wrapper.validate_field_operation, old_config, target_config) - - @patch("sonic_py_common.device_info.get_sonic_version_info", mock.Mock(return_value={"asic_type": "invalid-asic", "build_version": "SONiC.20181131"})) - def test_validate_field_modification_illegal__pfcwd(self): - old_config = {"PFC_WD": {"GLOBAL": {"POLL_INTERVAL": "60"}}} - target_config = {"PFC_WD": {"GLOBAL": {"POLL_INTERVAL": "80"}}} - config_wrapper = gu_common.ConfigWrapper() - self.assertRaises(gu_common.IllegalPatchOperationError, config_wrapper.validate_field_operation, old_config, target_config) - - def test_validate_field_operation_illegal__rm_loopback0(self): - old_config = { - "LOOPBACK_INTERFACE": { - "Loopback0": {}, - "Loopback0|10.1.0.32/32": {}, - "Loopback1": {}, - "Loopback1|10.1.0.33/32": {} - } - } - target_config = { - "LOOPBACK_INTERFACE": { - "Loopback1": {}, - "Loopback1|10.1.0.33/32": {} - } - } - config_wrapper = gu_common.ConfigWrapper() - self.assertRaises(gu_common.IllegalPatchOperationError, config_wrapper.validate_field_operation, old_config, target_config) - def test_ctor__default_values_set(self): config_wrapper = gu_common.ConfigWrapper()