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: 86 additions & 31 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1357,12 +1357,67 @@ 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)
if multi_asic.is_multi_asic():
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
# Initialize a dictionary to hold changes categorized by ASIC
changes_by_asic = {}

# Function to extract ASIC identifier from the change path
def extract_asic_id(path):
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
start = path.find("/") + 1
end = path.find("/", start)
return path[start:end], path[end:] # Also return the modified path without ASIC ID

# Function to apply patch for a single ASIC.
def apply_patch_for_asic(asic_changes):
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
asic_id, changes = asic_changes

# Replace localhost to empty string which is db definition of Host
if asic_id.lower() == "localhost":
asic_id = ""
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved

try:
# Call apply_patch with the ASIC-specific changes and predefined parameters
GenericUpdater(namespace=asic_id).apply_patch(jsonpatch.JsonPatch(changes), config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)
results[asic_id] = {"success": True, "message": "Success"}

log.log_notice(f"'apply-patch' executed successfully for {asic_id} by {changes}")
except Exception as e:
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
results[asic_id] = {"success": False, "message": str(e)}
log.log_notice(f"'apply-patch' executed failed for {asic_id} by {changes} due to {str(e)}")
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved

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

# Modify the 'path' in the change to remove the ASIC ID
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
change["path"] = modified_path

# Check if the ASIC ID is already in our dictionary, if not, initialize it
if asic_id not in changes_by_asic:
changes_by_asic[asic_id] = []

# Add the modified change to the appropriate list based on ASIC ID
changes_by_asic[asic_id].append(change)

for asic_changes in changes_by_asic.items():
apply_patch_for_asic(asic_changes)

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

if failures:
failure_messages = '\n'.join([f"- {asic_id}: {results[asic_id]['message']}" for asic_id in failures])
raise Exception(f"Failed to apply patch on the following ASICs:\n{failure_messages}")
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved

else:
GenericUpdater(multi_asic.DEFAULT_NAMESPACE).apply_patch(patch, config_format, verbose, dry_run, ignore_non_yang_tables, ignore_path)
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved

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 +2133,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 +2207,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 +2219,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 +2256,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 +2274,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 +2291,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 +2329,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 +2402,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 +2589,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 +3559,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 +4617,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 +4685,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 +5112,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 +5457,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 +6635,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 +6659,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 +6717,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 +6730,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 +6746,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 +7074,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 +7419,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
25 changes: 18 additions & 7 deletions generic_config_updater/change_applier.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import tempfile
from collections import defaultdict
from swsscommon.swsscommon import ConfigDBConnector
from sonic_py_common import multi_asic
from .gu_common import genericUpdaterLogging

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
Expand All @@ -32,8 +33,8 @@ def log_error(m):
logger.log(logger.LOG_PRIORITY_ERROR, m, print_to_console)


def get_config_db():
config_db = ConfigDBConnector()
def get_config_db(namespace=multi_asic.DEFAULT_NAMESPACE):
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace)
config_db.connect()
return config_db

Expand Down Expand Up @@ -73,8 +74,9 @@ class ChangeApplier:

updater_conf = None

def __init__(self):
self.config_db = get_config_db()
def __init__(self, namespace=multi_asic.DEFAULT_NAMESPACE):
self.namespace = namespace
self.config_db = get_config_db(self.namespace)
self.backend_tables = [
"BUFFER_PG",
"BUFFER_PROFILE",
Expand Down Expand Up @@ -160,15 +162,24 @@ def apply(self, change):
log_error("Failed to apply Json change")
return ret


def remove_backend_tables_from_config(self, data):
for key in self.backend_tables:
data.pop(key, None)


def _get_running_config(self):
(_, fname) = tempfile.mkstemp(suffix="_changeApplier")
os.system("sonic-cfggen -d --print-data > {}".format(fname))

command = "sonic-cfggen -d --print-data"

if self.namespace is not None and self.namespace != multi_asic.DEFAULT_NAMESPACE:
command += " -n {}".format(self.namespace)
command += " > {}".format(fname)

ret_code = os.system(command)
xincunli-sonic marked this conversation as resolved.
Show resolved Hide resolved
if ret_code != 0:
# Handle the error appropriately, e.g., raise an exception or log an error
raise RuntimeError("sonic-cfggen command failed with return code {}".format(ret_code))

run_data = {}
with open(fname, "r") as s:
run_data = json.load(s)
Expand Down
Loading
Loading