Skip to content

Commit 85638b2

Browse files
authored
YANG Validation for ConfigDB Updates: DEVICE_METADATA, SNMP, SNMP_COMMUNITY tables (sonic-net#2481)
1 parent da44d71 commit 85638b2

6 files changed

+159
-53
lines changed

config/main.py

+94-50
Original file line numberDiff line numberDiff line change
@@ -1944,11 +1944,15 @@ def override_config_db(config_db, config_input):
19441944
@click.argument('new_hostname', metavar='<new_hostname>', required=True)
19451945
def hostname(new_hostname):
19461946
"""Change device hostname without impacting the traffic."""
1947-
1948-
config_db = ConfigDBConnector()
1947+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
19491948
config_db.connect()
1950-
config_db.mod_entry(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, 'localhost',
1951-
{'hostname': new_hostname})
1949+
try:
1950+
config_db.mod_entry(swsscommon.CFG_DEVICE_METADATA_TABLE_NAME, 'localhost',
1951+
{'hostname': new_hostname})
1952+
except ValueError as e:
1953+
ctx = click.get_current_context()
1954+
ctx.fail("Failed to write new hostname to ConfigDB. Error: {}".format(e))
1955+
19521956

19531957
click.echo('Please note loaded setting will be lost after system reboot. To'
19541958
' preserve setting, run `config save`.')
@@ -1966,32 +1970,42 @@ def synchronous_mode(sync_mode):
19661970
config reload -y \n
19671971
2. systemctl restart swss
19681972
"""
1969-
1970-
if sync_mode == 'enable' or sync_mode == 'disable':
1971-
config_db = ConfigDBConnector()
1972-
config_db.connect()
1973+
if ADHOC_VALIDATION:
1974+
if sync_mode != 'enable' and sync_mode != 'disable':
1975+
raise click.BadParameter("Error: Invalid argument %s, expect either enable or disable" % sync_mode)
1976+
1977+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
1978+
config_db.connect()
1979+
try:
19731980
config_db.mod_entry('DEVICE_METADATA' , 'localhost', {"synchronous_mode" : sync_mode})
1974-
click.echo("""Wrote %s synchronous mode into CONFIG_DB, swss restart required to apply the configuration: \n
1981+
except ValueError as e:
1982+
ctx = click.get_current_context()
1983+
ctx.fail("Error: Invalid argument %s, expect either enable or disable" % sync_mode)
1984+
1985+
click.echo("""Wrote %s synchronous mode into CONFIG_DB, swss restart required to apply the configuration: \n
19751986
Option 1. config save -y \n
19761987
config reload -y \n
19771988
Option 2. systemctl restart swss""" % sync_mode)
1978-
else:
1979-
raise click.BadParameter("Error: Invalid argument %s, expect either enable or disable" % sync_mode)
19801989

19811990
#
19821991
# 'yang_config_validation' command ('config yang_config_validation ...')
19831992
#
19841993
@config.command('yang_config_validation')
19851994
@click.argument('yang_config_validation', metavar='<enable|disable>', required=True)
19861995
def yang_config_validation(yang_config_validation):
1987-
# Enable or disable YANG validation on updates to ConfigDB
1988-
if yang_config_validation == 'enable' or yang_config_validation == 'disable':
1989-
config_db = ConfigDBConnector()
1990-
config_db.connect()
1996+
if ADHOC_VALIDATION:
1997+
if yang_config_validation != 'enable' and yang_config_validation != 'disable':
1998+
raise click.BadParameter("Error: Invalid argument %s, expect either enable or disable" % yang_config_validation)
1999+
2000+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
2001+
config_db.connect()
2002+
try:
19912003
config_db.mod_entry('DEVICE_METADATA', 'localhost', {"yang_config_validation": yang_config_validation})
1992-
click.echo("""Wrote %s yang config validation into CONFIG_DB""" % yang_config_validation)
1993-
else:
1994-
raise click.BadParameter("Error: Invalid argument %s, expect either enable or disable" % yang_config_validation)
2004+
except ValueError as e:
2005+
ctx = click.get_current_context()
2006+
ctx.fail("Error: Invalid argument %s, expect either enable or disable" % yang_config_validation)
2007+
2008+
click.echo("""Wrote %s yang config validation into CONFIG_DB""" % yang_config_validation)
19952009

19962010
#
19972011
# 'portchannel' group ('config portchannel ...')
@@ -3193,16 +3207,24 @@ def snmp_user_secret_check(snmp_secret):
31933207
def add_community(db, community, string_type):
31943208
""" Add snmp community string"""
31953209
string_type = string_type.upper()
3196-
if not is_valid_community_type(string_type):
3197-
sys.exit(1)
3198-
if not snmp_community_secret_check(community):
3199-
sys.exit(2)
3200-
snmp_communities = db.cfgdb.get_table("SNMP_COMMUNITY")
3201-
if community in snmp_communities:
3202-
click.echo("SNMP community {} is already configured".format(community))
3203-
sys.exit(3)
3204-
db.cfgdb.set_entry('SNMP_COMMUNITY', community, {'TYPE': string_type})
3205-
click.echo("SNMP community {} added to configuration".format(community))
3210+
if ADHOC_VALIDATION:
3211+
if not is_valid_community_type(string_type):
3212+
sys.exit(1)
3213+
if not snmp_community_secret_check(community):
3214+
sys.exit(2)
3215+
snmp_communities = db.cfgdb.get_table("SNMP_COMMUNITY")
3216+
if community in snmp_communities:
3217+
click.echo("SNMP community {} is already configured".format(community))
3218+
sys.exit(3)
3219+
3220+
config_db = ValidatedConfigDBConnector(db.cfgdb)
3221+
try:
3222+
config_db.set_entry('SNMP_COMMUNITY', community, {'TYPE': string_type})
3223+
click.echo("SNMP community {} added to configuration".format(community))
3224+
except ValueError as e:
3225+
ctx = click.get_current_context()
3226+
ctx.fail("SNMP community configuration failed. Error: {}".format(e))
3227+
32063228
try:
32073229
click.echo("Restarting SNMP service...")
32083230
clicommon.run_command("systemctl reset-failed snmp.service", display_cmd=False)
@@ -3217,20 +3239,27 @@ def add_community(db, community, string_type):
32173239
@clicommon.pass_db
32183240
def del_community(db, community):
32193241
""" Delete snmp community string"""
3220-
snmp_communities = db.cfgdb.get_table("SNMP_COMMUNITY")
3221-
if community not in snmp_communities:
3222-
click.echo("SNMP community {} is not configured".format(community))
3223-
sys.exit(1)
3224-
else:
3225-
db.cfgdb.set_entry('SNMP_COMMUNITY', community, None)
3242+
if ADHOC_VALIDATION:
3243+
snmp_communities = db.cfgdb.get_table("SNMP_COMMUNITY")
3244+
if community not in snmp_communities:
3245+
click.echo("SNMP community {} is not configured".format(community))
3246+
sys.exit(1)
3247+
3248+
config_db = ValidatedConfigDBConnector(db.cfgdb)
3249+
try:
3250+
config_db.set_entry('SNMP_COMMUNITY', community, None)
32263251
click.echo("SNMP community {} removed from configuration".format(community))
3227-
try:
3228-
click.echo("Restarting SNMP service...")
3229-
clicommon.run_command("systemctl reset-failed snmp.service", display_cmd=False)
3230-
clicommon.run_command("systemctl restart snmp.service", display_cmd=False)
3231-
except SystemExit as e:
3232-
click.echo("Restart service snmp failed with error {}".format(e))
3233-
raise click.Abort()
3252+
except JsonPatchConflict as e:
3253+
ctx = click.get_current_context()
3254+
ctx.fail("SNMP community {} is not configured. Error: {}".format(community, e))
3255+
3256+
try:
3257+
click.echo("Restarting SNMP service...")
3258+
clicommon.run_command("systemctl reset-failed snmp.service", display_cmd=False)
3259+
clicommon.run_command("systemctl restart snmp.service", display_cmd=False)
3260+
except SystemExit as e:
3261+
click.echo("Restart service snmp failed with error {}".format(e))
3262+
raise click.Abort()
32343263

32353264

32363265
@community.command('replace')
@@ -3286,7 +3315,7 @@ def add_contact(db, contact, contact_email):
32863315
click.echo("Contact already exists. Use sudo config snmp contact modify instead")
32873316
sys.exit(1)
32883317
else:
3289-
db.cfgdb.set_entry('SNMP', 'CONTACT', {contact: contact_email})
3318+
db.cfgdb.set_entry('SNMP', 'CONTACT', {contact: contact_email}) # TODO: ERROR IN YANG MODEL. Contact name is not defined as key
32903319
click.echo("Contact name {} and contact email {} have been added to "
32913320
"configuration".format(contact, contact_email))
32923321
try:
@@ -3399,19 +3428,24 @@ def location(db):
33993428
@clicommon.pass_db
34003429
def add_location(db, location):
34013430
""" Add snmp location"""
3431+
config_db = ValidatedConfigDBConnector(db.cfgdb)
34023432
if isinstance(location, tuple):
34033433
location = " ".join(location)
34043434
elif isinstance(location, list):
34053435
location = " ".join(location)
3406-
snmp = db.cfgdb.get_table("SNMP")
3436+
snmp = config_db.get_table("SNMP")
34073437
try:
34083438
if snmp['LOCATION']:
34093439
click.echo("Location already exists")
34103440
sys.exit(1)
34113441
except KeyError:
34123442
if "LOCATION" not in snmp.keys():
3413-
db.cfgdb.set_entry('SNMP', 'LOCATION', {'Location': location})
3414-
click.echo("SNMP Location {} has been added to configuration".format(location))
3443+
try:
3444+
config_db.set_entry('SNMP', 'LOCATION', {'Location': location})
3445+
click.echo("SNMP Location {} has been added to configuration".format(location))
3446+
except ValueError:
3447+
ctx = click.get_current_context()
3448+
ctx.fail("Failed to set SNMP location. Error: {}".format(e))
34153449
try:
34163450
click.echo("Restarting SNMP service...")
34173451
clicommon.run_command("systemctl reset-failed snmp.service", display_cmd=False)
@@ -3426,15 +3460,20 @@ def add_location(db, location):
34263460
@clicommon.pass_db
34273461
def delete_location(db, location):
34283462
""" Delete snmp location"""
3463+
config_db = ValidatedConfigDBConnector(db.cfgdb)
34293464
if isinstance(location, tuple):
34303465
location = " ".join(location)
34313466
elif isinstance(location, list):
34323467
location = " ".join(location)
34333468
snmp = db.cfgdb.get_table("SNMP")
34343469
try:
34353470
if location == snmp['LOCATION']['Location']:
3436-
db.cfgdb.set_entry('SNMP', 'LOCATION', None)
3437-
click.echo("SNMP Location {} removed from configuration".format(location))
3471+
try:
3472+
config_db.set_entry('SNMP', 'LOCATION', None)
3473+
click.echo("SNMP Location {} removed from configuration".format(location))
3474+
except (ValueError, JsonPatchConflict) as e:
3475+
ctx = click.get_current_context()
3476+
ctx.fail("Failed to remove SNMP location from configuration. Error: {}".format(e))
34383477
try:
34393478
click.echo("Restarting SNMP service...")
34403479
clicommon.run_command("systemctl reset-failed snmp.service", display_cmd=False)
@@ -3456,19 +3495,24 @@ def delete_location(db, location):
34563495
@clicommon.pass_db
34573496
def modify_location(db, location):
34583497
""" Modify snmp location"""
3498+
config_db = ValidatedConfigDBConnector(db.cfgdb)
34593499
if isinstance(location, tuple):
34603500
location = " ".join(location)
34613501
elif isinstance(location, list):
34623502
location = " ".join(location)
3463-
snmp = db.cfgdb.get_table("SNMP")
3503+
snmp = config_db.get_table("SNMP")
34643504
try:
34653505
snmp_location = snmp['LOCATION']['Location']
34663506
if location in snmp_location:
34673507
click.echo("SNMP location {} already exists".format(location))
34683508
sys.exit(1)
34693509
else:
3470-
db.cfgdb.mod_entry('SNMP', 'LOCATION', {'Location': location})
3471-
click.echo("SNMP location {} modified in configuration".format(location))
3510+
try:
3511+
config_db.mod_entry('SNMP', 'LOCATION', {'Location': location})
3512+
click.echo("SNMP location {} modified in configuration".format(location))
3513+
except ValueError as e:
3514+
ctx = click.get_current_context()
3515+
ctx.fail("Failed to modify SNMP location. Error: {}".format(e))
34723516
try:
34733517
click.echo("Restarting SNMP service...")
34743518
clicommon.run_command("systemctl reset-failed snmp.service", display_cmd=False)

tests/config_snmp_test.py

+11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import show.main as show
77
import clear.main as clear
88
import config.main as config
9+
import config.validated_config_db_connector as validated_config_db_connector
910

1011
import pytest
1112

@@ -865,6 +866,16 @@ def test_is_valid_email(self, invalid_email):
865866
output = config.is_valid_email(invalid_email)
866867
assert output == False
867868

869+
@patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True))
870+
@patch("config.validated_config_db_connector.ValidatedConfigDBConnector.validated_set_entry", mock.Mock(side_effect=ValueError))
871+
def test_config_snmp_community_add_new_community_with_invalid_type_yang_validation(self):
872+
config.ADHOC_VALIDATION = False
873+
runner = CliRunner()
874+
result = runner.invoke(config.config.commands["snmp"].commands["community"].commands["add"], ["Everest", "RT"])
875+
print(result.exit_code)
876+
assert result.exit_code != 0
877+
assert 'SNMP community configuration failed' in result.output
878+
868879
@classmethod
869880
def teardown_class(cls):
870881
print("TEARDOWN")

