Skip to content

Commit 58dbb3e

Browse files
authored
YANG Validation for ConfigDB Updates: TACPLUS, TACPLUS_SERVER, AAA, VLAN_SUB_INTERFACE tables + decorated validated_mod_entry (sonic-net#2452)
1 parent 062f18a commit 58dbb3e

File tree

5 files changed

+243
-92
lines changed

5 files changed

+243
-92
lines changed

config/aaa.py

+35-14
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import ipaddress
33
import re
44
from swsscommon.swsscommon import ConfigDBConnector
5+
from .validated_config_db_connector import ValidatedConfigDBConnector
6+
from jsonpatch import JsonPatchConflict
57
import utilities_common.cli as clicommon
68

9+
ADHOC_VALIDATION = True
710
RADIUS_MAXSERVERS = 8
811
RADIUS_PASSKEY_MAX_LEN = 65
912
VALID_CHARS_MSG = "Valid chars are ASCII printable except SPACE, '#', and ','"
@@ -13,19 +16,27 @@ def is_secret(secret):
1316

1417

1518
def add_table_kv(table, entry, key, val):
16-
config_db = ConfigDBConnector()
19+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
1720
config_db.connect()
18-
config_db.mod_entry(table, entry, {key:val})
21+
try:
22+
config_db.mod_entry(table, entry, {key:val})
23+
except ValueError as e:
24+
ctx = click.get_current_context()
25+
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
1926

2027

2128
def del_table_key(table, entry, key):
22-
config_db = ConfigDBConnector()
29+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
2330
config_db.connect()
2431
data = config_db.get_entry(table, entry)
2532
if data:
2633
if key in data:
2734
del data[key]
28-
config_db.set_entry(table, entry, data)
35+
try:
36+
config_db.set_entry(table, entry, data)
37+
except (ValueError, JsonPatchConflict) as e:
38+
ctx = click.get_current_context()
39+
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
2940

3041
@click.group()
3142
def aaa():
@@ -246,11 +257,12 @@ def passkey(ctx, secret):
246257
@click.option('-m', '--use-mgmt-vrf', help="Management vrf, default is no vrf", is_flag=True)
247258
def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf):
248259
"""Specify a TACACS+ server"""
249-
if not clicommon.is_ipaddress(address):
250-
click.echo('Invalid ip address')
251-
return
260+
if ADHOC_VALIDATION:
261+
if not clicommon.is_ipaddress(address):
262+
click.echo('Invalid ip address') # TODO: MISSING CONSTRAINT IN YANG MODEL
263+
return
252264

253-
config_db = ConfigDBConnector()
265+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
254266
config_db.connect()
255267
old_data = config_db.get_entry('TACPLUS_SERVER', address)
256268
if old_data != {}:
@@ -268,7 +280,11 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf):
268280
data['passkey'] = key
269281
if use_mgmt_vrf :
270282
data['vrf'] = "mgmt"
271-
config_db.set_entry('TACPLUS_SERVER', address, data)
283+
try:
284+
config_db.set_entry('TACPLUS_SERVER', address, data)
285+
except ValueError as e:
286+
ctx = click.get_current_context()
287+
ctx.fail("Invalid ip address. Error: {}".format(e))
272288
tacacs.add_command(add)
273289

274290

@@ -278,13 +294,18 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf):
278294
@click.argument('address', metavar='<ip_address>')
279295
def delete(address):
280296
"""Delete a TACACS+ server"""
281-
if not clicommon.is_ipaddress(address):
282-
click.echo('Invalid ip address')
283-
return
297+
if ADHOC_VALIDATION:
298+
if not clicommon.is_ipaddress(address):
299+
click.echo('Invalid ip address')
300+
return
284301

285-
config_db = ConfigDBConnector()
302+
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
286303
config_db.connect()
287-
config_db.set_entry('TACPLUS_SERVER', address, None)
304+
try:
305+
config_db.set_entry('TACPLUS_SERVER', address, None)
306+
except JsonPatchConflict as e:
307+
ctx = click.get_current_context()
308+
ctx.fail("Invalid ip address. Error: {}".format(e))
288309
tacacs.add_command(delete)
289310

290311

config/main.py

