diff --git a/generic_config_updater/field_operation_validators.py b/generic_config_updater/field_operation_validators.py index 84cc48547f..72af9c8bd0 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 f12a14d8eb..2dcf1649b7 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 e8c66fcbbe..a6cb8de094 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)) 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 0000000000..4ffe11d5bd --- /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/gcu_feature_patch_application_test.py b/tests/generic_config_updater/gcu_feature_patch_application_test.py index 3f744e20ca..db625e8cd1 100644 --- a/tests/generic_config_updater/gcu_feature_patch_application_test.py +++ b/tests/generic_config_updater/gcu_feature_patch_application_test.py @@ -1,6 +1,7 @@ import jsonpatch import unittest import copy +import mock from unittest.mock import MagicMock, Mock from mock import patch @@ -31,7 +32,8 @@ def get_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: # @@ -52,6 +54,7 @@ def test_feature_patch_application_success(self): 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: # diff --git a/tests/generic_config_updater/gu_common_test.py b/tests/generic_config_updater/gu_common_test.py index a319a25ead..a2a776c0bb 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_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_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) - def test_ctor__default_values_set(self): config_wrapper = gu_common.ConfigWrapper()