Skip to content

Commit 259a37d

Browse files
authored
[config]Support single file reload for multiasic (#3349) (#3481)
ADO: 27595279 What I did Extend config reload to support single file reloading for multi-asic How I did it Add the single file reload support for mutli-asic How to verify it Unit test and manual test on multi-asic DUT
1 parent 7783abd commit 259a37d

2 files changed

Lines changed: 405 additions & 109 deletions

File tree

config/main.py

Lines changed: 185 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from sonic_yang_cfg_generator import SonicYangCfgDbGenerator
3232
from utilities_common import util_base
3333
from swsscommon import swsscommon
34-
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector
34+
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector, ConfigDBPipeConnector
3535
from utilities_common.db import Db
3636
from utilities_common.intf_filter import parse_interface_in_filter
3737
from utilities_common import bgp_util
@@ -1196,7 +1196,7 @@ def validate_gre_type(ctx, _, value):
11961196
raise click.UsageError("{} is not a valid GRE type".format(value))
11971197

11981198

1199-
def multi_asic_save_config(db, filename):
1199+
def multiasic_save_to_singlefile(db, filename):
12001200
"""A function to save all asic's config to single file
12011201
"""
12021202
all_current_config = {}
@@ -1263,6 +1263,96 @@ def validate_patch(patch):
12631263
except Exception as e:
12641264
raise GenericConfigUpdaterError(f"Validate json patch: {patch} failed due to:{e}")
12651265

1266+
1267+
def multiasic_validate_single_file(filename):
1268+
ns_list = [DEFAULT_NAMESPACE, *multi_asic.get_namespace_list()]
1269+
file_input = read_json_file(filename)
1270+
file_ns_list = [DEFAULT_NAMESPACE if key == HOST_NAMESPACE else key for key in file_input]
1271+
if set(ns_list) != set(file_ns_list):
1272+
click.echo(
1273+
"Input file {} must contain all asics config. ns_list: {} file ns_list: {}".format(
1274+
filename, ns_list, file_ns_list)
1275+
)
1276+
raise click.Abort()
1277+
1278+
1279+
def load_sysinfo_if_missing(asic_config):
1280+
device_metadata = asic_config.get('DEVICE_METADATA', {})
1281+
platform = device_metadata.get("localhost", {}).get("platform")
1282+
mac = device_metadata.get("localhost", {}).get("mac")
1283+
if not platform:
1284+
log.log_warning("platform is missing from Input file")
1285+
return True
1286+
elif not mac:
1287+
log.log_warning("mac is missing from Input file")
1288+
return True
1289+
else:
1290+
return False
1291+
1292+
1293+
def flush_configdb(namespace=DEFAULT_NAMESPACE):
1294+
if namespace is DEFAULT_NAMESPACE:
1295+
config_db = ConfigDBConnector()
1296+
else:
1297+
config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace)
1298+
1299+
config_db.connect()
1300+
client = config_db.get_redis_client(config_db.CONFIG_DB)
1301+
client.flushdb()
1302+
return client, config_db
1303+
1304+
1305+
def migrate_db_to_lastest(namespace=DEFAULT_NAMESPACE):
1306+
# Migrate DB contents to latest version
1307+
db_migrator = '/usr/local/bin/db_migrator.py'
1308+
if os.path.isfile(db_migrator) and os.access(db_migrator, os.X_OK):
1309+
if namespace is DEFAULT_NAMESPACE:
1310+
command = [db_migrator, '-o', 'migrate']
1311+
else:
1312+
command = [db_migrator, '-o', 'migrate', '-n', namespace]
1313+
clicommon.run_command(command, display_cmd=True)
1314+
1315+
1316+
def multiasic_write_to_db(filename, load_sysinfo):
1317+
file_input = read_json_file(filename)
1318+
for ns in [DEFAULT_NAMESPACE, *multi_asic.get_namespace_list()]:
1319+
asic_name = HOST_NAMESPACE if ns == DEFAULT_NAMESPACE else ns
1320+
asic_config = file_input[asic_name]
1321+
1322+
asic_load_sysinfo = True if load_sysinfo else False
1323+
if not asic_load_sysinfo:
1324+
asic_load_sysinfo = load_sysinfo_if_missing(asic_config)
1325+
1326+
if asic_load_sysinfo:
1327+
cfg_hwsku = asic_config.get("DEVICE_METADATA", {}).\
1328+
get("localhost", {}).get("hwsku")
1329+
if not cfg_hwsku:
1330+
click.secho("Could not get the HWSKU from config file, Exiting!", fg='magenta')
1331+
sys.exit(1)
1332+
1333+
client, _ = flush_configdb(ns)
1334+
1335+
if asic_load_sysinfo:
1336+
if ns is DEFAULT_NAMESPACE:
1337+
command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '--write-to-db']
1338+
else:
1339+
command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '-n', str(ns), '--write-to-db']
1340+
clicommon.run_command(command, display_cmd=True)
1341+
1342+
if ns is DEFAULT_NAMESPACE:
1343+
config_db = ConfigDBPipeConnector(use_unix_socket_path=True)
1344+
else:
1345+
config_db = ConfigDBPipeConnector(use_unix_socket_path=True, namespace=ns)
1346+
1347+
config_db.connect(False)
1348+
sonic_cfggen.FormatConverter.to_deserialized(asic_config)
1349+
data = sonic_cfggen.FormatConverter.output_to_db(asic_config)
1350+
config_db.mod_config(sonic_cfggen.FormatConverter.output_to_db(data))
1351+
client.set(config_db.INIT_INDICATOR, 1)
1352+
1353+
migrate_db_to_lastest(ns)
1354+
1355+
12661356
# This is our main entrypoint - the main 'config' command
12671357
@click.group(cls=clicommon.AbbreviationGroup, context_settings=CONTEXT_SETTINGS)
12681358
@click.pass_context
@@ -1350,7 +1440,7 @@ def save(db, filename):
13501440
# save all ASIC configurations to that single file.
13511441
if len(cfg_files) == 1 and multi_asic.is_multi_asic():
13521442
filename = cfg_files[0]
1353-
multi_asic_save_config(db, filename)
1443+
multiasic_save_to_singlefile(db, filename)
13541444
return
13551445
elif len(cfg_files) != num_cfg_file:
13561446
click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file))
@@ -1668,11 +1758,15 @@ def reload(db, filename, yes, load_sysinfo, no_service_restart, force, file_form
16681758
if multi_asic.is_multi_asic() and file_format == 'config_db':
16691759
num_cfg_file += num_asic
16701760

1761+
multiasic_single_file_mode = False
16711762
# If the user give the filename[s], extract the file names.
16721763
if filename is not None:
16731764
cfg_files = filename.split(',')
16741765

1675-
if len(cfg_files) != num_cfg_file:
1766+
if len(cfg_files) == 1 and multi_asic.is_multi_asic():
1767+
multiasic_validate_single_file(cfg_files[0])
1768+
multiasic_single_file_mode = True
1769+
elif len(cfg_files) != num_cfg_file:
16761770
click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file))
16771771
return
16781772