+64-52
Original file line numberDiff line numberDiff line change
@@ -6760,73 +6760,82 @@ def is_subintf_shortname(intf):
67606760
@click.argument('vid', metavar='<vid>', required=False, type=click.IntRange(1,4094))
67616761
@click.pass_context
67626762
def add_subinterface(ctx, subinterface_name, vid):
6763+
config_db = ValidatedConfigDBConnector(ctx.obj['db'])
67636764
sub_intf_sep_idx = subinterface_name.find(VLAN_SUB_INTERFACE_SEPARATOR)
6764-
if sub_intf_sep_idx == -1:
6765-
ctx.fail("{} is invalid vlan subinterface".format(subinterface_name))
6766-
67676765
interface_alias = subinterface_name[:sub_intf_sep_idx]
6768-
if interface_alias is None:
6769-
ctx.fail("{} invalid subinterface".format(interface_alias))
6770-
6771-
if interface_alias.startswith("Po") is True:
6772-
intf_table_name = CFG_PORTCHANNEL_PREFIX
6773-
elif interface_alias.startswith("Eth") is True:
6774-
intf_table_name = 'PORT'
6775-
6776-
config_db = ctx.obj['db']
6777-
port_dict = config_db.get_table(intf_table_name)
6778-
parent_intf = get_intf_longname(interface_alias)
6779-
if interface_alias is not None:
6780-
if not port_dict:
6781-
ctx.fail("{} parent interface not found. {} table none".format(interface_alias, intf_table_name))
6782-
if parent_intf not in port_dict.keys():
6783-
ctx.fail("{} parent interface not found".format(subinterface_name))
6784-
6785-
# Validate if parent is portchannel member
6786-
portchannel_member_table = config_db.get_table('PORTCHANNEL_MEMBER')
6787-
if interface_is_in_portchannel(portchannel_member_table, parent_intf):
6788-
ctx.fail("{} is configured as a member of portchannel. Cannot configure subinterface"
6789-
.format(parent_intf))
6766+
if ADHOC_VALIDATION:
6767+
if sub_intf_sep_idx == -1:
6768+
ctx.fail("{} is invalid vlan subinterface".format(subinterface_name))
67906769

6791-
# Validate if parent is vlan member
6792-
vlan_member_table = config_db.get_table('VLAN_MEMBER')
6793-
if interface_is_in_vlan(vlan_member_table, parent_intf):
6794-
ctx.fail("{} is configured as a member of vlan. Cannot configure subinterface"
6795-
.format(parent_intf))
6770+
if interface_alias is None:
6771+
ctx.fail("{} invalid subinterface".format(interface_alias))
67966772

