Skip to content

Commit 5ce06b2

Browse files
Add golden config check (sonic-net#3770)
* Add golden config check * fix format * Addressing comment. * fix tests. * fix format * fix common function * Fix UT * fix test * reset mock * fix ut * fix ut * remove unnecessary test * Add negative test * fix format * remove empty dict check * Add none check * fix condition * fix invalid golden
1 parent 3c50dee commit 5ce06b2

File tree

2 files changed

+105
-21
lines changed

2 files changed

+105
-21
lines changed

config/main.py

+28-9
Original file line numberDiff line numberDiff line change
@@ -1392,26 +1392,41 @@ def multiasic_write_to_db(filename, load_sysinfo):
13921392

13931393
def config_file_yang_validation(filename):
13941394
config = read_json_file(filename)
1395+
1396+
# Check if the config is not a dictionary
1397+
if not isinstance(config, dict):
1398+
return False
1399+
1400+
# If the device is multi-ASIC, check if all required namespaces exist
1401+
if multi_asic.is_multi_asic():
1402+
required_namespaces = [HOST_NAMESPACE, *multi_asic.get_namespace_list()]
1403+
for ns in required_namespaces:
1404+
asic_name = HOST_NAMESPACE if ns == DEFAULT_NAMESPACE else ns
1405+
if asic_name not in config:
1406+
return False
1407+
13951408
sy = sonic_yang.SonicYang(YANG_DIR)
13961409
sy.loadYangModel()
13971410
asic_list = [HOST_NAMESPACE]
13981411
if multi_asic.is_multi_asic():
13991412
asic_list.extend(multi_asic.get_namespace_list())
14001413
for scope in asic_list:
14011414
config_to_check = config.get(scope) if multi_asic.is_multi_asic() else config
1402-
try:
1403-
sy.loadData(configdbJson=config_to_check)
1404-
sy.validate_data_tree()
1405-
except sonic_yang.SonicYangException as e:
1406-
click.secho("{} fails YANG validation! Error: {}".format(filename, str(e)),
1407-
fg='magenta')
1408-
raise click.Abort()
1415+
if config_to_check:
1416+
try:
1417+
sy.loadData(configdbJson=config_to_check)
1418+
sy.validate_data_tree()
1419+
except sonic_yang.SonicYangException as e:
1420+
click.secho("{} fails YANG validation! Error: {}".format(filename, str(e)),
1421+
fg='magenta')
1422+
raise click.Abort()
14091423

14101424
sy.tablesWithOutYang.pop('bgpraw', None)
14111425
if len(sy.tablesWithOutYang):
14121426
click.secho("Config tables are missing yang models: {}".format(str(sy.tablesWithOutYang.keys())),
14131427
fg='magenta')
14141428
raise click.Abort()
1429+
return True
14151430

14161431

14171432
# This is our main entrypoint - the main 'config' command
@@ -2048,7 +2063,10 @@ def load_minigraph(db, no_service_restart, traffic_shift_away, override_config,
20482063
fg='magenta')
20492064
raise click.Abort()
20502065

2051-
config_file_yang_validation(golden_config_path)
2066+
if not config_file_yang_validation(golden_config_path):
2067+
click.secho("Invalid golden config file:'{}'!".format(golden_config_path),
2068+
fg='magenta')
2069+
raise click.Abort()
20522070

20532071
config_to_check = read_json_file(golden_config_path)
20542072
# Dependency check golden config json
@@ -2057,7 +2075,8 @@ def load_minigraph(db, no_service_restart, traffic_shift_away, override_config,
20572075
asic_list.extend(multi_asic.get_namespace_list())
20582076
for scope in asic_list:
20592077
host_config = config_to_check.get(scope) if multi_asic.is_multi_asic() else config_to_check
2060-
table_hard_dependency_check(host_config)
2078+
if host_config:
2079+
table_hard_dependency_check(host_config)
20612080

20622081
# Stop services before config push
20632082
if not no_service_restart:

tests/config_test.py

+77-12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import config.main as config
2929
import config.validated_config_db_connector as validated_config_db_connector
30+
from config.main import config_file_yang_validation
3031

3132
# Add Test, module and script path.
3233
test_path = os.path.dirname(os.path.abspath(__file__))
@@ -966,6 +967,16 @@ def setup_class(cls):
966967
import config.main
967968
importlib.reload(config.main)
968969

970+
def read_json_file_side_effect(self, filename):
971+
return {
972+
'DEVICE_METADATA': {
973+
'localhost': {
974+
'platform': 'x86_64-mlnx_msn2700-r0',
975+
'mac': '00:02:03:04:05:07'
976+
}
977+
}
978+
}
979+
969980
@mock.patch('sonic_py_common.device_info.get_paths_to_platform_and_hwsku_dirs', mock.MagicMock(return_value=("dummy_path", None)))
970981
@mock.patch('config.main.subprocess.check_call')
971982
def test_load_minigraph(self, mock_check_call, get_cmd_module, setup_single_broadcom_asic):
@@ -1135,12 +1146,9 @@ def test_load_minigraph_with_specified_golden_config_path(self, get_cmd_module):
11351146
def is_file_side_effect(filename):
11361147
return True if 'golden_config' in filename else False
11371148

1138-
def read_json_file_side_effect(filename):
1139-
return {}
1140-
11411149
with mock.patch("utilities_common.cli.run_command", mock.MagicMock(side_effect=mock_run_command_side_effect)) as mock_run_command, \
11421150
mock.patch('os.path.isfile', mock.MagicMock(side_effect=is_file_side_effect)), \
1143-
mock.patch('config.main.read_json_file', mock.MagicMock(side_effect=read_json_file_side_effect)):
1151+
mock.patch('config.main.read_json_file', mock.MagicMock(side_effect=self.read_json_file_side_effect)):
11441152
(config, show) = get_cmd_module
11451153
runner = CliRunner()
11461154
result = runner.invoke(config.config.commands["load_minigraph"], ["--override_config", "--golden_config_path", "golden_config.json", "-y"])
@@ -1152,18 +1160,32 @@ def test_load_minigraph_with_default_golden_config_path(self, get_cmd_module):
11521160
def is_file_side_effect(filename):
11531161
return True if 'golden_config' in filename else False
11541162

1155-
def read_json_file_side_effect(filename):
1156-
return {}
1157-
11581163
with mock.patch("utilities_common.cli.run_command", mock.MagicMock(side_effect=mock_run_command_side_effect)) as mock_run_command, \
11591164
mock.patch('os.path.isfile', mock.MagicMock(side_effect=is_file_side_effect)), \
1160-
mock.patch('config.main.read_json_file', mock.MagicMock(side_effect=read_json_file_side_effect)):
1165+
mock.patch('config.main.read_json_file', mock.MagicMock(side_effect=self.read_json_file_side_effect)):
11611166
(config, show) = get_cmd_module
11621167
runner = CliRunner()
11631168
result = runner.invoke(config.config.commands["load_minigraph"], ["--override_config", "-y"])
11641169
assert result.exit_code == 0
11651170
assert "config override-config-table /etc/sonic/golden_config_db.json" in result.output
11661171

1172+
@mock.patch(
1173+
'sonic_py_common.device_info.get_paths_to_platform_and_hwsku_dirs',
1174+
mock.MagicMock(return_value=("dummy_path", None)))
1175+
def test_load_minigraph_with_invalid_golden_config(self, get_cmd_module):
1176+
def is_file_side_effect(filename):
1177+
return True if 'golden_config' in filename else False
1178+
1179+
with mock.patch("utilities_common.cli.run_command",
1180+
mock.MagicMock(side_effect=mock_run_command_side_effect)), \
1181+
mock.patch('os.path.isfile', mock.MagicMock(side_effect=is_file_side_effect)), \
1182+
mock.patch('config.main.read_json_file', mock.MagicMock(return_value=[])):
1183+
(config, show) = get_cmd_module
1184+
runner = CliRunner()
1185+
result = runner.invoke(config.config.commands["load_minigraph"], ["--override_config", "-y"])
1186+
assert result.exit_code != 0
1187+
assert "Invalid golden config file:" in result.output
1188+
11671189
@mock.patch('sonic_py_common.device_info.get_paths_to_platform_and_hwsku_dirs',
11681190
mock.MagicMock(return_value=("dummy_path", None)))
11691191
def test_load_minigraph_hard_dependency_check(self, get_cmd_module):
@@ -1234,11 +1256,9 @@ def test_load_minigraph_with_traffic_shift_away_with_golden_config(self, get_cmd
12341256
def is_file_side_effect(filename):
12351257
return True if 'golden_config' in filename else False
12361258

1237-
def read_json_file_side_effect(filename):
1238-
return {}
1239-
12401259
with mock.patch('os.path.isfile', mock.MagicMock(side_effect=is_file_side_effect)), \
1241-
mock.patch('config.main.read_json_file', mock.MagicMock(side_effect=read_json_file_side_effect)):
1260+
mock.patch('config.main.read_json_file', mock.MagicMock(
1261+
side_effect=self.read_json_file_side_effect)):
12421262
(config, show) = get_cmd_module
12431263
db = Db()
12441264
golden_config = {}
@@ -1251,6 +1271,51 @@ def read_json_file_side_effect(filename):
12511271
assert "TSA" in result.output
12521272
assert "[WARNING] Golden configuration may override Traffic-shift-away state" in result.output
12531273

1274+
def test_config_file_yang_validation(self):
1275+
# Test with empty config
1276+
with mock.patch('config.main.read_json_file', return_value="") as mock_read_json_file:
1277+
with mock.patch('config.main.sonic_yang.SonicYang.loadYangModel') as mock_load_yang_model:
1278+
assert not config_file_yang_validation('dummy_file.json')
1279+
mock_read_json_file.assert_called_once_with('dummy_file.json')
1280+
mock_load_yang_model.assert_not_called()
1281+
1282+
# Test with non-dict config
1283+
with mock.patch('config.main.read_json_file', return_value=[]) as mock_read_json_file:
1284+
with mock.patch('config.main.sonic_yang.SonicYang.loadYangModel') as mock_load_yang_model:
1285+
assert not config_file_yang_validation('dummy_file.json')
1286+
mock_read_json_file.assert_called_once_with('dummy_file.json')
1287+
mock_load_yang_model.assert_not_called()
1288+
1289+
# Test with missing namespaces in multi-ASIC config
1290+
with mock.patch('config.main.read_json_file', return_value={'localhost': {}}) as mock_read_json_file:
1291+
with mock.patch('config.main.multi_asic.is_multi_asic', return_value=True):
1292+
with mock.patch('config.main.multi_asic.get_namespace_list', return_value=['asic0', 'asic1']):
1293+
with mock.patch('config.main.sonic_yang.SonicYang.loadYangModel') as mock_load_yang_model:
1294+
assert not config_file_yang_validation('dummy_file.json')
1295+
mock_read_json_file.assert_called_once_with('dummy_file.json')
1296+
mock_load_yang_model.assert_not_called()
1297+
1298+
# Test with valid config
1299+
valid_config = {
1300+
'DEVICE_METADATA': {
1301+
'localhost': {
1302+
'platform': 'x86_64-mlnx_msn2700-r0',
1303+
'mac': '00:02:03:04:05:07'
1304+
}
1305+
}
1306+
}
1307+
1308+
with mock.patch('config.main.read_json_file', return_value=valid_config) as mock_read_json_file, \
1309+
mock.patch('config.main.multi_asic.is_multi_asic', return_value=False), \
1310+
mock.patch('config.main.sonic_yang.SonicYang.loadYangModel') as mock_load_yang_model, \
1311+
mock.patch('config.main.sonic_yang.SonicYang.loadData') as mock_load_data, \
1312+
mock.patch('config.main.sonic_yang.SonicYang.validate_data_tree') as mock_validate_data_tree:
1313+
assert config_file_yang_validation('dummy_file.json')
1314+
mock_read_json_file.assert_called_once_with('dummy_file.json')
1315+
mock_load_yang_model.assert_called_once()
1316+
mock_load_data.assert_called_once_with(configdbJson=valid_config)
1317+
mock_validate_data_tree.assert_called_once()
1318+
12541319
@classmethod
12551320
def teardown_class(cls):
12561321
os.environ['UTILITIES_UNIT_TESTING'] = "0"

0 commit comments

Comments
 (0)