Skip to content

Commit 123504a

Browse files
authored
YANG validation for ConfigDB Updates: portchannel add/remove, loopback interface, VLAN
YANG validation for ConfigDB Updates: portchannel add/remove, loopback interface, VLAN
1 parent 28f6820 commit 123504a

8 files changed

+415
-134
lines changed

config/main.py

+77-49
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import itertools
1515
import copy
1616

17+
from jsonpatch import JsonPatchConflict
1718
from collections import OrderedDict
1819
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat
1920
from minigraph import parse_device_desc_xml, minigraph_encoder
@@ -31,6 +32,7 @@
3132
import utilities_common.cli as clicommon
3233
from utilities_common.helper import get_port_pbh_binding, get_port_acl_binding
3334
from utilities_common.general import load_db_config, load_module_from_source
35+
from .validated_config_db_connector import ValidatedConfigDBConnector
3436
import utilities_common.multi_asic as multi_asic_util
3537

3638
from .utils import log
@@ -104,6 +106,7 @@
104106
TTL_RANGE = click.IntRange(min=0, max=255)
105107
QUEUE_RANGE = click.IntRange(min=0, max=255)
106108
GRE_TYPE_RANGE = click.IntRange(min=0, max=65535)
109+
ADHOC_VALIDATION = True
107110

