Skip to content

Commit 42f51c2

Browse files
authored
sonic-utilities: Update config reload() to verify formatting of an input file (sonic-net#2529)
sonic-utilities: bugfix-9499 Update config/main.py to verify if a config input file is properly formatted before writing it into confib_db Update tests/config_test.py to include test cases for invalid input file Add tests/config_reload_input/config_db_invalid.json as invalid input file used in tests/config_test.py Signed-off-by: [email protected] What I did Include a check to validate if all input files are properly formatted before trying to write them to config_db to resolve bug 9499 in sonic-buildimage. How I did it Include a check to validate if all input files are properly formatted before trying to write them to config_db. How to verify it Run tests/config_test.py. With the change added in main.py, run 'config reload' with an inproperly formatted input file. Previous command output (if the output of a command-line utility has changed) crystalnet@ibr02:~$ sudo config reload conf_db.json Clear current config and reload config in config_db from the file(s) conf_db.json ? [y/N]: y Disabling container monitoring ... Stopping SONiC target ... Running command: /usr/local/bin/sonic-cfggen -j /etc/sonic/init_cfg.json -j conf_db.json --write-to-db Traceback (most recent call last): File "/usr/local/bin/sonic-cfggen", line 452, in main() File "/usr/local/bin/sonic-cfggen", line 322, in main _process_json(args, data) File "/usr/local/bin/sonic-cfggen", line 236, in _process_json deep_update(data, FormatConverter.to_deserialized(json.load(stream))) File "/usr/lib/python3.9/json/init.py", line 293, in load return loads(fp.read(), File "/usr/lib/python3.9/json/init.py", line 346, in loads return _default_decoder.decode(s) File "/usr/lib/python3.9/json/decoder.py", line 337, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "/usr/lib/python3.9/json/decoder.py", line 353, in raw_decode obj, end = self.scan_once(s, idx) json.decoder.JSONDecodeError: Expecting ',' delimiter: line 713 column 5 (char 21870) *Then the terminal becomes unusable until the device is reloaded. New command output (if the output of a command-line utility has changed) crystalnet@ibr02:~$ sudo config reload conf_db.json Clear current config and reload config in config_db from the file(s) conf_db.json ? [y/N]: y Bad format: json file broken. Expecting ',' delimiter: line 713 column 5 (char 21870)
1 parent a5e1e2b commit 42f51c2

3 files changed

Lines changed: 208 additions & 8 deletions

File tree

config/main.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,17 @@ def load_backend_acl(cfg_db, device_type):
11891189
if os.path.isfile(BACKEND_ACL_FILE):
11901190
clicommon.run_command("acl-loader update incremental {}".format(BACKEND_ACL_FILE), display_cmd=True)
11911191

1192+
def validate_config_file(file):
1193+
"""
1194+
A validator to check config files for syntax errors
1195+
"""
1196+
try:
1197+
# Load golden config json
1198+
read_json_file(file)
1199+
except Exception as e:
1200+
click.secho("Bad format: json file '{}' broken.\n{}".format(file, str(e)),
1201+
fg='magenta')
1202+
sys.exit(1)
11921203

11931204
# This is our main entrypoint - the main 'config' command
11941205
@click.group(cls=clicommon.AbbreviationGroup, context_settings=CONTEXT_SETTINGS)
@@ -1567,10 +1578,8 @@ def reload(db, filename, yes, load_sysinfo, no_service_restart, force, file_form
15671578
click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file))
15681579
return
15691580

1570-
#Stop services before config push
1571-
if not no_service_restart:
1572-
log.log_info("'reload' stopping services...")
1573-
_stop_services()
1581+
# Create a dictionary to store each cfg_file, namespace, and a bool representing if a the file exists
1582+
cfg_file_dict = {}
15741583

15751584
# In Single ASIC platforms we have single DB service. In multi-ASIC platforms we have a global DB
15761585
# service running in the host + DB services running in each ASIC namespace created per ASIC.
@@ -1595,9 +1604,27 @@ def reload(db, filename, yes, load_sysinfo, no_service_restart, force, file_form
15951604
else:
15961605
file = DEFAULT_CONFIG_YANG_FILE
15971606

1598-
1599-
# Check the file exists before proceeding.
1607+
# Check if the file exists before proceeding
1608+
# Instead of exiting, skip the current namespace and check the next one
16001609
if not os.path.exists(file):
1610+
cfg_file_dict[inst] = [file, namespace, False]
1611+
continue
1612+
cfg_file_dict[inst] = [file, namespace, True]
1613+
1614+
# Check the file is properly formatted before proceeding.
1615+
validate_config_file(file)
1616+
1617+
#Validate INIT_CFG_FILE if it exits
1618+
if os.path.isfile(INIT_CFG_FILE):
1619+
validate_config_file(INIT_CFG_FILE)
1620+
1621+
#Stop services before config push
1622+
if not no_service_restart:
1623+
log.log_info("'reload' stopping services...")
1624+
_stop_services()
1625+
1626+
for file, namespace, file_exists in cfg_file_dict.values():
1627+
if not file_exists:
16011628
click.echo("The config file {} doesn't exist".format(file))
16021629
continue
16031630

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"DEVICE_METADATA": {
3+
"localhost": {
4+
"docker_routing_config_mode": "split",
5+
"hostname": "sonic",
6+
"hwsku": "Seastone-DX010-25-50",
7+
"mac": "00:e0:ec:89:6e:48",
8+
"platform": "x86_64-cel_seastone-r0",
9+
"type": "ToRRouter"
10+
}
11+
}
12+
"VLAN_MEMBER": {
13+
"Vlan1000|Ethernet0": {
14+
"tagging_mode": "untagged",
15+
},
16+
"Vlan1000|Ethernet4": {
17+
"tagging_mode": "untagged"
18+
},
19+
"Vlan1000|Ethernet8": {
20+
"tagging_mode": "untagged"
21+
}
22+
},
23+
"VLAN": {
24+
"Vlan1000": {
25+
"vlanid": "1000",
26+
"dhcp_servers": [
27+
"192.0.0.1",
28+
"192.0.0.2",
29+
"192.0.0.3",
30+
"192.0.0.4"
31+
]
32+
}
33+
},
34+
"PORT": {
35+
"Ethernet0": {
36+
"alias": "Eth1",
37+
"lanes": "65, 66, 67, 68",
38+
"description": "Ethernet0 100G link",
39+
"speed": "100000"
40+
},
41+
"Ethernet4": {
42+
"admin_status": "up",
43+
"alias": "fortyGigE0/4",
44+
"description": "Servers0:eth0",
45+
"index": "1",
46+
"lanes": "29,30,31,32",
47+
"mtu": "9100",
48+
"pfc_asym": "off",
49+
"speed": "40000"
50+
},
51+
"Ethernet8": {
52+
"admin_status": "up",
53+
"alias": "fortyGigE0/8",
54+
"description": "Servers1:eth0",
55+
"index": "2",
56+
"lanes": "33,34,35,36",
57+
"mtu": "9100",
58+
"pfc_asym": "off",
59+
"speed": "40000"
60+
}
61+
}
62+
}

tests/config_test.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import traceback
55
import json
66
import jsonpatch
7+
import shutil
78
import sys
89
import unittest
910
import ipaddress
@@ -88,6 +89,12 @@
8889
Reloading Monit configuration ...
8990
"""
9091

92+
RELOAD_CONFIG_DB_OUTPUT_INVALID_MSG = """\
93+
Bad format: json file"""
94+
95+
RELOAD_CONFIG_DB_OUTPUT_INVALID_ERROR = """\
96+
Expecting ',' delimiter: line 12 column 5 (char 321)"""
97+
9198
RELOAD_YANG_CFG_OUTPUT = """\
9299
Stopping SONiC target ...
93100
Running command: /usr/local/bin/sonic-cfggen -Y /tmp/config.json --write-to-db
@@ -104,6 +111,15 @@
104111
Reloading Monit configuration ...
105112
"""
106113

114+
RELOAD_MASIC_CONFIG_DB_OUTPUT_FILE_NOT_EXIST = """\
115+
Stopping SONiC target ...
116+
Running command: /usr/local/bin/sonic-cfggen -j /tmp/config.json --write-to-db
117+
The config file non_exist.json doesn't exist
118+
Running command: /usr/local/bin/sonic-cfggen -j /tmp/config.json -n asic1 --write-to-db
119+
Restarting SONiC target ...
120+
Reloading Monit configuration ...
121+
"""
122+
107123
reload_config_with_sys_info_command_output="""\
108124
Running command: /usr/local/bin/sonic-cfggen -H -k Seastone-DX010-25-50 --write-to-db"""
109125

@@ -195,6 +211,7 @@ def mock_run_command_side_effect_gnmi(*args, **kwargs):
195211

196212
class TestConfigReload(object):
197213
dummy_cfg_file = os.path.join(os.sep, "tmp", "config.json")
214+
dummy_cfg_file_contents = os.path.join(mock_db_path, "config_db.json")
198215

199216
@classmethod
200217
def setup_class(cls):
@@ -206,7 +223,8 @@ def setup_class(cls):
206223

207224
import config.main
208225
importlib.reload(config.main)
209-
open(cls.dummy_cfg_file, 'w').close()
226+
shutil.copyfile(cls.dummy_cfg_file_contents, cls.dummy_cfg_file)
227+
open(cls.dummy_cfg_file, 'r').close()
210228

211229
def test_config_reload(self, get_cmd_module, setup_single_broadcom_asic):
212230
with mock.patch("utilities_common.cli.run_command", mock.MagicMock(side_effect=mock_run_command_side_effect)) as mock_run_command:
@@ -479,14 +497,17 @@ def teardown_class(cls):
479497

480498
class TestReloadConfig(object):
481499
dummy_cfg_file = os.path.join(os.sep, "tmp", "config.json")
500+
dummy_cfg_file_contents = os.path.join(mock_db_path, "config_db.json")
501+
dummy_cfg_file_invalid = os.path.join(mock_db_path, "config_db_invalid.json")
482502

483503
@classmethod
484504
def setup_class(cls):
485505
os.environ['UTILITIES_UNIT_TESTING'] = "1"
486506
print("SETUP")
487507
import config.main
488508
importlib.reload(config.main)
489-
open(cls.dummy_cfg_file, 'w').close()
509+
shutil.copyfile(cls.dummy_cfg_file_contents, cls.dummy_cfg_file)
510+
open(cls.dummy_cfg_file, 'r').close()
490511

491512
def test_reload_config(self, get_cmd_module, setup_single_broadcom_asic):
492513
with mock.patch(
@@ -507,6 +528,27 @@ def test_reload_config(self, get_cmd_module, setup_single_broadcom_asic):
507528
assert "\n".join([l.rstrip() for l in result.output.split('\n')]) \
508529
== RELOAD_CONFIG_DB_OUTPUT
509530

531+
def test_reload_config_invalid_config_file(self, get_cmd_module, setup_single_broadcom_asic):
532+
with mock.patch(
533+
"utilities_common.cli.run_command",
534+
mock.MagicMock(side_effect=mock_run_command_side_effect)
535+
) as mock_run_command:
536+
(config, show) = get_cmd_module
537+
runner = CliRunner()
538+
539+
result = runner.invoke(
540+
config.config.commands["reload"],
541+
[self.dummy_cfg_file_invalid, '-y', '-f'])
542+
543+
print(result.exit_code)
544+
print(result.output)
545+
traceback.print_tb(result.exc_info[2])
546+
assert result.exit_code == 1
547+
548+
output = "\n".join([l.rstrip() for l in result.output.split('\n')])
549+
assert RELOAD_CONFIG_DB_OUTPUT_INVALID_MSG in output
550+
assert RELOAD_CONFIG_DB_OUTPUT_INVALID_ERROR in output
551+
510552
def test_config_reload_disabled_service(self, get_cmd_module, setup_single_broadcom_asic):
511553
with mock.patch(
512554
"utilities_common.cli.run_command",
@@ -549,6 +591,54 @@ def test_reload_config_masic(self, get_cmd_module, setup_multi_broadcom_masic):
549591
assert "\n".join([l.rstrip() for l in result.output.split('\n')]) \
550592
== RELOAD_MASIC_CONFIG_DB_OUTPUT
551593

594+
def test_reload_config_masic_invalid(self, get_cmd_module, setup_multi_broadcom_masic):
595+
with mock.patch(
596+
"utilities_common.cli.run_command",
597+
mock.MagicMock(side_effect=mock_run_command_side_effect)
598+
) as mock_run_command:
599+
(config, show) = get_cmd_module
600+
runner = CliRunner()
601+
# 3 config files: 1 for host and 2 for asic
602+
cfg_files = "{},{},{}".format(
603+
self.dummy_cfg_file,
604+
self.dummy_cfg_file_invalid,
605+
self.dummy_cfg_file)
606+
result = runner.invoke(
607+
config.config.commands["reload"],
608+
[cfg_files, '-y', '-f'])
609+
610+
print(result.exit_code)
611+
print(result.output)
612+
traceback.print_tb(result.exc_info[2])
613+
assert result.exit_code == 1
614+
615+
output = "\n".join([l.rstrip() for l in result.output.split('\n')])
616+
assert RELOAD_CONFIG_DB_OUTPUT_INVALID_MSG in output
617+
assert RELOAD_CONFIG_DB_OUTPUT_INVALID_ERROR in output
618+
619+
def test_reload_config_masic_non_exist_file(self, get_cmd_module, setup_multi_broadcom_masic):
620+
with mock.patch(
621+
"utilities_common.cli.run_command",
622+
mock.MagicMock(side_effect=mock_run_command_side_effect)
623+
) as mock_run_command:
624+
(config, show) = get_cmd_module
625+
runner = CliRunner()
626+
# 3 config files: 1 for host and 2 for asic
627+
cfg_files = "{},{},{}".format(
628+
self.dummy_cfg_file,
629+
"non_exist.json",
630+
self.dummy_cfg_file)
631+
result = runner.invoke(
632+
config.config.commands["reload"],
633+
[cfg_files, '-y', '-f'])
634+
635+
print(result.exit_code)
636+
print(result.output)
637+
traceback.print_tb(result.exc_info[2])
638+
assert result.exit_code == 0
639+
assert "\n".join([l.rstrip() for l in result.output.split('\n')]) \
640+
== RELOAD_MASIC_CONFIG_DB_OUTPUT_FILE_NOT_EXIST
641+
552642
def test_reload_yang_config(self, get_cmd_module,
553643
setup_single_broadcom_asic):
554644
with mock.patch(
@@ -568,6 +658,27 @@ def test_reload_yang_config(self, get_cmd_module,
568658
assert "\n".join([l.rstrip() for l in result.output.split('\n')]) \
569659
== RELOAD_YANG_CFG_OUTPUT
570660

661+
def test_reload_yang_config_invalid(self, get_cmd_module,
662+
setup_single_broadcom_asic):
663+
with mock.patch(
664+
"utilities_common.cli.run_command",
665+
mock.MagicMock(side_effect=mock_run_command_side_effect)
666+
) as mock_run_command:
667+
(config, show) = get_cmd_module
668+
runner = CliRunner()
669+
670+
result = runner.invoke(config.config.commands["reload"],
671+
[self.dummy_cfg_file_invalid, '-y', '-f', '-t', 'config_yang'])
672+
673+
print(result.exit_code)
674+
print(result.output)
675+
traceback.print_tb(result.exc_info[2])
676+
assert result.exit_code == 1
677+
678+
output = "\n".join([l.rstrip() for l in result.output.split('\n')])
679+
assert RELOAD_CONFIG_DB_OUTPUT_INVALID_MSG in output
680+
assert RELOAD_CONFIG_DB_OUTPUT_INVALID_ERROR in output
681+
571682
@classmethod
572683
def teardown_class(cls):
573684
os.environ['UTILITIES_UNIT_TESTING'] = "0"

0 commit comments

Comments
 (0)