Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Multi ASIC support for apply-patch #3249

Merged
merged 14 commits into from
Apr 23, 2024
117 changes: 85 additions & 32 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from jsonpatch import JsonPatchConflict
from jsonpointer import JsonPointerException
from collections import OrderedDict
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat
from generic_config_updater.generic_updater import GenericUpdater, ConfigFormat, extract_scope
from minigraph import parse_device_desc_xml, minigraph_encoder
from natsort import natsorted
from portconfig import get_child_ports
Expand Down Expand Up @@ -1152,6 +1152,24 @@ def validate_gre_type(ctx, _, value):
return gre_type_value
except ValueError:
raise click.UsageError("{} is not a valid GRE type".format(value))

# Function to apply patch for a single ASIC.
def apply_patch_for_scope(scope_changes, results, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path):
scope, changes = scope_changes
# Replace localhost to DEFAULT_NAMESPACE which is db definition of Host
if scope.lower() == "localhost" or scope == "":
scope = multi_asic.DEFAULT_NAMESPACE

scope_for_log = scope if scope else "localhost"
try:
# Call apply_patch with the ASIC-specific changes and predefined parameters
GenericUpdater(namespace=scope).apply_patch(jsonpatch.JsonPatch(changes), config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)
results[scope_for_log] = {"success": True, "message": "Success"}
log.log_notice(f"'apply-patch' executed successfully for {scope_for_log} by {changes}")
except Exception as e:
results[scope_for_log] = {"success": False, "message": str(e)}
log.log_error(f"'apply-patch' executed failed for {scope_for_log} by {changes} due to {str(e)}")


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

results = {}
config_format = ConfigFormat[format.upper()]
GenericUpdater().apply_patch(patch, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)
# Initialize a dictionary to hold changes categorized by scope
changes_by_scope = {}

# Iterate over each change in the JSON Patch
for change in patch:
scope, modified_path = extract_scope(change["path"])

# Modify the 'path' in the change to remove the scope
change["path"] = modified_path

# Check if the scope is already in our dictionary, if not, initialize it
if scope not in changes_by_scope:
changes_by_scope[scope] = []

# Add the modified change to the appropriate list based on scope
changes_by_scope[scope].append(change)

# Empty case to force validate YANG model.
if not changes_by_scope:
asic_list = [multi_asic.DEFAULT_NAMESPACE]
asic_list.extend(multi_asic.get_namespace_list())
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
for asic in asic_list:
changes_by_scope[asic] = []

# Apply changes for each scope
for scope_changes in changes_by_scope.items():
apply_patch_for_scope(scope_changes, results, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)

# Check if any updates failed
failures = [scope for scope, result in results.items() if not result['success']]

if failures:
failure_messages = '\n'.join([f"- {failed_scope}: {results[failed_scope]['message']}" for failed_scope in failures])
raise Exception(f"Failed to apply patch on the following scopes:\n{failure_messages}")

log.log_notice(f"Patch applied successfully for {patch}.")
click.secho("Patch applied successfully.", fg="cyan", underline=True)
except Exception as ex:
click.secho("Failed to apply patch", fg="red", underline=True, err=True)
click.secho("Failed to apply patch due to: {}".format(ex), fg="red", underline=True, err=True)
ctx.fail(ex)

@config.command()
Expand Down Expand Up @@ -2078,15 +2131,15 @@ def synchronous_mode(sync_mode):
if ADHOC_VALIDATION:
if sync_mode != 'enable' and sync_mode != 'disable':
raise click.BadParameter("Error: Invalid argument %s, expect either enable or disable" % sync_mode)

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()
try:
config_db.mod_entry('DEVICE_METADATA' , 'localhost', {"synchronous_mode" : sync_mode})
except ValueError as e:
ctx = click.get_current_context()
ctx.fail("Error: Invalid argument %s, expect either enable or disable" % sync_mode)