108111
# Load sonic-cfggen from source since /usr/local/bin/sonic-cfggen does not have .py extension.
109112
sonic_cfggen = load_module_from_source('sonic_cfggen', '/usr/local/bin/sonic-cfggen')
@@ -2040,51 +2043,64 @@ def portchannel(db, ctx, namespace):
20402043
@click.pass_context
20412044
def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate):
20422045
"""Add port channel"""
2043-
if is_portchannel_name_valid(portchannel_name) != True:
2044-
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'"
2045-
.format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
2046-
2047-
db = ctx.obj['db']
2048-
2049-
if is_portchannel_present_in_db(db, portchannel_name):
2050-
ctx.fail("{} already exists!".format(portchannel_name))
2051-
2046+
20522047
fvs = {
20532048
'admin_status': 'up',
20542049
'mtu': '9100',
20552050
'lacp_key': 'auto',
20562051
'fast_rate': fast_rate.lower(),
20572052
}
2053+
20582054
if min_links != 0:
20592055
fvs['min_links'] = str(min_links)
20602056
if fallback != 'false':
20612057
fvs['fallback'] = 'true'
2062-
db.set_entry('PORTCHANNEL', portchannel_name, fvs)
2063-
2058+
2059+
if ADHOC_VALIDATION:
2060+
db = ctx.obj['db']
2061+
if is_portchannel_name_valid(portchannel_name) != True:
2062+
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'"
2063+
.format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
2064+
if is_portchannel_present_in_db(db, portchannel_name):
2065+
ctx.fail("{} already exists!".format(portchannel_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL
2066+
else:
2067+
db = ValidatedConfigDBConnector(ctx.obj['db'])
2068+
2069+
try:
2070+
db.set_entry('PORTCHANNEL', portchannel_name, fvs)
2071+
except ValueError:
2072+
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'".format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
2073+
20642074
@portchannel.command('del')
20652075
@click.argument('portchannel_name', metavar='<portchannel_name>', required=True)
20662076
@click.pass_context
20672077
def remove_portchannel(ctx, portchannel_name):
20682078
"""Remove port channel"""
2069-
if is_portchannel_name_valid(portchannel_name) != True:
2070-
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'"
2071-
.format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
2072-
2073-
db = ctx.obj['db']
2074-
2075-
# Dont proceed if the port channel does not exist
2076-
if is_portchannel_present_in_db(db, portchannel_name) is False:
2077-
ctx.fail("{} is not present.".format(portchannel_name))
2078-
2079-
# Dont let to remove port channel if vlan membership exists
2080-
for k,v in db.get_table('VLAN_MEMBER'):
2081-
if v == portchannel_name:
2082-
ctx.fail("{} has vlan {} configured, remove vlan membership to proceed".format(portchannel_name, str(k)))
2083-
2084-
if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0:
2085-
click.echo("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name))
2079+
2080+
if ADHOC_VALIDATION:
2081+
db = ctx.obj['db']
2082+
if is_portchannel_name_valid(portchannel_name) != True:
2083+
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'"
2084+
.format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
2085+
2086+
# Don't proceed if the port channel does not exist
2087+
if is_portchannel_present_in_db(db, portchannel_name) is False:
2088+
ctx.fail("{} is not present.".format(portchannel_name))
2089+
2090+
# Dont let to remove port channel if vlan membership exists
2091+
for k,v in db.get_table('VLAN_MEMBER'): # TODO: MISSING CONSTRAINT IN YANG MODEL
2092+
if v == portchannel_name:
2093+
ctx.fail("{} has vlan {} configured, remove vlan membership to proceed".format(portchannel_name, str(k)))
2094+
2095+
if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0: # TODO: MISSING CONSTRAINT IN YANG MODEL
2096+
ctx.fail("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name))
20862097
else:
2098+
db = ValidatedConfigDBConnector(ctx.obj['db'])
2099+
2100+
try:
20872101
db.set_entry('PORTCHANNEL', portchannel_name, None)
2102+
except JsonPatchConflict:
2103+
ctx.fail("{} is not present.".format(portchannel_name))
20882104

20892105
@portchannel.group(cls=clicommon.AbbreviationGroup, name='member')
20902106
@click.pass_context
@@ -2113,8 +2129,8 @@ def add_portchannel_member(ctx, portchannel_name, port_name):
21132129
# Dont proceed if the port channel does not exist
21142130
if is_portchannel_present_in_db(db, portchannel_name) is False:
21152131
ctx.fail("{} is not present.".format(portchannel_name))
2116-
2117-
# Dont allow a port to be member of port channel if it is configured with an IP address
2132+
2133+
# Don't allow a port to be member of port channel if it is configured with an IP address
21182134
for key,value in db.get_table('INTERFACE').items():
21192135
if type(key) == tuple:
21202136
continue
@@ -6157,36 +6173,48 @@ def loopback(ctx, redis_unix_socket_path):
61576173
@click.argument('loopback_name', metavar='<loopback_name>', required=True)
61586174
@click.pass_context
61596175
def add_loopback(ctx, loopback_name):
6160-
config_db = ctx.obj['db']
6161-
if is_loopback_name_valid(loopback_name) is False:
6162-
ctx.fail("{} is invalid, name should have prefix '{}' and suffix '{}' "
6163-
.format(loopback_name, CFG_LOOPBACK_PREFIX, CFG_LOOPBACK_NO))
6164-
6165-
lo_intfs = [k for k, v in config_db.get_table('LOOPBACK_INTERFACE').items() if type(k) != tuple]
6166-
if loopback_name in lo_intfs:
6167-
ctx.fail("{} already exists".format(loopback_name))
6168-
6169-
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, {"NULL" : "NULL"})
6176+
if ADHOC_VALIDATION:
6177+
config_db = ctx.obj['db']
6178+
if is_loopback_name_valid(loopback_name) is False:
6179+
ctx.fail("{} is invalid, name should have prefix '{}' and suffix '{}' "
6180+
.format(loopback_name, CFG_LOOPBACK_PREFIX, CFG_LOOPBACK_NO))
6181+
6182+
lo_intfs = [k for k, v in config_db.get_table('LOOPBACK_INTERFACE').items() if type(k) != tuple]
6183+
if loopback_name in lo_intfs:
6184+
ctx.fail("{} already exists".format(loopback_name)) # TODO: MISSING CONSTRAINT IN YANG VALIDATION
6185+
else:
6186+
config_db = ValidatedConfigDBConnector(ctx.obj['db'])
6187+
6188+
try:
6189+
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, {"NULL" : "NULL"})
6190+
except ValueError:
6191+
ctx.fail("{} is invalid, name should have prefix '{}' and suffix '{}' ".format(loopback_name, CFG_LOOPBACK_PREFIX, CFG_LOOPBACK_NO))
61706192

61716193
@loopback.command('del')
61726194
@click.argument('loopback_name', metavar='<loopback_name>', required=True)
61736195
@click.pass_context
61746196
def del_loopback(ctx, loopback_name):
61756197
config_db = ctx.obj['db']
6176-
if is_loopback_name_valid(loopback_name) is False:
6177-
ctx.fail("{} is invalid, name should have prefix '{}' and suffix '{}' "
6178-
.format(loopback_name, CFG_LOOPBACK_PREFIX, CFG_LOOPBACK_NO))
6179-
61806198
lo_config_db = config_db.get_table('LOOPBACK_INTERFACE')
6181-
lo_intfs = [k for k, v in lo_config_db.items() if type(k) != tuple]
6182-
if loopback_name not in lo_intfs:
6183-
ctx.fail("{} does not exists".format(loopback_name))
6199+
6200+
if ADHOC_VALIDATION:
6201+
if is_loopback_name_valid(loopback_name) is False:
6202+
ctx.fail("{} is invalid, name should have prefix '{}' and suffix '{}' "
6203+
.format(loopback_name, CFG_LOOPBACK_PREFIX, CFG_LOOPBACK_NO))
6204+
lo_intfs = [k for k, v in lo_config_db.items() if type(k) != tuple]
6205+
if loopback_name not in lo_intfs:
6206+
ctx.fail("{} does not exist".format(loopback_name))
6207+
else:
6208+
config_db = ValidatedConfigDBConnector(ctx.obj['db'])
61846209

61856210
ips = [ k[1] for k in lo_config_db if type(k) == tuple and k[0] == loopback_name ]
61866211
for ip in ips:
61876212
config_db.set_entry('LOOPBACK_INTERFACE', (loopback_name, ip), None)
6188-
6189-
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, None)
6213+
6214+
try:
6215+
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, None)
6216+
except JsonPatchConflict:
6217+
ctx.fail("{} does not exist".format(loopback_name))
61906218