@@ -1681,127 +1775,109 @@ def reload(db, filename, yes, load_sysinfo, no_service_restart, force, file_form
16811775
log.log_notice("'reload' stopping services...")
16821776
_stop_services()
16831777

1684-
# In Single ASIC platforms we have single DB service. In multi-ASIC platforms we have a global DB
1685-
# service running in the host + DB services running in each ASIC namespace created per ASIC.
1686-
# In the below logic, we get all namespaces in this platform and add an empty namespace ''
1687-
# denoting the current namespace which we are in ( the linux host )
1688-
for inst in range(-1, num_cfg_file-1):
1689-
# Get the namespace name, for linux host it is None
1690-
if inst == -1:
1691-
namespace = None
1692-
else:
1693-
namespace = "{}{}".format(NAMESPACE_PREFIX, inst)
1694-
1695-
# Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json
1696-
if cfg_files:
1697-
file = cfg_files[inst+1]
1698-
# Save to tmpfile in case of stdin input which can only be read once
1699-
if file == "/dev/stdin":
1700-
file_input = read_json_file(file)
1701-
(_, tmpfname) = tempfile.mkstemp(dir="/tmp", suffix="_configReloadStdin")
1702-
write_json_file(file_input, tmpfname)
1703-
file = tmpfname
1704-
else:
1705-
if file_format == 'config_db':
1706-
if namespace is None:
1707-
file = DEFAULT_CONFIG_DB_FILE
1708-
else:
1709-
file = "/etc/sonic/config_db{}.json".format(inst)
1778+
if multiasic_single_file_mode:
1779+
multiasic_write_to_db(cfg_files[0], load_sysinfo)
1780+
else:
1781+
# In Single ASIC platforms we have single DB service. In multi-ASIC platforms we have a global DB
1782+
# service running in the host + DB services running in each ASIC namespace created per ASIC.
1783+
# In the below logic, we get all namespaces in this platform and add an empty namespace ''
1784+
# denoting the current namespace which we are in ( the linux host )
1785+
for inst in range(-1, num_cfg_file-1):
1786+
# Get the namespace name, for linux host it is DEFAULT_NAMESPACE
1787+
if inst == -1:
1788+
namespace = DEFAULT_NAMESPACE
17101789
else:
1711-
file = DEFAULT_CONFIG_YANG_FILE
1712-
1790+
namespace = "{}{}".format(NAMESPACE_PREFIX, inst)
1791+
1792+
# Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json
1793+
if cfg_files:
1794+
file = cfg_files[inst+1]
1795+
# Save to tmpfile in case of stdin input which can only be read once
1796+
if file == "/dev/stdin":
1797+
file_input = read_json_file(file)
1798+
(_, tmpfname) = tempfile.mkstemp(dir="/tmp", suffix="_configReloadStdin")
1799+
write_json_file(file_input, tmpfname)
1800+
file = tmpfname
1801+
else:
1802+
if file_format == 'config_db':
1803+
if namespace is DEFAULT_NAMESPACE:
1804+
file = DEFAULT_CONFIG_DB_FILE
1805+
else:
1806+
file = "/etc/sonic/config_db{}.json".format(inst)
1807+
else:
1808+
file = DEFAULT_CONFIG_YANG_FILE
17131809

1714-
# Check the file exists before proceeding.
1715-
if not os.path.exists(file):
1716-
click.echo("The config file {} doesn't exist".format(file))
1717-
continue
1810+
# Check the file exists before proceeding.
1811+
if not os.path.exists(file):
1812+
click.echo("The config file {} doesn't exist".format(file))
1813+
continue
17181814

1719-
if file_format == 'config_db':
1720-
file_input = read_json_file(file)
1815+
if file_format == 'config_db':
1816+
file_input = read_json_file(file)
1817+
if not load_sysinfo:
1818+
load_sysinfo = load_sysinfo_if_missing(file_input)
1819+
1820+
if load_sysinfo:
1821+
try:
1822+
command = [SONIC_CFGGEN_PATH, "-j", file, '-v', "DEVICE_METADATA.localhost.hwsku"]
1823+
proc = subprocess.Popen(command, text=True, stdout=subprocess.PIPE)
1824+
output, err = proc.communicate()
1825+
1826+
except FileNotFoundError as e:
1827+
click.echo("{}".format(str(e)), err=True)
1828+
raise click.Abort()
1829+
except Exception as e:
1830+
click.echo("{}\n{}".format(type(e), str(e)), err=True)
1831+
raise click.Abort()
1832+
1833+
if not output:
1834+
click.secho("Could not get the HWSKU from config file, Exiting!!!", fg='magenta')
1835+
sys.exit(1)
17211836

1722-
platform = file_input.get("DEVICE_METADATA", {}).\
1723-
get(HOST_NAMESPACE, {}).get("platform")
1724-
mac = file_input.get("DEVICE_METADATA", {}).\
1725-
get(HOST_NAMESPACE, {}).get("mac")
1837+
cfg_hwsku = output.strip()
17261838

1727-
if not platform or not mac:
1728-
log.log_warning("Input file does't have platform or mac. platform: {}, mac: {}"
1729-
.format(None if platform is None else platform, None if mac is None else mac))
1730-
load_sysinfo = True
1839+
client, config_db = flush_configdb(namespace)
17311840

1732-
if load_sysinfo:
1733-
try:
1734-
command = [SONIC_CFGGEN_PATH, "-j", file, '-v', "DEVICE_METADATA.localhost.hwsku"]
1735-
proc = subprocess.Popen(command, text=True, stdout=subprocess.PIPE)
1736-
output, err = proc.communicate()
1841+
if load_sysinfo:
1842+
if namespace is DEFAULT_NAMESPACE:
1843+
command = [
1844+
str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '--write-to-db']
1845+
else:
1846+
command = [
1847+
str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '-n', str(namespace), '--write-to-db']
1848+
clicommon.run_command(command, display_cmd=True)
17371849

1738-
except FileNotFoundError as e:
1739-
click.echo("{}".format(str(e)), err=True)
1740-
raise click.Abort()
1741-
except Exception as e:
1742-
click.echo("{}\n{}".format(type(e), str(e)), err=True)
1743-
raise click.Abort()
1850+
# For the database service running in linux host we use the file user gives as input
1851+
# or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace,
1852+
# the default config_db<namespaceID>.json format is used.
17441853

1745-
if not output:
1746-
click.secho("Could not get the HWSKU from config file, Exiting!!!", fg='magenta')
1747-
sys.exit(1)
1854+
config_gen_opts = []
17481855

1749-
cfg_hwsku = output.strip()
1856+
if os.path.isfile(INIT_CFG_FILE):
1857+
config_gen_opts += ['-j', str(INIT_CFG_FILE)]
17501858

1751-
if namespace is None:
1752-
config_db = ConfigDBConnector()
1753-
else:
1754-
config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace)
1755-
1756-
config_db.connect()
1757-
client = config_db.get_redis_client(config_db.CONFIG_DB)
1758-
client.flushdb()
1759-
1760-
if load_sysinfo:
1761-
if namespace is None:
1762-
command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '--write-to-db']
1859+
if file_format == 'config_db':
1860+
config_gen_opts += ['-j', str(file)]
17631861
else:
1764-
command = [str(SONIC_CFGGEN_PATH), '-H', '-k', str(cfg_hwsku), '-n', str(namespace), '--write-to-db']
1765-
clicommon.run_command(command, display_cmd=True)
1766-
1767-
# For the database service running in linux host we use the file user gives as input
1768-
# or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace,
1769-
# the default config_db<namespaceID>.json format is used.
1862+
config_gen_opts += ['-Y', str(file)]
17701863

