19
19
from jsonpatch import JsonPatchConflict
20
20
from jsonpointer import JsonPointerException
21
21
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
23
23
from minigraph import parse_device_desc_xml , minigraph_encoder
24
24
from natsort import natsorted
25
25
from portconfig import get_child_ports
@@ -1152,6 +1152,24 @@ def validate_gre_type(ctx, _, value):
1152
1152
return gre_type_value
1153
1153
except ValueError :
1154
1154
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
+
1155
1173
1156
1174
# This is our main entrypoint - the main 'config' command
1157
1175
@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
1357
1375
patch_as_json = json .loads (text )
1358
1376
patch = jsonpatch .JsonPatch (patch_as_json )
1359
1377
1378
+ results = {}
1360
1379
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 ] = []
1362
1393
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 } ." )
1363
1416
click .secho ("Patch applied successfully." , fg = "cyan" , underline = True )
1364
1417
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 )
1366
1419
ctx .fail (ex )
1367
1420
1368
1421
@config .command ()
@@ -2078,15 +2131,15 @@ def synchronous_mode(sync_mode):
2078
2131
if ADHOC_VALIDATION :
2079
2132
if sync_mode != 'enable' and sync_mode != 'disable' :
2080
2133
raise click .BadParameter ("Error: Invalid argument %s, expect either enable or disable" % sync_mode )
2081
-
2134
+
2082
2135
config_db = ValidatedConfigDBConnector (ConfigDBConnector ())
2083
2136
config_db .connect ()
2084
2137
try :
2085
2138
config_db .mod_entry ('DEVICE_METADATA' , 'localhost' , {"synchronous_mode" : sync_mode })
2086
2139
except ValueError as e :
2087
2140
ctx = click .get_current_context ()
2088
2141
ctx .fail ("Error: Invalid argument %s, expect either enable or disable" % sync_mode )
2089
-
2142
+
2090
2143
click .echo ("""Wrote %s synchronous mode into CONFIG_DB, swss restart required to apply the configuration: \n
2091
2144
Option 1. config save -y \n
2092
2145
config reload -y \n
@@ -2152,7 +2205,7 @@ def portchannel(db, ctx, namespace):
2152
2205
@click .pass_context
2153
2206
def add_portchannel (ctx , portchannel_name , min_links , fallback , fast_rate ):
2154
2207
"""Add port channel"""
2155
-
2208
+
2156
2209
fvs = {
2157
2210
'admin_status' : 'up' ,
2158
2211
'mtu' : '9100' ,
@@ -2164,26 +2217,26 @@ def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate):
2164
2217
fvs ['min_links' ] = str (min_links )
2165
2218
if fallback != 'false' :
2166
2219
fvs ['fallback' ] = 'true'
2167
-
2220
+
2168
2221
db = ValidatedConfigDBConnector (ctx .obj ['db' ])
2169
2222
if ADHOC_VALIDATION :
2170
2223
if is_portchannel_name_valid (portchannel_name ) != True :
2171
2224
ctx .fail ("{} is invalid!, name should have prefix '{}' and suffix '{}'"
2172
2225
.format (portchannel_name , CFG_PORTCHANNEL_PREFIX , CFG_PORTCHANNEL_NO ))
2173
2226
if is_portchannel_present_in_db (db , portchannel_name ):
2174
2227
ctx .fail ("{} already exists!" .format (portchannel_name )) # TODO: MISSING CONSTRAINT IN YANG MODEL
2175
-
2228
+
2176
2229
try :
2177
2230
db .set_entry ('PORTCHANNEL' , portchannel_name , fvs )
2178
2231
except ValueError :
2179
2232
ctx .fail ("{} is invalid!, name should have prefix '{}' and suffix '{}'" .format (portchannel_name , CFG_PORTCHANNEL_PREFIX , CFG_PORTCHANNEL_NO ))
2180
-
2233
+
2181
2234
@portchannel .command ('del' )
2182
2235
@click .argument ('portchannel_name' , metavar = '<portchannel_name>' , required = True )
2183
2236
@click .pass_context
2184
2237
def remove_portchannel (ctx , portchannel_name ):
2185
2238
"""Remove port channel"""
2186
-
2239
+
2187
2240
db = ValidatedConfigDBConnector (ctx .obj ['db' ])
2188
2241
if ADHOC_VALIDATION :
2189
2242
if is_portchannel_name_valid (portchannel_name ) != True :
@@ -2201,7 +2254,7 @@ def remove_portchannel(ctx, portchannel_name):
2201
2254
2202
2255
if len ([(k , v ) for k , v in db .get_table ('PORTCHANNEL_MEMBER' ) if k == portchannel_name ]) != 0 : # TODO: MISSING CONSTRAINT IN YANG MODEL
2203
2256
ctx .fail ("Error: Portchannel {} contains members. Remove members before deleting Portchannel!" .format (portchannel_name ))
2204
-
2257
+
2205
2258
try :
2206
2259
db .set_entry ('PORTCHANNEL' , portchannel_name , None )
2207
2260
except JsonPatchConflict :
@@ -2219,7 +2272,7 @@ def portchannel_member(ctx):
2219
2272
def add_portchannel_member (ctx , portchannel_name , port_name ):
2220
2273
"""Add member to port channel"""
2221
2274
db = ValidatedConfigDBConnector (ctx .obj ['db' ])
2222
-
2275
+
2223
2276
if ADHOC_VALIDATION :
2224
2277
if clicommon .is_port_mirror_dst_port (db , port_name ):
2225
2278
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):
2236
2289
# Dont proceed if the port channel does not exist
2237
2290
if is_portchannel_present_in_db (db , portchannel_name ) is False :
2238
2291
ctx .fail ("{} is not present." .format (portchannel_name ))
2239
-
2292
+
2240
2293
# Don't allow a port to be member of port channel if it is configured with an IP address
2241
2294
for key ,value in db .get_table ('INTERFACE' ).items ():
2242
2295
if type (key ) == tuple :
@@ -2274,7 +2327,7 @@ def add_portchannel_member(ctx, portchannel_name, port_name):
2274
2327
member_port_speed = member_port_entry .get (PORT_SPEED )
2275
2328
2276
2329
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 :
2278
2331
ctx .fail ("Port speed of {} is different than the other members of the portchannel {}"
2279
2332
.format (port_name , portchannel_name ))
2280
2333
@@ -2347,7 +2400,7 @@ def del_portchannel_member(ctx, portchannel_name, port_name):
2347
2400
# Dont proceed if the the port is not an existing member of the port channel
2348
2401
if not is_port_member_of_this_portchannel (db , port_name , portchannel_name ):
2349
2402
ctx .fail ("{} is not a member of portchannel {}" .format (port_name , portchannel_name ))
2350
-
2403
+
2351
2404
try :
2352
2405
db .set_entry ('PORTCHANNEL_MEMBER' , portchannel_name + '|' + port_name , None )
2353
2406
except JsonPatchConflict :
@@ -2534,7 +2587,7 @@ def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer
2534
2587
if not namespaces ['front_ns' ]:
2535
2588
config_db = ValidatedConfigDBConnector (ConfigDBConnector ())
2536
2589
config_db .connect ()
2537
- if ADHOC_VALIDATION :
2590
+ if ADHOC_VALIDATION :
2538
2591
if validate_mirror_session_config (config_db , session_name , None , src_port , direction ) is False :
2539
2592
return
2540
2593
try :
@@ -3504,7 +3557,7 @@ def del_community(db, community):
3504
3557
if community not in snmp_communities :
3505
3558
click .echo ("SNMP community {} is not configured" .format (community ))
3506
3559
sys .exit (1 )
3507
-
3560
+
3508
3561
config_db = ValidatedConfigDBConnector (db .cfgdb )
3509
3562
try :
3510
3563
config_db .set_entry ('SNMP_COMMUNITY' , community , None )
@@ -4562,7 +4615,7 @@ def fec(ctx, interface_name, interface_fec, verbose):
4562
4615
def ip (ctx ):
4563
4616
"""Set IP interface attributes"""
4564
4617
pass
4565
-
4618
+
4566
4619
def validate_vlan_exists (db ,text ):
4567
4620
data = db .get_table ('VLAN' )
4568
4621
keys = list (data .keys ())
@@ -4630,12 +4683,12 @@ def add(ctx, interface_name, ip_addr, gw):
4630
4683
table_name = get_interface_table_name (interface_name )
4631
4684
if table_name == "" :
4632
4685
ctx .fail ("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]" )
4633
-
4686
+
4634
4687
if table_name == "VLAN_INTERFACE" :
4635
4688
if not validate_vlan_exists (config_db , interface_name ):
4636
4689
ctx .fail (f"Error: { interface_name } does not exist. Vlan must be created before adding an IP address" )
4637
4690
return
4638
-
4691
+
4639
4692
interface_entry = config_db .get_entry (table_name , interface_name )
4640
4693
if len (interface_entry ) == 0 :
4641
4694
if table_name == "VLAN_SUB_INTERFACE" :
@@ -5057,7 +5110,7 @@ def cable_length(ctx, interface_name, length):
5057
5110
5058
5111
if not is_dynamic_buffer_enabled (config_db ):
5059
5112
ctx .fail ("This command can only be supported on a system with dynamic buffer enabled" )
5060
-
5113
+
5061
5114
if ADHOC_VALIDATION :
5062
5115
# Check whether port is legal
5063
5116
ports = config_db .get_entry ("PORT" , interface_name )
@@ -5402,7 +5455,7 @@ def unbind(ctx, interface_name):
5402
5455
config_db .set_entry (table_name , interface_name , subintf_entry )
5403
5456
else :
5404
5457
config_db .set_entry (table_name , interface_name , None )
5405
-
5458
+
5406
5459
click .echo ("Interface {} IP disabled and address(es) removed due to unbinding VRF." .format (interface_name ))
5407
5460
#
5408
5461
# 'ipv6' subgroup ('config interface ipv6 ...')
@@ -6580,7 +6633,7 @@ def add_loopback(ctx, loopback_name):
6580
6633
lo_intfs = [k for k , v in config_db .get_table ('LOOPBACK_INTERFACE' ).items () if type (k ) != tuple ]
6581
6634
if loopback_name in lo_intfs :
6582
6635
ctx .fail ("{} already exists" .format (loopback_name )) # TODO: MISSING CONSTRAINT IN YANG VALIDATION
6583
-
6636
+
6584
6637
try :
6585
6638
config_db .set_entry ('LOOPBACK_INTERFACE' , loopback_name , {"NULL" : "NULL" })
6586
6639
except ValueError :
@@ -6604,7 +6657,7 @@ def del_loopback(ctx, loopback_name):
6604
6657
ips = [ k [1 ] for k in lo_config_db if type (k ) == tuple and k [0 ] == loopback_name ]
6605
6658
for ip in ips :
6606
6659
config_db .set_entry ('LOOPBACK_INTERFACE' , (loopback_name , ip ), None )
6607
-
6660
+
6608
6661
try :
6609
6662
config_db .set_entry ('LOOPBACK_INTERFACE' , loopback_name , None )
6610
6663
except JsonPatchConflict :
@@ -6662,9 +6715,9 @@ def ntp(ctx):
6662
6715
def add_ntp_server (ctx , ntp_ip_address ):
6663
6716
""" Add NTP server IP """
6664
6717
if ADHOC_VALIDATION :
6665
- if not clicommon .is_ipaddress (ntp_ip_address ):
6718
+ if not clicommon .is_ipaddress (ntp_ip_address ):
6666
6719
ctx .fail ('Invalid IP address' )
6667
- db = ValidatedConfigDBConnector (ctx .obj ['db' ])
6720
+ db = ValidatedConfigDBConnector (ctx .obj ['db' ])
6668
6721
ntp_servers = db .get_table ("NTP_SERVER" )
6669
6722
if ntp_ip_address in ntp_servers :
6670
6723
click .echo ("NTP server {} is already configured" .format (ntp_ip_address ))
@@ -6675,7 +6728,7 @@ def add_ntp_server(ctx, ntp_ip_address):
6675
6728
{'resolve_as' : ntp_ip_address ,
6676
6729
'association_type' : 'server' })
6677
6730
except ValueError as e :
6678
- ctx .fail ("Invalid ConfigDB. Error: {}" .format (e ))
6731
+ ctx .fail ("Invalid ConfigDB. Error: {}" .format (e ))
6679
6732
click .echo ("NTP server {} added to configuration" .format (ntp_ip_address ))
6680
6733
try :
6681
6734
click .echo ("Restarting ntp-config service..." )
@@ -6691,7 +6744,7 @@ def del_ntp_server(ctx, ntp_ip_address):
6691
6744
if ADHOC_VALIDATION :
6692
6745
if not clicommon .is_ipaddress (ntp_ip_address ):
6693
6746
ctx .fail ('Invalid IP address' )
6694
- db = ValidatedConfigDBConnector (ctx .obj ['db' ])
6747
+ db = ValidatedConfigDBConnector (ctx .obj ['db' ])
6695
6748
ntp_servers = db .get_table ("NTP_SERVER" )
6696
6749
if ntp_ip_address in ntp_servers :
6697
6750
try :
@@ -7019,19 +7072,19 @@ def add(ctx, name, ipaddr, port, vrf):
7019
7072
if not is_valid_collector_info (name , ipaddr , port , vrf ):
7020
7073
return
7021
7074
7022
- config_db = ValidatedConfigDBConnector (ctx .obj ['db' ])
7075
+ config_db = ValidatedConfigDBConnector (ctx .obj ['db' ])
7023
7076
collector_tbl = config_db .get_table ('SFLOW_COLLECTOR' )
7024
7077
7025
7078
if (collector_tbl and name not in collector_tbl and len (collector_tbl ) == 2 ):
7026
7079
click .echo ("Only 2 collectors can be configured, please delete one" )
7027
7080
return
7028
-
7081
+
7029
7082
try :
7030
7083
config_db .mod_entry ('SFLOW_COLLECTOR' , name ,
7031
7084
{"collector_ip" : ipaddr , "collector_port" : port ,
7032
7085
"collector_vrf" : vrf })
7033
7086
except ValueError as e :
7034
- ctx .fail ("Invalid ConfigDB. Error: {}" .format (e ))
7087
+ ctx .fail ("Invalid ConfigDB. Error: {}" .format (e ))
7035
7088
return
7036
7089
7037
7090
#
@@ -7364,7 +7417,7 @@ def add_subinterface(ctx, subinterface_name, vid):
7364
7417
if vid is not None :
7365
7418
subintf_dict .update ({"vlan" : vid })
7366
7419
subintf_dict .update ({"admin_status" : "up" })
7367
-
7420
+
7368
7421
try :
7369
7422
config_db .set_entry ('VLAN_SUB_INTERFACE' , subinterface_name , subintf_dict )
7370
7423
except ValueError as e :
0 commit comments