Skip to content

Commit d48a830

Browse files
Add Multi ASIC support for apply-patch (#3249)
* Add Multi ASIC support for apply-patch * Add more test cases. * Ignore mock diff exception * Address comments. * Fix errors * Add empty case handle * Refactor extract scope * Fix UT * Fix extract for single asic * Adding localhost into log if scope is empty * Fix format in log * Fix log * Fix log * Fix variable
1 parent b143ea6 commit d48a830

File tree

8 files changed

+726
-141
lines changed

8 files changed

+726
-141
lines changed

config/main.py

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from jsonpatch import JsonPatchConflict
2020
from jsonpointer import JsonPointerException
2121
from collections import OrderedDict
22-
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat
22+
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat, extract_scope
2323
from minigraph import parse_device_desc_xml, minigraph_encoder
2424
from natsort import natsorted
2525
from portconfig import get_child_ports
@@ -1152,6 +1152,24 @@ def validate_gre_type(ctx, _, value):
11521152
return gre_type_value
11531153
except ValueError:
11541154
raise click.UsageError("{} is not a valid GRE type".format(value))
1155+
1156+
# Function to apply patch for a single ASIC.
1157+
def apply_patch_for_scope(scope_changes, results, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path):
1158+
scope, changes = scope_changes
1159+
# Replace localhost to DEFAULT_NAMESPACE which is db definition of Host
1160+
if scope.lower() == "localhost" or scope == "":
1161+
scope = multi_asic.DEFAULT_NAMESPACE
1162+
1163+
scope_for_log = scope if scope else "localhost"
1164+
try:
1165+
# Call apply_patch with the ASIC-specific changes and predefined parameters
1166+
GenericUpdater(namespace=scope).apply_patch(jsonpatch.JsonPatch(changes), config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)
1167+
results[scope_for_log] = {"success": True, "message": "Success"}
1168+
log.log_notice(f"'apply-patch' executed successfully for {scope_for_log} by {changes}")
1169+
except Exception as e:
1170+
results[scope_for_log] = {"success": False, "message": str(e)}
1171+
log.log_error(f"'apply-patch' executed failed for {scope_for_log} by {changes} due to {str(e)}")
1172+
11551173

11561174
# This is our main entrypoint - the main 'config' command
11571175
@click.group(cls=clicommon.AbbreviationGroup, context_settings=CONTEXT_SETTINGS)
@@ -1357,12 +1375,47 @@ def apply_patch(ctx, patch_file_path, format, dry_run, ignore_non_yang_tables, i
13571375
patch_as_json = json.loads(text)
13581376
patch = jsonpatch.JsonPatch(patch_as_json)
13591377

1378+
results = {}
13601379
config_format = ConfigFormat[format.upper()]
1361-
GenericUpdater().apply_patch(patch, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)
1380+
# Initialize a dictionary to hold changes categorized by scope
1381+
changes_by_scope = {}
1382+
1383+
# Iterate over each change in the JSON Patch
1384+
for change in patch:
1385+
scope, modified_path = extract_scope(change["path"])
1386+
1387+
# Modify the 'path' in the change to remove the scope
1388+
change["path"] = modified_path
1389+
1390+
# Check if the scope is already in our dictionary, if not, initialize it
1391+
if scope not in changes_by_scope:
1392+
changes_by_scope[scope] = []
13621393

1394+
# Add the modified change to the appropriate list based on scope
1395+
changes_by_scope[scope].append(change)
1396+
1397+
# Empty case to force validate YANG model.
1398+
if not changes_by_scope:
1399+
asic_list = [multi_asic.DEFAULT_NAMESPACE]
1400+
asic_list.extend(multi_asic.get_namespace_list())
1401+
for asic in asic_list:
1402+
changes_by_scope[asic] = []
1403+
1404+
# Apply changes for each scope
1405+
for scope_changes in changes_by_scope.items():
1406+
apply_patch_for_scope(scope_changes, results, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)
1407+
1408+
# Check if any updates failed
1409+
failures = [scope for scope, result in results.items() if not result['success']]
1410+
1411+
if failures:
1412+
failure_messages = '\n'.join([f"- {failed_scope}: {results[failed_scope]['message']}" for failed_scope in failures])
1413+
raise Exception(f"Failed to apply patch on the following scopes:\n{failure_messages}")
1414+
1415+
log.log_notice(f"Patch applied successfully for {patch}.")
13631416
click.secho("Patch applied successfully.", fg="cyan", underline=True)
13641417
except Exception as ex:
1365-
click.secho("Failed to apply patch", fg="red", underline=True, err=True)
1418+
click.secho("Failed to apply patch due to: {}".format(ex), fg="red", underline=True, err=True)
13661419
ctx.fail(ex)
13671420

13681421
@config.command()
@@ -2078,15 +2131,15 @@ def synchronous_mode(sync_mode):
20782131
if ADHOC_VALIDATION:
20792132
if sync_mode != 'enable' and sync_mode != 'disable':
20802133
raise click.BadParameter("Error: Invalid argument %s, expect either enable or disable" % sync_mode)
2081-
2134+
20822135
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
20832136
config_db.connect()
20842137
try:
20852138
config_db.mod_entry('DEVICE_METADATA' , 'localhost', {"synchronous_mode" : sync_mode})
20862139
except ValueError as e:
20872140
ctx = click.get_current_context()
20882141
ctx.fail("Error: Invalid argument %s, expect either enable or disable" % sync_mode)
2089-
2142+
20902143
click.echo("""Wrote %s synchronous mode into CONFIG_DB, swss restart required to apply the configuration: \n
20912144
Option 1. config save -y \n
20922145
config reload -y \n
@@ -2152,7 +2205,7 @@ def portchannel(db, ctx, namespace):
21522205
@click.pass_context
21532206
def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate):
21542207
"""Add port channel"""
2155-
2208+
21562209
fvs = {
21572210
'admin_status': 'up',
21582211
'mtu': '9100',
@@ -2164,26 +2217,26 @@ def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate):
21642217
fvs['min_links'] = str(min_links)
21652218
if fallback != 'false':
21662219
fvs['fallback'] = 'true'
2167-
2220+
21682221
db = ValidatedConfigDBConnector(ctx.obj['db'])
21692222
if ADHOC_VALIDATION:
21702223
if is_portchannel_name_valid(portchannel_name) != True:
21712224
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'"
21722225
.format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
21732226
if is_portchannel_present_in_db(db, portchannel_name):
21742227
ctx.fail("{} already exists!".format(portchannel_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL
2175-
2228+
21762229
try:
21772230
db.set_entry('PORTCHANNEL', portchannel_name, fvs)
21782231
except ValueError:
21792232
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'".format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
2180-
2233+
21812234
@portchannel.command('del')
21822235
@click.argument('portchannel_name', metavar='<portchannel_name>', required=True)
21832236
@click.pass_context
21842237
def remove_portchannel(ctx, portchannel_name):
21852238
"""Remove port channel"""
2186-
2239+
21872240
db = ValidatedConfigDBConnector(ctx.obj['db'])
21882241
if ADHOC_VALIDATION:
21892242
if is_portchannel_name_valid(portchannel_name) != True:
@@ -2201,7 +2254,7 @@ def remove_portchannel(ctx, portchannel_name):
22012254

22022255
if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0: # TODO: MISSING CONSTRAINT IN YANG MODEL
22032256
ctx.fail("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name))
2204-
2257+
22052258
try:
22062259
db.set_entry('PORTCHANNEL', portchannel_name, None)
22072260
except JsonPatchConflict:
@@ -2219,7 +2272,7 @@ def portchannel_member(ctx):
22192272
def add_portchannel_member(ctx, portchannel_name, port_name):
22202273
"""Add member to port channel"""
22212274
db = ValidatedConfigDBConnector(ctx.obj['db'])
2222-
2275+
22232276
if ADHOC_VALIDATION:
22242277
if clicommon.is_port_mirror_dst_port(db, port_name):
22252278
ctx.fail("{} is configured as mirror destination port".format(port_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL
@@ -2236,7 +2289,7 @@ def add_portchannel_member(ctx, portchannel_name, port_name):
22362289
# Dont proceed if the port channel does not exist
22372290
if is_portchannel_present_in_db(db, portchannel_name) is False:
22382291
ctx.fail("{} is not present.".format(portchannel_name))
2239-
2292+
22402293
# Don't allow a port to be member of port channel if it is configured with an IP address
22412294
for key,value in db.get_table('INTERFACE').items():
22422295
if type(key) == tuple:
@@ -2274,7 +2327,7 @@ def add_portchannel_member(ctx, portchannel_name, port_name):
22742327
member_port_speed = member_port_entry.get(PORT_SPEED)
22752328

22762329
port_speed = port_entry.get(PORT_SPEED) # TODO: MISSING CONSTRAINT IN YANG MODEL
2277-
if member_port_speed != port_speed:
2330+
if member_port_speed != port_speed:
22782331
ctx.fail("Port speed of {} is different than the other members of the portchannel {}"
22792332
.format(port_name, portchannel_name))
22802333

@@ -2347,7 +2400,7 @@ def del_portchannel_member(ctx, portchannel_name, port_name):
23472400
# Dont proceed if the the port is not an existing member of the port channel
23482401
if not is_port_member_of_this_portchannel(db, port_name, portchannel_name):
23492402
ctx.fail("{} is not a member of portchannel {}".format(port_name, portchannel_name))
2350-
2403+
23512404
try:
23522405
db.set_entry('PORTCHANNEL_MEMBER', portchannel_name + '|' + port_name, None)
23532406
except JsonPatchConflict:
@@ -2534,7 +2587,7 @@ def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer
25342587
if not namespaces['front_ns']:
25352588
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
25362589
config_db.connect()
2537-
if ADHOC_VALIDATION:
2590+
if ADHOC_VALIDATION:
25382591
if validate_mirror_session_config(config_db, session_name, None, src_port, direction) is False:
25392592
return
25402593
try:
@@ -3504,7 +3557,7 @@ def del_community(db, community):
35043557
if community not in snmp_communities:
35053558
click.echo("SNMP community {} is not configured".format(community))
35063559
sys.exit(1)
3507-
3560+
35083561
config_db = ValidatedConfigDBConnector(db.cfgdb)
35093562
try:
35103563
config_db.set_entry('SNMP_COMMUNITY', community, None)
@@ -4562,7 +4615,7 @@ def fec(ctx, interface_name, interface_fec, verbose):
45624615
def ip(ctx):
45634616
"""Set IP interface attributes"""
45644617
pass
4565-
4618+
45664619
def validate_vlan_exists(db,text):
45674620
data = db.get_table('VLAN')
45684621
keys = list(data.keys())
@@ -4630,12 +4683,12 @@ def add(ctx, interface_name, ip_addr, gw):
46304683
table_name = get_interface_table_name(interface_name)
46314684
if table_name == "":
46324685
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")
4633-
4686+
46344687
if table_name == "VLAN_INTERFACE":
46354688
if not validate_vlan_exists(config_db, interface_name):
46364689
ctx.fail(f"Error: {interface_name} does not exist. Vlan must be created before adding an IP address")
46374690
return
4638-
4691+
46394692
interface_entry = config_db.get_entry(table_name, interface_name)
46404693
if len(interface_entry) == 0:
46414694
if table_name == "VLAN_SUB_INTERFACE":
@@ -5057,7 +5110,7 @@ def cable_length(ctx, interface_name, length):
50575110

50585111
if not is_dynamic_buffer_enabled(config_db):
50595112
ctx.fail("This command can only be supported on a system with dynamic buffer enabled")
5060-
5113+
50615114
if ADHOC_VALIDATION:
50625115
# Check whether port is legal
50635116
ports = config_db.get_entry("PORT", interface_name)
@@ -5402,7 +5455,7 @@ def unbind(ctx, interface_name):
54025455
config_db.set_entry(table_name, interface_name, subintf_entry)
54035456
else:
54045457
config_db.set_entry(table_name, interface_name, None)
5405-
5458+
54065459
click.echo("Interface {} IP disabled and address(es) removed due to unbinding VRF.".format(interface_name))
54075460
#
54085461
# 'ipv6' subgroup ('config interface ipv6 ...')
@@ -6580,7 +6633,7 @@ def add_loopback(ctx, loopback_name):
65806633
lo_intfs = [k for k, v in config_db.get_table('LOOPBACK_INTERFACE').items() if type(k) != tuple]
65816634
if loopback_name in lo_intfs:
65826635
ctx.fail("{} already exists".format(loopback_name)) # TODO: MISSING CONSTRAINT IN YANG VALIDATION
6583-
6636+
65846637
try:
65856638
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, {"NULL" : "NULL"})
65866639
except ValueError:
@@ -6604,7 +6657,7 @@ def del_loopback(ctx, loopback_name):
66046657
ips = [ k[1] for k in lo_config_db if type(k) == tuple and k[0] == loopback_name ]
66056658
for ip in ips:
66066659
config_db.set_entry('LOOPBACK_INTERFACE', (loopback_name, ip), None)
6607-
6660+
66086661
try:
66096662
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, None)
66106663
except JsonPatchConflict:
@@ -6662,9 +6715,9 @@ def ntp(ctx):
66626715
def add_ntp_server(ctx, ntp_ip_address):
66636716
""" Add NTP server IP """
66646717
if ADHOC_VALIDATION:
6665-
if not clicommon.is_ipaddress(ntp_ip_address):
6718+
if not clicommon.is_ipaddress(ntp_ip_address):
66666719
ctx.fail('Invalid IP address')
6667-
db = ValidatedConfigDBConnector(ctx.obj['db'])
6720+
db = ValidatedConfigDBConnector(ctx.obj['db'])
66686721
ntp_servers = db.get_table("NTP_SERVER")
66696722
if ntp_ip_address in ntp_servers:
66706723
click.echo("NTP server {} is already configured".format(ntp_ip_address))
@@ -6675,7 +6728,7 @@ def add_ntp_server(ctx, ntp_ip_address):
66756728
{'resolve_as': ntp_ip_address,
66766729
'association_type': 'server'})
66776730
except ValueError as e:
6678-
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
6731+
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
66796732
click.echo("NTP server {} added to configuration".format(ntp_ip_address))
66806733
try:
66816734
click.echo("Restarting ntp-config service...")
@@ -6691,7 +6744,7 @@ def del_ntp_server(ctx, ntp_ip_address):
66916744
if ADHOC_VALIDATION:
66926745
if not clicommon.is_ipaddress(ntp_ip_address):
66936746
ctx.fail('Invalid IP address')
6694-
db = ValidatedConfigDBConnector(ctx.obj['db'])
6747+
db = ValidatedConfigDBConnector(ctx.obj['db'])
66956748
ntp_servers = db.get_table("NTP_SERVER")
66966749
if ntp_ip_address in ntp_servers:
66976750
try:
@@ -7019,19 +7072,19 @@ def add(ctx, name, ipaddr, port, vrf):
70197072
if not is_valid_collector_info(name, ipaddr, port, vrf):
70207073
return
70217074

7022-
config_db = ValidatedConfigDBConnector(ctx.obj['db'])
7075+
config_db = ValidatedConfigDBConnector(ctx.obj['db'])
70237076
collector_tbl = config_db.get_table('SFLOW_COLLECTOR')
70247077

70257078
if (collector_tbl and name not in collector_tbl and len(collector_tbl) == 2):
70267079
click.echo("Only 2 collectors can be configured, please delete one")
70277080
return
7028-
7081+
70297082
try:
70307083
config_db.mod_entry('SFLOW_COLLECTOR', name,
70317084
{"collector_ip": ipaddr, "collector_port": port,
70327085
"collector_vrf": vrf})
70337086
except ValueError as e:
7034-
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
7087+
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
70357088
return
70367089

70377090
#
@@ -7364,7 +7417,7 @@ def add_subinterface(ctx, subinterface_name, vid):
73647417
if vid is not None:
73657418
subintf_dict.update({"vlan" : vid})
73667419
subintf_dict.update({"admin_status" : "up"})
7367-
7420+
73687421
try:
73697422
config_db.set_entry('VLAN_SUB_INTERFACE', subinterface_name, subintf_dict)
73707423
except ValueError as e:

0 commit comments

Comments
 (0)