61916219

61926220
@config.group(cls=clicommon.AbbreviationGroup)
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import jsonpatch
2+
from jsonpointer import JsonPointer
3+
4+
from sonic_py_common import device_info
5+
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat
6+
from generic_config_updater.gu_common import EmptyTableError, genericUpdaterLogging
7+
8+
def ValidatedConfigDBConnector(config_db_connector):
9+
yang_enabled = device_info.is_yang_config_validation_enabled(config_db_connector)
10+
if yang_enabled:
11+
config_db_connector.set_entry = validated_set_entry
12+
config_db_connector.delete_table = validated_delete_table
13+
return config_db_connector
14+
15+
def make_path_value_jsonpatch_compatible(table, key, value):
16+
if type(key) == tuple:
17+
path = JsonPointer.from_parts([table, '|'.join(key)]).path
18+
else:
19+
path = JsonPointer.from_parts([table, key]).path
20+
if value == {"NULL" : "NULL"}:
21+
value = {}
22+
return path, value
23+
24+
def create_gcu_patch(op, table, key=None, value=None):
25+
if key:
26+
path, value = make_path_value_jsonpatch_compatible(table, key, value)
27+
else:
28+
path = "/{}".format(table)
29+
30+
gcu_json_input = []
31+
gcu_json = {"op": "{}".format(op),
32+
"path": "{}".format(path)}
33+
if op == "add":
34+
gcu_json["value"] = value
35+
36+
gcu_json_input.append(gcu_json)
37+
gcu_patch = jsonpatch.JsonPatch(gcu_json_input)
38+
return gcu_patch
39+
40+
def validated_delete_table(table):
41+
gcu_patch = create_gcu_patch("remove", table)
42+
format = ConfigFormat.CONFIGDB.name
43+
config_format = ConfigFormat[format.upper()]
44+
try:
45+
GenericUpdater().apply_patch(patch=gcu_patch, config_format=config_format, verbose=False, dry_run=False, ignore_non_yang_tables=False, ignore_paths=None)
46+
except ValueError as e:
47+
logger = genericUpdaterLogging.get_logger(title="Patch Applier", print_all_to_console=True)
48+
logger.log_notice("Unable to remove entry, as doing so will result in invalid config. Error: {}".format(e))
49+
50+
def validated_set_entry(table, key, value):
51+
if value is not None:
52+
op = "add"
53+
else:
54+
op = "remove"
55+
56+
gcu_patch = create_gcu_patch(op, table, key, value)
57+
format = ConfigFormat.CONFIGDB.name
58+
config_format = ConfigFormat[format.upper()]
59+
60+
try:
61+
GenericUpdater().apply_patch(patch=gcu_patch, config_format=config_format, verbose=False, dry_run=False, ignore_non_yang_tables=False, ignore_paths=None)
62+
except EmptyTableError:
63+
validated_delete_table(table)

0 commit comments

Comments
 (0)