tests/config_test.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -1603,7 +1603,7 @@ def setup_class(cls):
16031603
import config.main
16041604
importlib.reload(config.main)
16051605

1606-
@mock.patch('config.main.ConfigDBConnector')
1606+
@mock.patch('config.main.ValidatedConfigDBConnector')
16071607
def test_hostname_add(self, db_conn_patch, get_cmd_module):
16081608
db_conn_patch().mod_entry = mock.Mock()
16091609
(config, show) = get_cmd_module
@@ -1625,6 +1625,19 @@ def test_hostname_add(self, db_conn_patch, get_cmd_module):
16251625
# Check new hostname was part of args
16261626
assert {'hostname': 'new_hostname'} in args
16271627

1628+
@patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True))
1629+
@patch("config.validated_config_db_connector.ValidatedConfigDBConnector.validated_mod_entry", mock.Mock(side_effect=ValueError))
1630+
def test_invalid_hostname_add_yang_validation(self):
1631+
config.ADHOC_VALIDATION = False
1632+
runner = CliRunner()
1633+
db = Db()
1634+
obj = {'db':db.cfgdb}
1635+
1636+
result = runner.invoke(config.config.commands["hostname"],
1637+
["invalid_hostname"], obj=obj)
1638+
assert result.exit_code != 0
1639+
assert "Failed to write new hostname" in result.output
1640+
16281641
@classmethod
16291642
def teardown_class(cls):
16301643
print("TEARDOWN")