click.echo("""Wrote %s synchronous mode into CONFIG_DB, swss restart required to apply the configuration: \n
Option 1. config save -y \n
config reload -y \n
Expand Down Expand Up @@ -2152,7 +2205,7 @@ def portchannel(db, ctx, namespace):
@click.pass_context
def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate):
"""Add port channel"""

fvs = {
'admin_status': 'up',
'mtu': '9100',
Expand All @@ -2164,26 +2217,26 @@ def add_portchannel(ctx, portchannel_name, min_links, fallback, fast_rate):
fvs['min_links'] = str(min_links)
if fallback != 'false':
fvs['fallback'] = 'true'

db = ValidatedConfigDBConnector(ctx.obj['db'])
if ADHOC_VALIDATION:
if is_portchannel_name_valid(portchannel_name) != True:
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'"
.format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))
if is_portchannel_present_in_db(db, portchannel_name):
ctx.fail("{} already exists!".format(portchannel_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL

try:
db.set_entry('PORTCHANNEL', portchannel_name, fvs)
except ValueError:
ctx.fail("{} is invalid!, name should have prefix '{}' and suffix '{}'".format(portchannel_name, CFG_PORTCHANNEL_PREFIX, CFG_PORTCHANNEL_NO))

@portchannel.command('del')
@click.argument('portchannel_name', metavar='<portchannel_name>', required=True)
@click.pass_context
def remove_portchannel(ctx, portchannel_name):
"""Remove port channel"""

db = ValidatedConfigDBConnector(ctx.obj['db'])
if ADHOC_VALIDATION:
if is_portchannel_name_valid(portchannel_name) != True:
Expand All @@ -2201,7 +2254,7 @@ def remove_portchannel(ctx, portchannel_name):

if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0: # TODO: MISSING CONSTRAINT IN YANG MODEL
ctx.fail("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name))

try:
db.set_entry('PORTCHANNEL', portchannel_name, None)
except JsonPatchConflict:
Expand All @@ -2219,7 +2272,7 @@ def portchannel_member(ctx):
def add_portchannel_member(ctx, portchannel_name, port_name):
"""Add member to port channel"""
db = ValidatedConfigDBConnector(ctx.obj['db'])

if ADHOC_VALIDATION:
if clicommon.is_port_mirror_dst_port(db, port_name):
ctx.fail("{} is configured as mirror destination port".format(port_name)) # TODO: MISSING CONSTRAINT IN YANG MODEL
Expand All @@ -2236,7 +2289,7 @@ def add_portchannel_member(ctx, portchannel_name, port_name):
# Dont proceed if the port channel does not exist
if is_portchannel_present_in_db(db, portchannel_name) is False:
ctx.fail("{} is not present.".format(portchannel_name))

# Don't allow a port to be member of port channel if it is configured with an IP address
for key,value in db.get_table('INTERFACE').items():
if type(key) == tuple:
Expand Down Expand Up @@ -2274,7 +2327,7 @@ def add_portchannel_member(ctx, portchannel_name, port_name):
member_port_speed = member_port_entry.get(PORT_SPEED)

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

Expand Down Expand Up @@ -2347,7 +2400,7 @@ def del_portchannel_member(ctx, portchannel_name, port_name):
# Dont proceed if the the port is not an existing member of the port channel
if not is_port_member_of_this_portchannel(db, port_name, portchannel_name):
ctx.fail("{} is not a member of portchannel {}".format(port_name, portchannel_name))

try:
db.set_entry('PORTCHANNEL_MEMBER', portchannel_name + '|' + port_name, None)
except JsonPatchConflict:
Expand Down Expand Up @@ -2534,7 +2587,7 @@ def add_erspan(session_name, src_ip, dst_ip, dscp, ttl, gre_type, queue, policer
if not namespaces['front_ns']:
config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()
if ADHOC_VALIDATION:
if ADHOC_VALIDATION:
if validate_mirror_session_config(config_db, session_name, None, src_port, direction) is False:
return
try:
Expand Down Expand Up @@ -3504,7 +3557,7 @@ def del_community(db, community):
if community not in snmp_communities:
click.echo("SNMP community {} is not configured".format(community))
sys.exit(1)

config_db = ValidatedConfigDBConnector(db.cfgdb)
try:
config_db.set_entry('SNMP_COMMUNITY', community, None)
Expand Down Expand Up @@ -4562,7 +4615,7 @@ def fec(ctx, interface_name, interface_fec, verbose):
def ip(ctx):
"""Set IP interface attributes"""
pass

def validate_vlan_exists(db,text):
data = db.get_table('VLAN')
keys = list(data.keys())
Expand Down Expand Up @@ -4630,12 +4683,12 @@ def add(ctx, interface_name, ip_addr, gw):
table_name = get_interface_table_name(interface_name)
if table_name == "":
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")

if table_name == "VLAN_INTERFACE":
if not validate_vlan_exists(config_db, interface_name):
ctx.fail(f"Error: {interface_name} does not exist. Vlan must be created before adding an IP address")
return

interface_entry = config_db.get_entry(table_name, interface_name)
if len(interface_entry) == 0:
if table_name == "VLAN_SUB_INTERFACE":
Expand Down Expand Up @@ -5057,7 +5110,7 @@ def cable_length(ctx, interface_name, length):

if not is_dynamic_buffer_enabled(config_db):
ctx.fail("This command can only be supported on a system with dynamic buffer enabled")

if ADHOC_VALIDATION:
# Check whether port is legal
ports = config_db.get_entry("PORT", interface_name)
Expand Down Expand Up @@ -5402,7 +5455,7 @@ def unbind(ctx, interface_name):
config_db.set_entry(table_name, interface_name, subintf_entry)
else:
config_db.set_entry(table_name, interface_name, None)

click.echo("Interface {} IP disabled and address(es) removed due to unbinding VRF.".format(interface_name))
#
# 'ipv6' subgroup ('config interface ipv6 ...')
Expand Down Expand Up @@ -6580,7 +6633,7 @@ def add_loopback(ctx, loopback_name):
lo_intfs = [k for k, v in config_db.get_table('LOOPBACK_INTERFACE').items() if type(k) != tuple]
if loopback_name in lo_intfs:
ctx.fail("{} already exists".format(loopback_name)) # TODO: MISSING CONSTRAINT IN YANG VALIDATION

try:
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, {"NULL" : "NULL"})
except ValueError:
Expand All @@ -6604,7 +6657,7 @@ def del_loopback(ctx, loopback_name):
ips = [ k[1] for k in lo_config_db if type(k) == tuple and k[0] == loopback_name ]
for ip in ips:
config_db.set_entry('LOOPBACK_INTERFACE', (loopback_name, ip), None)

try:
config_db.set_entry('LOOPBACK_INTERFACE', loopback_name, None)
except JsonPatchConflict:
Expand Down Expand Up @@ -6662,9 +6715,9 @@ def ntp(ctx):
def add_ntp_server(ctx, ntp_ip_address):
""" Add NTP server IP """
if ADHOC_VALIDATION:
if not clicommon.is_ipaddress(ntp_ip_address):
if not clicommon.is_ipaddress(ntp_ip_address):
ctx.fail('Invalid IP address')
db = ValidatedConfigDBConnector(ctx.obj['db'])
db = ValidatedConfigDBConnector(ctx.obj['db'])
ntp_servers = db.get_table("NTP_SERVER")
if ntp_ip_address in ntp_servers:
click.echo("NTP server {} is already configured".format(ntp_ip_address))
Expand All @@ -6675,7 +6728,7 @@ def add_ntp_server(ctx, ntp_ip_address):
{'resolve_as': ntp_ip_address,
'association_type': 'server'})
except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
click.echo("NTP server {} added to configuration".format(ntp_ip_address))
try:
click.echo("Restarting ntp-config service...")
Expand All @@ -6691,7 +6744,7 @@ def del_ntp_server(ctx, ntp_ip_address):
if ADHOC_VALIDATION:
if not clicommon.is_ipaddress(ntp_ip_address):
ctx.fail('Invalid IP address')
db = ValidatedConfigDBConnector(ctx.obj['db'])
db = ValidatedConfigDBConnector(ctx.obj['db'])
ntp_servers = db.get_table("NTP_SERVER")
if ntp_ip_address in ntp_servers:
try:
Expand Down Expand Up @@ -7019,19 +7072,19 @@ def add(ctx, name, ipaddr, port, vrf):
if not is_valid_collector_info(name, ipaddr, port, vrf):
return

config_db = ValidatedConfigDBConnector(ctx.obj['db'])
config_db = ValidatedConfigDBConnector(ctx.obj['db'])
collector_tbl = config_db.get_table('SFLOW_COLLECTOR')

if (collector_tbl and name not in collector_tbl and len(collector_tbl) == 2):
click.echo("Only 2 collectors can be configured, please delete one")
return

try:
config_db.mod_entry('SFLOW_COLLECTOR', name,
{"collector_ip": ipaddr, "collector_port": port,
"collector_vrf": vrf})
except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
return

#
Expand Down Expand Up @@ -7364,7 +7417,7 @@ def add_subinterface(ctx, subinterface_name, vid):
if vid is not None:
subintf_dict.update({"vlan" : vid})
subintf_dict.update({"admin_status" : "up"})

try:
config_db.set_entry('VLAN_SUB_INTERFACE', subinterface_name, subintf_dict)
except ValueError as e:
Expand Down
Loading
Loading