6797-
sub_intfs = [k for k,v in config_db.get_table('VLAN_SUB_INTERFACE').items() if type(k) != tuple]
6798-
if subinterface_name in sub_intfs:
6799-
ctx.fail("{} already exists".format(subinterface_name))
6773+
if interface_alias.startswith("Po") is True:
6774+
intf_table_name = CFG_PORTCHANNEL_PREFIX
6775+
elif interface_alias.startswith("Eth") is True:
6776+
intf_table_name = 'PORT'
6777+
else:
6778+
ctx.fail("{} is invalid vlan subinterface".format(subinterface_name))
6779+
6780+
port_dict = config_db.get_table(intf_table_name)
6781+
parent_intf = get_intf_longname(interface_alias)
6782+
if interface_alias is not None:
6783+
if not port_dict:
6784+
ctx.fail("{} parent interface not found. {} table none".format(interface_alias, intf_table_name))
6785+
if parent_intf not in port_dict.keys():
6786+
ctx.fail("{} parent interface not found".format(subinterface_name))
6787+
6788+
# Validate if parent is portchannel member
6789+
portchannel_member_table = config_db.get_table('PORTCHANNEL_MEMBER')
6790+
if interface_is_in_portchannel(portchannel_member_table, parent_intf): # TODO: MISSING CONSTRAINT IN YANG MODEL
6791+
ctx.fail("{} is configured as a member of portchannel. Cannot configure subinterface"
6792+
.format(parent_intf))
6793+
6794+
# Validate if parent is vlan member
6795+
vlan_member_table = config_db.get_table('VLAN_MEMBER')
6796+
if interface_is_in_vlan(vlan_member_table, parent_intf): # TODO: MISSING CONSTRAINT IN YANG MODEL
6797+
ctx.fail("{} is configured as a member of vlan. Cannot configure subinterface"
6798+
.format(parent_intf))
6799+
6800+
sub_intfs = [k for k,v in config_db.get_table('VLAN_SUB_INTERFACE').items() if type(k) != tuple]
6801+
if subinterface_name in sub_intfs:
6802+
ctx.fail("{} already exists".format(subinterface_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL
6803+
6804+
if subintf_vlan_check(config_db, get_intf_longname(interface_alias), vid) is True:
6805+
ctx.fail("Vlan {} encap already configured on other subinterface on {}".format(vid, interface_alias)) # TODO: MISSING CONSTRAINT IN YANG MODEL
6806+
6807+
if vid is None and is_subintf_shortname(subinterface_name):
6808+
ctx.fail("{} Encap vlan is mandatory or short name subinterfaces".format(subinterface_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL
68006809

68016810
subintf_dict = {}
68026811
if vid is not None:
68036812
subintf_dict.update({"vlan" : vid})
6804-
elif is_subintf_shortname(subinterface_name):
6805-
ctx.fail("{} Encap vlan is mandatory for short name subinterfaces".format(subinterface_name))
6806-
6807-
if subintf_vlan_check(config_db, get_intf_longname(interface_alias), vid) is True:
6808-
ctx.fail("Vlan {} encap already configured on other subinterface on {}".format(vid, interface_alias))
6809-
68106813
subintf_dict.update({"admin_status" : "up"})
6811-
config_db.set_entry('VLAN_SUB_INTERFACE', subinterface_name, subintf_dict)
6814+
6815+
try:
6816+
config_db.set_entry('VLAN_SUB_INTERFACE', subinterface_name, subintf_dict)
6817+
except ValueError as e:
6818+
ctx.fail("Invalid vlan subinterface. Error: {}".format(e))
68126819

68136820
@subinterface.command('del')
68146821
@click.argument('subinterface_name', metavar='<subinterface_name>', required=True)
68156822
@click.pass_context
68166823
def del_subinterface(ctx, subinterface_name):
6817-
sub_intf_sep_idx = subinterface_name.find(VLAN_SUB_INTERFACE_SEPARATOR)
6818-
if sub_intf_sep_idx == -1:
6819-
ctx.fail("{} is invalid vlan subinterface".format(subinterface_name))
6824+
config_db = ValidatedConfigDBConnector(ctx.obj['db'])
68206825

6821-
config_db = ctx.obj['db']
6822-
#subinterface_name = subintf_get_shortname(subinterface_name)
6823-
if interface_name_is_valid(config_db, subinterface_name) is False:
6824-
ctx.fail("{} is invalid ".format(subinterface_name))
6826+
if ADHOC_VALIDATION:
6827+
sub_intf_sep_idx = subinterface_name.find(VLAN_SUB_INTERFACE_SEPARATOR)
6828+
if sub_intf_sep_idx == -1:
6829+
ctx.fail("{} is invalid vlan subinterface".format(subinterface_name))
6830+
6831+
#subinterface_name = subintf_get_shortname(subinterface_name)
6832+
if interface_name_is_valid(config_db, subinterface_name) is False:
6833+
ctx.fail("{} is invalid ".format(subinterface_name))
68256834

6826-
subintf_config_db = config_db.get_table('VLAN_SUB_INTERFACE')
6827-
sub_intfs = [k for k,v in subintf_config_db.items() if type(k) != tuple]
6828-
if subinterface_name not in sub_intfs:
6829-
ctx.fail("{} does not exists".format(subinterface_name))
6835+
subintf_config_db = config_db.get_table('VLAN_SUB_INTERFACE')
6836+
sub_intfs = [k for k,v in subintf_config_db.items() if type(k) != tuple]
6837+
if subinterface_name not in sub_intfs:
6838+
ctx.fail("{} does not exists".format(subinterface_name))
68306839

68316840
ips = {}
68326841
ips = [ k[1] for k in config_db.get_table('VLAN_SUB_INTERFACE') if type(k) == tuple and k[0] == subinterface_name ]
@@ -6842,7 +6851,10 @@ def del_subinterface(ctx, subinterface_name):
68426851
for ip in ips:
68436852
config_db.set_entry('INTERFACE', (subinterface_name, ip), None)
68446853

6845-
config_db.set_entry('VLAN_SUB_INTERFACE', subinterface_name, None)
6854+
try:
6855+
config_db.set_entry('VLAN_SUB_INTERFACE', subinterface_name, None)
6856+
except JsonPatchConflict as e:
6857+
ctx.fail("{} is invalid vlan subinterface. Error: {}".format(subinterface_name, e))
68466858

68476859
if __name__ == '__main__':
68486860
config()
+73-19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import jsonpatch
2+
import copy
23
from jsonpointer import JsonPointer
34

45
from sonic_py_common import device_info
@@ -17,33 +18,83 @@ def __getattr__(self, name):
1718
return self.validated_set_entry
1819
if name == "delete_table":
1920
return self.validated_delete_table
21+
if name == "mod_entry":
22+
return self.validated_mod_entry
2023
return self.connector.__getattribute__(name)
2124

25+
def stringify_value(self, value):
26+
if isinstance(value, dict):
27+
value = {str(k):str(v) for k, v in value.items()}
28+
else:
29+
value = str(value)
30+
return value
31+
2232
def make_path_value_jsonpatch_compatible(self, table, key, value):
2333
if type(key) == tuple:
2434
path = JsonPointer.from_parts([table, '|'.join(key)]).path
35+
elif type(key) == list:
36+
path = JsonPointer.from_parts([table, *key]).path
2537
else:
2638
path = JsonPointer.from_parts([table, key]).path
2739
if value == {"NULL" : "NULL"}:
2840
value = {}
41+
else:
42+
value = self.stringify_value(value)
2943
return path, value
3044

31-
def create_gcu_patch(self, op, table, key=None, value=None):
32-
if key:
33-
path, value = self.make_path_value_jsonpatch_compatible(table, key, value)
34-
else:
35-
path = "/{}".format(table)
36-
45+
def create_gcu_patch(self, op, table, key=None, value=None, mod_entry=False):
3746
gcu_json_input = []
38-
gcu_json = {"op": "{}".format(op),
39-
"path": "{}".format(path)}
40-
if op == "add":
41-
gcu_json["value"] = value
47+
"""Add patch element to create new table if necessary, as GCU is unable to add to nonexistent table"""
48+
if op == "add" and not self.get_table(table):
49+
gcu_json = {"op": "{}".format(op),
50+
"path": "/{}".format(table),
51+
"value": {}}
52+
gcu_json_input.append(gcu_json)
53+
54+
"""Add patch element to create ConfigDB path if necessary, as GCU is unable to add to a nonexistent path"""
55+
if op == "add" and not self.get_entry(table, key):
56+
path = JsonPointer.from_parts([table, key]).path
57+
gcu_json = {"op": "{}".format(op),
58+
"path": "{}".format(path),
59+
"value": {}}
60+
gcu_json_input.append(gcu_json)
61+
62+
def add_patch_entry():
63+
if key:
64+
patch_path, patch_value = self.make_path_value_jsonpatch_compatible(table, key, value)
65+
else:
66+
patch_path = "/{}".format(table)
67+
68+
gcu_json = {"op": "{}".format(op),
69+
"path": "{}".format(patch_path)}
70+
if op == "add":
71+
gcu_json["value"] = patch_value
72+
73+
gcu_json_input.append(gcu_json)
74+
75+
"""mod_entry makes path more granular so that preexisting fields in db are not removed"""
76+
if mod_entry:
77+
key_start = key
78+
value_copy = copy.deepcopy(value)
79+
for key_end, cleaned_value in value_copy.items():
80+
key = [key_start, key_end]
81+
value = cleaned_value
82+
add_patch_entry()
83+
else:
84+
add_patch_entry()
4285

43-
gcu_json_input.append(gcu_json)
4486
gcu_patch = jsonpatch.JsonPatch(gcu_json_input)
4587
return gcu_patch
4688

89+
def apply_patch(self, gcu_patch, table):
90+
format = ConfigFormat.CONFIGDB.name
91+
config_format = ConfigFormat[format.upper()]
92+
93+
try:
94+
GenericUpdater().apply_patch(patch=gcu_patch, config_format=config_format, verbose=False, dry_run=False, ignore_non_yang_tables=False, ignore_paths=None)
95+
except EmptyTableError:
96+
self.validated_delete_table(table)
97+
4798
def validated_delete_table(self, table):
4899
gcu_patch = self.create_gcu_patch("remove", table)
49100
format = ConfigFormat.CONFIGDB.name
@@ -54,17 +105,20 @@ def validated_delete_table(self, table):
54105
logger = genericUpdaterLogging.get_logger(title="Patch Applier", print_all_to_console=True)
55106
logger.log_notice("Unable to remove entry, as doing so will result in invalid config. Error: {}".format(e))
56107

108+
def validated_mod_entry(self, table, key, value):
109+
if value is not None:
110+
op = "add"
111+
else:
112+
op = "remove"
113+
114+
gcu_patch = self.create_gcu_patch(op, table, key, value, mod_entry=True)
115+
self.apply_patch(gcu_patch, table)
116+
57117
def validated_set_entry(self, table, key, value):
58118
if value is not None:
59119
op = "add"
60120
else:
61121
op = "remove"
62-
63-
gcu_patch = self.create_gcu_patch(op, table, key, value)
64-
format = ConfigFormat.CONFIGDB.name
65-
config_format = ConfigFormat[format.upper()]
66122

67-
try:
68-
GenericUpdater().apply_patch(patch=gcu_patch, config_format=config_format, verbose=False, dry_run=False, ignore_non_yang_tables=False, ignore_paths=None)
69-
except EmptyTableError:
70-
self.validated_delete_table(table)
123+
gcu_patch = self.create_gcu_patch(op, table, key, value)
124+
self.apply_patch(gcu_patch, table)

0 commit comments

Comments
 (0)