tests/synchronous_mode_test.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from click.testing import CliRunner
2+
from utilities_common.db import Db
3+
from unittest import mock
4+
from mock import patch
25
import config.main as config
6+
import config.validated_config_db_connector as validated_config_db_connector
37

48
class TestSynchronousMode(object):
59
@classmethod
@@ -11,7 +15,7 @@ def __check_result(self, result_msg, mode):
1115
expected_msg = """Wrote %s synchronous mode into CONFIG_DB, swss restart required to apply the configuration: \n
1216
Option 1. config save -y \n
1317
config reload -y \n
14-
Option 2. systemctl restart swss""" % mode
18+
Option 2. systemctl restart swss""" % mode
1519
else:
1620
expected_msg = "Error: Invalid argument %s, expect either enable or disable" % mode
1721

@@ -35,3 +39,17 @@ def test_synchronous_mode(self):
3539
print(result.output)
3640
assert result.exit_code != 0
3741
assert self.__check_result(result.output, "invalid-input")
42+
43+
@patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True))
44+
@patch("config.validated_config_db_connector.ValidatedConfigDBConnector.validated_mod_entry", mock.Mock(side_effect=ValueError))
45+
def test_synchronous_mode_invalid_yang_validation(self):
46+
config.ADHOC_VALIDATION = False
47+
runner = CliRunner()
48+
db = Db()
49+
obj = {'db':db.cfgdb}
50+
51+
result = runner.invoke(config.config.commands["synchronous_mode"], ["invalid-input"], obj=obj)
52+
print(result.exit_code)
53+
print(result.output)
54+
assert result.exit_code != 0
55+
assert self.__check_result(result.output, "invalid-input")

tests/tpid_test.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ def test_tpid_config_lag_mbr(self):
101101

102102
def test_tpid_add_lag_mbr_with_non_default_tpid(self):
103103
db = Db()
104-
obj = {'db':db.cfgdb}
104+
obj = {'db': db.cfgdb}
105+
config.ADHOC_VALIDATION = True
105106
runner = CliRunner()
106107
result = runner.invoke(config.config.commands["portchannel"].commands["member"].commands["add"], ["PortChannel0001","Ethernet20"], obj=obj)
107108
print(result.exit_code)

tests/yang_config_validation_test.py

+19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
from click.testing import CliRunner
2+
from utilities_common.db import Db
3+
from unittest import mock
4+
from mock import patch
25
import config.main as config
6+
import config.validated_config_db_connector as validated_config_db_connector
37

48
class TestYangConfigValidation(object):
59
@classmethod
@@ -15,6 +19,7 @@ def __check_result(self, result_msg, mode):
1519
return expected_msg in result_msg
1620

1721
def test_yang_config_validation(self):
22+
config.ADHOC_VALIDATION = True
1823
runner = CliRunner()
1924

2025
result = runner.invoke(config.config.commands["yang_config_validation"], ["enable"])
@@ -31,3 +36,17 @@ def test_yang_config_validation(self):
3136
print(result.output)
3237
assert result.exit_code != 0
3338
assert self.__check_result(result.output, "invalid-input")
39+
40+
@patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True))
41+
@patch("config.validated_config_db_connector.ValidatedConfigDBConnector.validated_mod_entry", mock.Mock(side_effect=ValueError))
42+
def test_invalid_yang_config_validation_using_yang(self):
43+
config.ADHOC_VALIDATION = False
44+
runner = CliRunner()
45+
db = Db()
46+
obj = {'db':db.cfgdb}
47+
48+
result = runner.invoke(config.config.commands["yang_config_validation"], ["invalid-input"], obj=obj)
49+
print(result.exit_code)
50+
print(result.output)
51+
assert result.exit_code != 0
52+
assert self.__check_result(result.output, "invalid-input")

0 commit comments

Comments
 (0)