1864+
if namespace is not DEFAULT_NAMESPACE:
1865+
config_gen_opts += ['-n', str(namespace)]
17711866

1772-
config_gen_opts = []
1867+
command = [SONIC_CFGGEN_PATH] + config_gen_opts + ['--write-to-db']
17731868

1774-
if os.path.isfile(INIT_CFG_FILE):
1775-
config_gen_opts += ['-j', str(INIT_CFG_FILE)]
1776-
1777-
if file_format == 'config_db':
1778-
config_gen_opts += ['-j', str(file)]
1779-
else:
1780-
config_gen_opts += ['-Y', str(file)]
1781-
1782-
if namespace is not None:
1783-
config_gen_opts += ['-n', str(namespace)]
1784-
1785-
command = [SONIC_CFGGEN_PATH] + config_gen_opts + ['--write-to-db']
1786-
1787-
clicommon.run_command(command, display_cmd=True)
1788-
client.set(config_db.INIT_INDICATOR, 1)
1869+
clicommon.run_command(command, display_cmd=True)
1870+
client.set(config_db.INIT_INDICATOR, 1)
17891871

1790-
if os.path.exists(file) and file.endswith("_configReloadStdin"):
1791-
# Remove tmpfile
1792-
try:
1793-
os.remove(file)
1794-
except OSError as e:
1795-
click.echo("An error occurred while removing the temporary file: {}".format(str(e)), err=True)
1872+
if os.path.exists(file) and file.endswith("_configReloadStdin"):
1873+
# Remove tmpfile
1874+
try:
1875+
os.remove(file)
1876+
except OSError as e:
1877+
click.echo("An error occurred while removing the temporary file: {}".format(str(e)), err=True)
17961878

1797-
# Migrate DB contents to latest version
1798-
db_migrator='/usr/local/bin/db_migrator.py'
1799-
if os.path.isfile(db_migrator) and os.access(db_migrator, os.X_OK):
1800-
if namespace is None:
1801-
command = [db_migrator, '-o', 'migrate']
1802-
else:
1803-
command = [db_migrator, '-o', 'migrate', '-n', str(namespace)]
1804-
clicommon.run_command(command, display_cmd=True)
1879+
# Migrate DB contents to latest version
1880+
migrate_db_to_lastest(namespace)
18051881

18061882
# Re-generate the environment variable in case config_db.json was edited
18071883
update_sonic_environment()

0 commit comments

Comments
 (0)