From cddefcf0a2214fe863ce5d98f441cb2daa6afb43 Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Fri, 10 Apr 2020 08:07:02 -0700 Subject: [PATCH 1/8] [Phase 1] Multi ASIC config command changes, db_mgrator.py script updates for handing namespace. --- config/main.py | 176 ++++++++++++++++++++++++++++++++++------- scripts/db_migrator.py | 20 +++-- 2 files changed, 163 insertions(+), 33 deletions(-) diff --git a/config/main.py b/config/main.py index 1d196826d7..33f284a786 100755 --- a/config/main.py +++ b/config/main.py @@ -27,6 +27,9 @@ SYSLOG_IDENTIFIER = "config" VLAN_SUB_INTERFACE_SEPARATOR = '.' ASIC_CONF_FILENAME = 'asic.conf' +PLATFORM_ROOT_PATH = '/usr/share/sonic/device' +DEFAULT_CONFIG_DB_FILE = '/etc/sonic/config_db.json' +NS_PREFIX = 'asic' INIT_CFG_FILE = '/etc/sonic/init_cfg.json' @@ -114,6 +117,48 @@ def run_command(command, display_cmd=False, ignore_error=False): if proc.returncode != 0 and not ignore_error: sys.exit(proc.returncode) +# API to check if this is a multi-asic device or not. +def is_multi_asic(): + num_asics = _get_num_asic() + + if num_asics > 1: + return True + else: + return False + +"""In case of Multi-Asic platform, Each ASIC will have a linux network namespace created. + So we loop through the databases in different namespaces and depending on the sub_role + decide whether this is a front end ASIC/namespace or a back end one. +""" +def get_all_namespaces(): + front_ns = [] + back_ns = [] + num_asics = _get_num_asic() + + if is_multi_asic(): + for asic in range(num_asics): + namespace = "{}{}".format(NS_PREFIX, asic) + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + config_db.connect() + + metadata = config_db.get_table('DEVICE_METADATA') + if metadata['localhost']['sub_role'] == 'FrontEnd': + front_ns.append(namespace) + elif metadata['localhost']['sub_role'] == 'BackEnd': + back_ns.append(namespace) + + return {'front_ns':front_ns, 'back_ns':back_ns} + +# Validate whether a given namespace name is valid in the device. +def validate_namespace(namespace): + if not is_multi_asic(): + return True + + namespaces = get_all_namespaces() + if namespace in namespaces['front_ns'] + namespaces['back_ns']: + return True + else: + return False def interface_alias_to_name(interface_alias): """Return default interface name if alias name is given as argument @@ -542,32 +587,96 @@ def config(): config.add_command(nat.nat) @config.command() +@click.option('-n', '--namespace', help='Namespace name') @click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, - expose_value=False, prompt='Existing file will be overwritten, continue?') -@click.argument('filename', default='/etc/sonic/config_db.json', type=click.Path()) -def save(filename): + expose_value=False, prompt='Existing files will be overwritten, continue?') +@click.argument('filename', default=DEFAULT_CONFIG_DB_FILE, type=click.Path()) +def save(namespace, filename): """Export current config DB to a file on disk.""" - command = "{} -d --print-data > {}".format(SONIC_CFGGEN_PATH, filename) - run_command(command, display_cmd=True) + if namespace: + if not is_multi_asic(): + click.echo("namespace is not significant in a Single ASIC platform") + return None + if not validate_namespace(namespace): + click.echo("invalid Namespace entered {}".format(namespace)) + return None + if filename == DEFAULT_CONFIG_DB_FILE: + inst = namespace[len(NS_PREFIX)] + filename = "/etc/sonic/config_db{}.json".format(inst) + click.echo("valid config_db file for namespace {} would be {}".format(namespace, filename)) + return None + + command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, filename) + run_command(command, display_cmd=True) + else: + # Save config for the database service running on linux host + command = "{} -d --print-data > {}".format(SONIC_CFGGEN_PATH, filename) + run_command(command, display_cmd=True) + + """In case of multi-asic mode we have additional config_db{NS}.json files for + various namespaces created per ASIC. {NS} is the namespace index. + """ + if is_multi_asic(): + ns_list = get_all_namespaces() + namespaces = ns_list['front_ns'] + ns_list['back_ns'] + for namespace in namespaces: + inst = namespace[len(NS_PREFIX)] + filename = "/etc/sonic/config_db{}.json".format(inst) + command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, filename) + run_command(command, display_cmd=True) @config.command() +@click.option('-n', '--namespace', help='Namespace name') @click.option('-y', '--yes', is_flag=True) -@click.argument('filename', default='/etc/sonic/config_db.json', type=click.Path(exists=True)) -def load(filename, yes): +@click.argument('filename', default=DEFAULT_CONFIG_DB_FILE, type=click.Path(exists=True)) +def load(filename, yes, namespace): """Import a previous saved config DB dump file.""" if not yes: click.confirm('Load config from the file %s?' % filename, abort=True) - command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, filename) - run_command(command, display_cmd=True) + + if namespace: + if not is_multi_asic(): + click.echo("namespace is not significant in a Single ASIC device") + return None + if not validate_namespace(namespace): + click.echo("invalid Namespace entered {}".format(namespace)) + return None + if filename == DEFAULT_CONFIG_DB_FILE: + inst = namespace[len(NS_PREFIX)] + filename = "/etc/sonic/config_db{}.json".format(inst) + click.echo("valid config_db file for namespace {} would be {}".format(namespace, filename)) + return None + + command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, filename) + run_command(command, display_cmd=True) + else: + # Load config for the database service running on linux host + command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, filename) + run_command(command, display_cmd=True) + + """In case of multi-asic mode we have additional config_db{NS}.json files for + various namespaces created per ASIC. {NS} is the namespace index. + """ + if is_multi_asic(): + ns_list = get_all_namespaces() + namespaces = ns_list['front_ns'] + ns_list['back_ns'] + for namespace in namespaces: + inst = namespace[len(NS_PREFIX)] + filename = "/etc/sonic/config_db{}.json".format(inst) + if os.path.isfile(filename): + command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, filename) + run_command(command, display_cmd=True) + else: + click.echo("The config_db file {} doesn't exist".format(filename)) @config.command() @click.option('-y', '--yes', is_flag=True) @click.option('-l', '--load-sysinfo', is_flag=True, help='load system default information (mac, portmap etc) first.') -@click.argument('filename', default='/etc/sonic/config_db.json', type=click.Path(exists=True)) +@click.argument('filename', default=DEFAULT_CONFIG_DB_FILE, type=click.Path(exists=True)) def reload(filename, yes, load_sysinfo): """Clear current configuration and import a previous saved config DB dump file.""" if not yes: - click.confirm('Clear current config and reload config from the file %s?' % filename, abort=True) + click.confirm('Clear current config and reload config ?', abort=True) log_info("'reload' executing...") @@ -584,26 +693,37 @@ def reload(filename, yes, load_sysinfo): #Stop services before config push log_info("'reload' stopping services...") _stop_services() - config_db = ConfigDBConnector() - config_db.connect() - client = config_db.get_redis_client(config_db.CONFIG_DB) - client.flushdb() - if load_sysinfo: - command = "{} -H -k {} --write-to-db".format(SONIC_CFGGEN_PATH, cfg_hwsku) - run_command(command, display_cmd=True) - if os.path.isfile(INIT_CFG_FILE): - command = "{} -j {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, filename) - else: - command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, filename) + """ This logic is common to the Single ASIC And multiple ASIC platforms. In the case of Single AISC + platforms we have single database service. In multi-ASIC platforms we have a global database + service running in the host + database services running in namespace created per ASIC. + Here we get all namespaces in the system. For single asic the ns_list will be empty. + By default we add the current namespace which we are in '' + """ + ns_list = get_all_namespaces() + namespaces = [''] + ns_list['front_ns'] + ns_list['back_ns'] + for namespace in namespaces: + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + config_db.connect() + client = config_db.get_redis_client(config_db.CONFIG_DB) + client.flushdb() + if load_sysinfo: + command = "{} -H -k {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, cfg_hwsku, namespace) + run_command(command, display_cmd=True) - run_command(command, display_cmd=True) - client.set(config_db.INIT_INDICATOR, 1) + if os.path.isfile(INIT_CFG_FILE): + command = "{} -j {} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, filename, namespace) + else: + command = "{} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, filename, namespace) - # Migrate DB contents to latest version - db_migrator='/usr/bin/db_migrator.py' - if os.path.isfile(db_migrator) and os.access(db_migrator, os.X_OK): - run_command(db_migrator + ' -o migrate') + run_command(command, display_cmd=True) + client.set(config_db.INIT_INDICATOR, 1) + + # Migrate DB contents to latest version + db_migrator='/usr/bin/db_migrator.py' + if os.path.isfile(db_migrator) and os.access(db_migrator, os.X_OK): + command = "{} -o migrate -n \"{}\"".format(db_migrator, namespace) + run_command(command, display_cmd=True) # We first run "systemctl reset-failed" to remove the "failed" # status from all services before we attempt to restart them diff --git a/scripts/db_migrator.py b/scripts/db_migrator.py index 8d61c29203..ee5a67f383 100755 --- a/scripts/db_migrator.py +++ b/scripts/db_migrator.py @@ -24,7 +24,7 @@ def log_error(msg): class DBMigrator(): - def __init__(self, socket=None): + def __init__(self, namespace, socket=None): """ Version string format: version___ @@ -46,10 +46,12 @@ def __init__(self, socket=None): if socket: db_kwargs['unix_socket_path'] = socket - self.configDB = ConfigDBConnector(**db_kwargs) + if namespace is None: + self.configDB = ConfigDBConnector(**db_kwargs) + else: + self.configDB = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace, **db_kwargs) self.configDB.db_connect('CONFIG_DB') - def migrate_pfc_wd_table(self): ''' Migrate all data entries from table PFC_WD_TABLE to PFC_WD @@ -291,14 +293,22 @@ def main(): required = False, help = 'the unix socket that the desired database listens on', default = None ) + parser.add_argument('-n', + dest='namespace', + metavar='namespace details', + type = str, + required = False, + help = 'The namespace whose DB instance we need to connect', + default = None ) args = parser.parse_args() operation = args.operation socket_path = args.socket + namespace = args.namespace if socket_path: - dbmgtr = DBMigrator(socket=socket_path) + dbmgtr = DBMigrator(namespace, socket=socket_path) else: - dbmgtr = DBMigrator() + dbmgtr = DBMigrator(namespace) result = getattr(dbmgtr, operation)() if result: From 27cdded73edb02d39d4909d7b521f51af88aa7ea Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Fri, 10 Apr 2020 10:51:04 -0700 Subject: [PATCH 2/8] Fixes and comment updates --- config/main.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/config/main.py b/config/main.py index 33f284a786..bce1fd559f 100755 --- a/config/main.py +++ b/config/main.py @@ -694,11 +694,10 @@ def reload(filename, yes, load_sysinfo): log_info("'reload' stopping services...") _stop_services() - """ This logic is common to the Single ASIC And multiple ASIC platforms. In the case of Single AISC - platforms we have single database service. In multi-ASIC platforms we have a global database - service running in the host + database services running in namespace created per ASIC. - Here we get all namespaces in the system. For single asic the ns_list will be empty. - By default we add the current namespace which we are in '' + """ In Single AISC platforms we have single DB service. In multi-ASIC platforms we have a global DB + service running in the host + DB services running in the namespace created per ASIC. + In the below logic, we get all namespaces in this platform and add an empty namespace '' + denoting the current namespace which we are in ( the linux host ) """ ns_list = get_all_namespaces() namespaces = [''] + ns_list['front_ns'] + ns_list['back_ns'] @@ -711,10 +710,19 @@ def reload(filename, yes, load_sysinfo): command = "{} -H -k {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, cfg_hwsku, namespace) run_command(command, display_cmd=True) + # For the database service running in linux host we use the file user gives as input + # or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace, + # the default config_db.json format is used. + if namespace == '': + cfg_file = filename + else: + inst = namespace[len(NS_PREFIX)] + cfg_file = "/etc/sonic/config_db{}.json".format(inst) + if os.path.isfile(INIT_CFG_FILE): - command = "{} -j {} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, filename, namespace) + command = "{} -j {} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, cfg_file, namespace) else: - command = "{} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, filename, namespace) + command = "{} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, cfg_file, namespace) run_command(command, display_cmd=True) client.set(config_db.INIT_INDICATOR, 1) From eaa2a2d94a306f671c239d31e309e5d9fc93b4e3 Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Sun, 19 Apr 2020 16:11:58 -0700 Subject: [PATCH 3/8] Comments addressed + added support for user to input the config files per namespace also. --- config/main.py | 155 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 53 deletions(-) diff --git a/config/main.py b/config/main.py index bce1fd559f..e5b8cf647c 100755 --- a/config/main.py +++ b/config/main.py @@ -27,9 +27,8 @@ SYSLOG_IDENTIFIER = "config" VLAN_SUB_INTERFACE_SEPARATOR = '.' ASIC_CONF_FILENAME = 'asic.conf' -PLATFORM_ROOT_PATH = '/usr/share/sonic/device' DEFAULT_CONFIG_DB_FILE = '/etc/sonic/config_db.json' -NS_PREFIX = 'asic' +NAMESPACE_PREFIX = 'asic' INIT_CFG_FILE = '/etc/sonic/init_cfg.json' @@ -137,7 +136,7 @@ def get_all_namespaces(): if is_multi_asic(): for asic in range(num_asics): - namespace = "{}{}".format(NS_PREFIX, asic) + namespace = "{}{}".format(NAMESPACE_PREFIX, asic) config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) config_db.connect() @@ -590,93 +589,129 @@ def config(): @click.option('-n', '--namespace', help='Namespace name') @click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, expose_value=False, prompt='Existing files will be overwritten, continue?') -@click.argument('filename', default=DEFAULT_CONFIG_DB_FILE, type=click.Path()) +@click.argument('filename',nargs=-1) def save(namespace, filename): """Export current config DB to a file on disk.""" if namespace: if not is_multi_asic(): - click.echo("namespace is not significant in a Single ASIC platform") + click.echo("Namespace is not significant in a Single ASIC platform") return None if not validate_namespace(namespace): - click.echo("invalid Namespace entered {}".format(namespace)) - return None - if filename == DEFAULT_CONFIG_DB_FILE: - inst = namespace[len(NS_PREFIX)] - filename = "/etc/sonic/config_db{}.json".format(inst) - click.echo("valid config_db file for namespace {} would be {}".format(namespace, filename)) + click.echo("Invalid Namespace entered {}".format(namespace)) return None + if not len(filename): + inst = namespace[len(NAMESPACE_PREFIX)] + cfg_file = "/etc/sonic/config_db{}.json".format(inst) + else: + cfg_file = filename[0] - command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, filename) + command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, cfg_file) run_command(command, display_cmd=True) else: - # Save config for the database service running on linux host - command = "{} -d --print-data > {}".format(SONIC_CFGGEN_PATH, filename) + if not len(filename): + host_cfg_file = DEFAULT_CONFIG_DB_FILE + else: + host_cfg_file = filename[0] + + command = "{} -d --print-data > {}".format(SONIC_CFGGEN_PATH, host_cfg_file) run_command(command, display_cmd=True) """In case of multi-asic mode we have additional config_db{NS}.json files for various namespaces created per ASIC. {NS} is the namespace index. """ if is_multi_asic(): - ns_list = get_all_namespaces() - namespaces = ns_list['front_ns'] + ns_list['back_ns'] - for namespace in namespaces: - inst = namespace[len(NS_PREFIX)] - filename = "/etc/sonic/config_db{}.json".format(inst) - command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, filename) + num_asic = _get_num_asic() + for inst in range(num_asic): + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) + # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json + if len(filename) > inst+1: + cfg_file = filename[inst+1] + else: + cfg_file = "/etc/sonic/config_db{}.json".format(inst) + + command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, cfg_file) run_command(command, display_cmd=True) @config.command() @click.option('-n', '--namespace', help='Namespace name') @click.option('-y', '--yes', is_flag=True) -@click.argument('filename', default=DEFAULT_CONFIG_DB_FILE, type=click.Path(exists=True)) +@click.argument('filename',nargs=-1) def load(filename, yes, namespace): """Import a previous saved config DB dump file.""" - if not yes: - click.confirm('Load config from the file %s?' % filename, abort=True) - if namespace: if not is_multi_asic(): - click.echo("namespace is not significant in a Single ASIC device") + click.echo("Namespace is not significant in a Single ASIC platform") return None if not validate_namespace(namespace): - click.echo("invalid Namespace entered {}".format(namespace)) - return None - if filename == DEFAULT_CONFIG_DB_FILE: - inst = namespace[len(NS_PREFIX)] - filename = "/etc/sonic/config_db{}.json".format(inst) - click.echo("valid config_db file for namespace {} would be {}".format(namespace, filename)) + click.echo("Invalid Namespace entered {}".format(namespace)) return None + if not len(filename): + inst = namespace[len(NAMESPACE_PREFIX)] + cfg_file = "/etc/sonic/config_db{}.json".format(inst) + else: + cfg_file = filename[0] + + if not os.path.isfile(cfg_file): + click.echo("The config_db file {} doesn't exist".format(cfg_file)) + return - command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, filename) + if not yes: + click.confirm('Load config from the file %s ?' % cfg_file, abort=True) + + command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, cfg_file) run_command(command, display_cmd=True) else: + if not len(filename): + message = 'Load config from the default config file[s] ?' + host_cfg_file = DEFAULT_CONFIG_DB_FILE + else: + message = 'Load config from the file[s] {} ?'.format(tuple(str(i) for i in filename)) + host_cfg_file = filename[0] + + if not yes: + click.confirm(message, abort=True) + # Load config for the database service running on linux host - command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, filename) - run_command(command, display_cmd=True) + if os.path.isfile(host_cfg_file): + command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, host_cfg_file) + run_command(command, display_cmd=True) + else: + click.echo("The config_db file {} doesn't exist".format(host_cfg_file)) + return """In case of multi-asic mode we have additional config_db{NS}.json files for various namespaces created per ASIC. {NS} is the namespace index. """ if is_multi_asic(): - ns_list = get_all_namespaces() - namespaces = ns_list['front_ns'] + ns_list['back_ns'] - for namespace in namespaces: - inst = namespace[len(NS_PREFIX)] - filename = "/etc/sonic/config_db{}.json".format(inst) - if os.path.isfile(filename): - command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, filename) + num_asic = _get_num_asic() + for inst in range(num_asic): + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) + # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json + if len(filename) > inst+1: + cfg_file = filename[inst+1] + else: + cfg_file = "/etc/sonic/config_db{}.json".format(inst) + + if os.path.isfile(cfg_file): + command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, cfg_file) run_command(command, display_cmd=True) else: - click.echo("The config_db file {} doesn't exist".format(filename)) + click.echo("The config_db file {} doesn't exist".format(cfg_file)) + return @config.command() @click.option('-y', '--yes', is_flag=True) @click.option('-l', '--load-sysinfo', is_flag=True, help='load system default information (mac, portmap etc) first.') -@click.argument('filename', default=DEFAULT_CONFIG_DB_FILE, type=click.Path(exists=True)) +@click.argument('filename', nargs=-1) def reload(filename, yes, load_sysinfo): """Clear current configuration and import a previous saved config DB dump file.""" + if not len(filename): + message = 'Clear current config and reload config from the default config file[s] ?' + else: + message = 'Clear current config and reload config from the file[s] {} ?'.format(tuple(str(i) for i in filename)) + if not yes: - click.confirm('Clear current config and reload config ?', abort=True) + click.confirm(message, abort=True) log_info("'reload' executing...") @@ -699,9 +734,28 @@ def reload(filename, yes, load_sysinfo): In the below logic, we get all namespaces in this platform and add an empty namespace '' denoting the current namespace which we are in ( the linux host ) """ - ns_list = get_all_namespaces() - namespaces = [''] + ns_list['front_ns'] + ns_list['back_ns'] - for namespace in namespaces: + num_asic = _get_num_asic() + for inst in range(-1, num_asic-1): + # Get the namespace name, for linux host it is '' empty namespace string. + if inst is -1: + namespace = '' + else: + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) + + # Get the config file, based on user input + if len(filename) > inst+1: + cfg_file = filename[inst+1] + else: + if namespace is '': + cfg_file = DEFAULT_CONFIG_DB_FILE + else: + cfg_file = "/etc/sonic/config_db{}.json".format(inst) + + #Check the file exists before proceeding. + if not os.path.isfile(cfg_file): + click.echo("The config_db file {} doesn't exist".format(cfg_file)) + continue + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) config_db.connect() client = config_db.get_redis_client(config_db.CONFIG_DB) @@ -711,13 +765,8 @@ def reload(filename, yes, load_sysinfo): run_command(command, display_cmd=True) # For the database service running in linux host we use the file user gives as input - # or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace, + # or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace, # the default config_db.json format is used. - if namespace == '': - cfg_file = filename - else: - inst = namespace[len(NS_PREFIX)] - cfg_file = "/etc/sonic/config_db{}.json".format(inst) if os.path.isfile(INIT_CFG_FILE): command = "{} -j {} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, cfg_file, namespace) From 40873b2ffe90454bdab66622396aac4d06d6232c Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Sat, 25 Apr 2020 18:16:20 -0700 Subject: [PATCH 4/8] Updates per comments + based on the updated SonicV2Connector/ConfigDBConnector class design --- config/main.py | 254 +++++++++++++++++++++++------------------ scripts/db_migrator.py | 9 +- 2 files changed, 148 insertions(+), 115 deletions(-) diff --git a/config/main.py b/config/main.py index e5b8cf647c..e46a7926b6 100755 --- a/config/main.py +++ b/config/main.py @@ -14,6 +14,7 @@ import ipaddress from swsssdk import ConfigDBConnector from swsssdk import SonicV2Connector +from swsssdk import SonicDBConfig from minigraph import parse_device_desc_xml import aaa @@ -578,6 +579,10 @@ def is_ipaddress(val): @click.group(context_settings=CONTEXT_SETTINGS) def config(): """SONiC command line - 'config' command""" + + # Load the global config file database_global.json once. + SonicDBConfig.load_sonic_global_db_config() + if os.geteuid() != 0: exit("Root privileges are required for this operation") config.add_command(aaa.aaa) @@ -586,135 +591,145 @@ def config(): config.add_command(nat.nat) @config.command() -@click.option('-n', '--namespace', help='Namespace name') @click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, expose_value=False, prompt='Existing files will be overwritten, continue?') -@click.argument('filename',nargs=-1) -def save(namespace, filename): +@click.argument('filename', required=False) +def save(filename): """Export current config DB to a file on disk.""" - if namespace: - if not is_multi_asic(): - click.echo("Namespace is not significant in a Single ASIC platform") - return None - if not validate_namespace(namespace): - click.echo("Invalid Namespace entered {}".format(namespace)) - return None - if not len(filename): - inst = namespace[len(NAMESPACE_PREFIX)] - cfg_file = "/etc/sonic/config_db{}.json".format(inst) - else: - cfg_file = filename[0] + num_asic = _get_num_asic() + cfg_files = [] - command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, cfg_file) - run_command(command, display_cmd=True) - else: - if not len(filename): - host_cfg_file = DEFAULT_CONFIG_DB_FILE + num_cfg_file = 1 + if is_multi_asic(): + num_cfg_file += num_asic + + # If the user give the filename[s], extract the file names. + if filename is not None: + cfg_files = filename.split(',') + + if len(cfg_files) != num_cfg_file: + click.echo("Input {} config file[s] separated by comma if needed".format(num_cfg_file)) + return + + """In case of multi-asic mode we have additional config_db{NS}.json files for + various namespaces created per ASIC. {NS} is the namespace index. + """ + for inst in range(-1, num_cfg_file-1): + #inst = -1, refers to the linux host where there is no namespace. + if inst is -1: + namespace = None else: - host_cfg_file = filename[0] + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) - command = "{} -d --print-data > {}".format(SONIC_CFGGEN_PATH, host_cfg_file) - run_command(command, display_cmd=True) + # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json + if cfg_files: + file = cfg_files[inst+1] + else: + if namespace is None: + file = DEFAULT_CONFIG_DB_FILE + else: + file = "/etc/sonic/config_db{}.json".format(inst) - """In case of multi-asic mode we have additional config_db{NS}.json files for - various namespaces created per ASIC. {NS} is the namespace index. - """ - if is_multi_asic(): - num_asic = _get_num_asic() - for inst in range(num_asic): - namespace = "{}{}".format(NAMESPACE_PREFIX, inst) - # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json - if len(filename) > inst+1: - cfg_file = filename[inst+1] - else: - cfg_file = "/etc/sonic/config_db{}.json".format(inst) + if namespace is None: + command = "{} -d --print-data > {}".format(SONIC_CFGGEN_PATH, file) + else: + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) + command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, file) - command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, cfg_file) - run_command(command, display_cmd=True) + run_command(command, display_cmd=True) @config.command() -@click.option('-n', '--namespace', help='Namespace name') @click.option('-y', '--yes', is_flag=True) -@click.argument('filename',nargs=-1) -def load(filename, yes, namespace): +@click.argument('filename', required=False) +def load(filename, yes): """Import a previous saved config DB dump file.""" - if namespace: - if not is_multi_asic(): - click.echo("Namespace is not significant in a Single ASIC platform") - return None - if not validate_namespace(namespace): - click.echo("Invalid Namespace entered {}".format(namespace)) - return None - if not len(filename): - inst = namespace[len(NAMESPACE_PREFIX)] - cfg_file = "/etc/sonic/config_db{}.json".format(inst) - else: - cfg_file = filename[0] + if filename is None: + message = 'Load config from the default config file[s] ?' + else: + message = 'Load config from the file[s] {} ?'.format(filename) + + if not yes: + click.confirm(message, abort=True) + + num_asic = _get_num_asic() + cfg_files = [] + + num_cfg_file = 1 + if is_multi_asic(): + num_cfg_file += num_asic - if not os.path.isfile(cfg_file): - click.echo("The config_db file {} doesn't exist".format(cfg_file)) + # If the user give the filename[s], extract the file names. + if filename is not None: + cfg_files = filename.split(',') + + if len(cfg_files) != num_cfg_file: + click.echo("Input {} config file[s] separated by comma if needed".format(num_cfg_file)) return - if not yes: - click.confirm('Load config from the file %s ?' % cfg_file, abort=True) + """In case of multi-asic mode we have additional config_db{NS}.json files for + various namespaces created per ASIC. {NS} is the namespace index. + """ + for inst in range(-1, num_cfg_file-1): + #inst = -1, refers to the linux host where there is no namespace. + if inst is -1: + namespace = None + else: + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) - command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, cfg_file) - run_command(command, display_cmd=True) - else: - if not len(filename): - message = 'Load config from the default config file[s] ?' - host_cfg_file = DEFAULT_CONFIG_DB_FILE + # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json + if cfg_files: + file = cfg_files[inst+1] else: - message = 'Load config from the file[s] {} ?'.format(tuple(str(i) for i in filename)) - host_cfg_file = filename[0] + if namespace is None: + file = DEFAULT_CONFIG_DB_FILE + else: + file = "/etc/sonic/config_db{}.json".format(inst) - if not yes: - click.confirm(message, abort=True) + # if any of the config files in linux host OR namespace is not present, return + if not os.path.isfile(file): + click.echo("The config_db file {} doesn't exist".format(file)) + return - # Load config for the database service running on linux host - if os.path.isfile(host_cfg_file): - command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, host_cfg_file) - run_command(command, display_cmd=True) + if namespace is None: + command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, file) else: - click.echo("The config_db file {} doesn't exist".format(host_cfg_file)) - return + namespace = "{}{}".format(NAMESPACE_PREFIX, inst) + command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, file) - """In case of multi-asic mode we have additional config_db{NS}.json files for - various namespaces created per ASIC. {NS} is the namespace index. - """ - if is_multi_asic(): - num_asic = _get_num_asic() - for inst in range(num_asic): - namespace = "{}{}".format(NAMESPACE_PREFIX, inst) - # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json - if len(filename) > inst+1: - cfg_file = filename[inst+1] - else: - cfg_file = "/etc/sonic/config_db{}.json".format(inst) + run_command(command, display_cmd=True) - if os.path.isfile(cfg_file): - command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, cfg_file) - run_command(command, display_cmd=True) - else: - click.echo("The config_db file {} doesn't exist".format(cfg_file)) - return @config.command() @click.option('-y', '--yes', is_flag=True) @click.option('-l', '--load-sysinfo', is_flag=True, help='load system default information (mac, portmap etc) first.') -@click.argument('filename', nargs=-1) +@click.argument('filename', required=False) def reload(filename, yes, load_sysinfo): """Clear current configuration and import a previous saved config DB dump file.""" - if not len(filename): + if filename is None: message = 'Clear current config and reload config from the default config file[s] ?' else: - message = 'Clear current config and reload config from the file[s] {} ?'.format(tuple(str(i) for i in filename)) + message = 'Clear current config and reload config from the file[s] {} ?'.format(filename) if not yes: click.confirm(message, abort=True) log_info("'reload' executing...") + num_asic = _get_num_asic() + cfg_files = [] + + num_cfg_file = 1 + if is_multi_asic(): + num_cfg_file += num_asic + + # If the user give the filename[s], extract the file names. + if filename is not None: + cfg_files = filename.split(',') + + if len(cfg_files) != num_cfg_file: + click.echo("Input {} config file[s] separated by comma if needed".format(num_cfg_file)) + return + if load_sysinfo: command = "{} -j {} -v DEVICE_METADATA.localhost.hwsku".format(SONIC_CFGGEN_PATH, filename) proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE) @@ -730,48 +745,60 @@ def reload(filename, yes, load_sysinfo): _stop_services() """ In Single AISC platforms we have single DB service. In multi-ASIC platforms we have a global DB - service running in the host + DB services running in the namespace created per ASIC. + service running in the host + DB services running in each ASIC namespace created per ASIC. In the below logic, we get all namespaces in this platform and add an empty namespace '' denoting the current namespace which we are in ( the linux host ) """ - num_asic = _get_num_asic() - for inst in range(-1, num_asic-1): - # Get the namespace name, for linux host it is '' empty namespace string. + for inst in range(-1, num_cfg_file-1): + # Get the namespace name, for linux host it is None if inst is -1: - namespace = '' + namespace = None else: namespace = "{}{}".format(NAMESPACE_PREFIX, inst) - # Get the config file, based on user input - if len(filename) > inst+1: - cfg_file = filename[inst+1] + # Get the file from user input, else take the default file /etc/sonic/config_db{NS_id}.json + if cfg_files: + file = cfg_files[inst+1] else: - if namespace is '': - cfg_file = DEFAULT_CONFIG_DB_FILE + if namespace is None: + file = DEFAULT_CONFIG_DB_FILE else: - cfg_file = "/etc/sonic/config_db{}.json".format(inst) + file = "/etc/sonic/config_db{}.json".format(inst) #Check the file exists before proceeding. - if not os.path.isfile(cfg_file): - click.echo("The config_db file {} doesn't exist".format(cfg_file)) + if not os.path.isfile(file): + click.echo("The config_db file {} doesn't exist".format(file)) continue - config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + if namespace is None: + config_db = ConfigDBConnector() + else: + config_db = ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + config_db.connect() client = config_db.get_redis_client(config_db.CONFIG_DB) client.flushdb() if load_sysinfo: - command = "{} -H -k {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, cfg_hwsku, namespace) + if namespace is None: + command = "{} -H -k {} --write-to-db".format(SONIC_CFGGEN_PATH, cfg_hwsku) + else: + command = "{} -H -k {} -n {} --write-to-db".format(SONIC_CFGGEN_PATH, cfg_hwsku, namespace) run_command(command, display_cmd=True) # For the database service running in linux host we use the file user gives as input # or by default DEFAULT_CONFIG_DB_FILE. In the case of database service running in namespace, # the default config_db.json format is used. - if os.path.isfile(INIT_CFG_FILE): - command = "{} -j {} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, cfg_file, namespace) + if namespace is None: + if os.path.isfile(INIT_CFG_FILE): + command = "{} -j {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, file) + else: + command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, file) else: - command = "{} -j {} -n \"{}\" --write-to-db".format(SONIC_CFGGEN_PATH, cfg_file, namespace) + if os.path.isfile(INIT_CFG_FILE): + command = "{} -j {} -j {} -n {} --write-to-db".format(SONIC_CFGGEN_PATH, INIT_CFG_FILE, file, namespace) + else: + command = "{} -j {} -n {} --write-to-db".format(SONIC_CFGGEN_PATH, file, namespace) run_command(command, display_cmd=True) client.set(config_db.INIT_INDICATOR, 1) @@ -779,7 +806,10 @@ def reload(filename, yes, load_sysinfo): # Migrate DB contents to latest version db_migrator='/usr/bin/db_migrator.py' if os.path.isfile(db_migrator) and os.access(db_migrator, os.X_OK): - command = "{} -o migrate -n \"{}\"".format(db_migrator, namespace) + if namespace is None: + command = "{} -o migrate".format(db_migrator) + else: + command = "{} -o migrate -n {}".format(db_migrator, namespace) run_command(command, display_cmd=True) # We first run "systemctl reset-failed" to remove the "failed" diff --git a/scripts/db_migrator.py b/scripts/db_migrator.py index ee5a67f383..4ea89e0bff 100755 --- a/scripts/db_migrator.py +++ b/scripts/db_migrator.py @@ -4,7 +4,7 @@ import sys import argparse import syslog -from swsssdk import ConfigDBConnector +from swsssdk import ConfigDBConnector, SonicDBConfig import sonic_device_util @@ -295,16 +295,19 @@ def main(): default = None ) parser.add_argument('-n', dest='namespace', - metavar='namespace details', + metavar='asic namespace', type = str, required = False, - help = 'The namespace whose DB instance we need to connect', + help = 'The asic namespace whose DB instance we need to connect', default = None ) args = parser.parse_args() operation = args.operation socket_path = args.socket namespace = args.namespace + if args.namespace is not None: + SonicDBConfig.load_sonic_global_db_config(namespace=args.namespace) + if socket_path: dbmgtr = DBMigrator(namespace, socket=socket_path) else: From 2f111395a164c703753f830e688f6bd688293304 Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Fri, 1 May 2020 08:06:14 -0700 Subject: [PATCH 5/8] Review comments update. --- config/main.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/config/main.py b/config/main.py index e46a7926b6..8f1bcd144b 100755 --- a/config/main.py +++ b/config/main.py @@ -12,9 +12,7 @@ import sonic_device_util import ipaddress -from swsssdk import ConfigDBConnector -from swsssdk import SonicV2Connector -from swsssdk import SonicDBConfig +from swsssdk import ConfigDBConnector, SonicV2Connector, SonicDBConfig from minigraph import parse_device_desc_xml import aaa @@ -147,7 +145,7 @@ def get_all_namespaces(): elif metadata['localhost']['sub_role'] == 'BackEnd': back_ns.append(namespace) - return {'front_ns':front_ns, 'back_ns':back_ns} + return {'front_ns': front_ns, 'back_ns': back_ns} # Validate whether a given namespace name is valid in the device. def validate_namespace(namespace): @@ -595,7 +593,9 @@ def config(): expose_value=False, prompt='Existing files will be overwritten, continue?') @click.argument('filename', required=False) def save(filename): - """Export current config DB to a file on disk.""" + """Export current config DB to a file on disk.\n + [FILENAME] : Filenames separated by comma with no spaces in between. + """ num_asic = _get_num_asic() cfg_files = [] @@ -608,7 +608,7 @@ def save(filename): cfg_files = filename.split(',') if len(cfg_files) != num_cfg_file: - click.echo("Input {} config file[s] separated by comma if needed".format(num_cfg_file)) + click.echo("Input {} config file[s] separated by comma for multiple files ".format(num_cfg_file)) return """In case of multi-asic mode we have additional config_db{NS}.json files for @@ -633,7 +633,6 @@ def save(filename): if namespace is None: command = "{} -d --print-data > {}".format(SONIC_CFGGEN_PATH, file) else: - namespace = "{}{}".format(NAMESPACE_PREFIX, inst) command = "{} -n {} -d --print-data > {}".format(SONIC_CFGGEN_PATH, namespace, file) run_command(command, display_cmd=True) @@ -642,7 +641,9 @@ def save(filename): @click.option('-y', '--yes', is_flag=True) @click.argument('filename', required=False) def load(filename, yes): - """Import a previous saved config DB dump file.""" + """Import a previous saved config DB dump file. + [FILENAME] : Filenames separated by comma with no spaces in between. + """ if filename is None: message = 'Load config from the default config file[s] ?' else: @@ -663,7 +664,7 @@ def load(filename, yes): cfg_files = filename.split(',') if len(cfg_files) != num_cfg_file: - click.echo("Input {} config file[s] separated by comma if needed".format(num_cfg_file)) + click.echo("Input {} config file[s] separated by comma for multiple files ".format(num_cfg_file)) return """In case of multi-asic mode we have additional config_db{NS}.json files for @@ -693,7 +694,6 @@ def load(filename, yes): if namespace is None: command = "{} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, file) else: - namespace = "{}{}".format(NAMESPACE_PREFIX, inst) command = "{} -n {} -j {} --write-to-db".format(SONIC_CFGGEN_PATH, namespace, file) run_command(command, display_cmd=True) @@ -704,7 +704,9 @@ def load(filename, yes): @click.option('-l', '--load-sysinfo', is_flag=True, help='load system default information (mac, portmap etc) first.') @click.argument('filename', required=False) def reload(filename, yes, load_sysinfo): - """Clear current configuration and import a previous saved config DB dump file.""" + """Clear current configuration and import a previous saved config DB dump file. + [FILENAME] : Filenames separated by comma with no spaces in between. + """ if filename is None: message = 'Clear current config and reload config from the default config file[s] ?' else: @@ -727,7 +729,7 @@ def reload(filename, yes, load_sysinfo): cfg_files = filename.split(',') if len(cfg_files) != num_cfg_file: - click.echo("Input {} config file[s] separated by comma if needed".format(num_cfg_file)) + click.echo("Input {} config file[s] separated by comma for multiple files ".format(num_cfg_file)) return if load_sysinfo: From 4794bebff152cc9c4b3418ee0d8ffbde47e253c1 Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Fri, 1 May 2020 13:01:59 -0700 Subject: [PATCH 6/8] Help string updated for config save/reload/load --- config/main.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/config/main.py b/config/main.py index 8f1bcd144b..2b7ab2ba71 100755 --- a/config/main.py +++ b/config/main.py @@ -594,7 +594,7 @@ def config(): @click.argument('filename', required=False) def save(filename): """Export current config DB to a file on disk.\n - [FILENAME] : Filenames separated by comma with no spaces in between. + : Names of configuration file(s) to use, separated by comma with no spaces in between """ num_asic = _get_num_asic() cfg_files = [] @@ -608,7 +608,7 @@ def save(filename): cfg_files = filename.split(',') if len(cfg_files) != num_cfg_file: - click.echo("Input {} config file[s] separated by comma for multiple files ".format(num_cfg_file)) + click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file)) return """In case of multi-asic mode we have additional config_db{NS}.json files for @@ -642,12 +642,12 @@ def save(filename): @click.argument('filename', required=False) def load(filename, yes): """Import a previous saved config DB dump file. - [FILENAME] : Filenames separated by comma with no spaces in between. + : Names of configuration file(s) to use, separated by comma with no spaces in between """ if filename is None: - message = 'Load config from the default config file[s] ?' + message = 'Load config from the default config file(s) ?' else: - message = 'Load config from the file[s] {} ?'.format(filename) + message = 'Load config from the file(s) {} ?'.format(filename) if not yes: click.confirm(message, abort=True) @@ -664,7 +664,7 @@ def load(filename, yes): cfg_files = filename.split(',') if len(cfg_files) != num_cfg_file: - click.echo("Input {} config file[s] separated by comma for multiple files ".format(num_cfg_file)) + click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file)) return """In case of multi-asic mode we have additional config_db{NS}.json files for @@ -705,12 +705,12 @@ def load(filename, yes): @click.argument('filename', required=False) def reload(filename, yes, load_sysinfo): """Clear current configuration and import a previous saved config DB dump file. - [FILENAME] : Filenames separated by comma with no spaces in between. + : Names of configuration file(s) to use, separated by comma with no spaces in between """ if filename is None: - message = 'Clear current config and reload config from the default config file[s] ?' + message = 'Clear current config and reload config from the default config file(s) ?' else: - message = 'Clear current config and reload config from the file[s] {} ?'.format(filename) + message = 'Clear current config and reload config from the file(s) {} ?'.format(filename) if not yes: click.confirm(message, abort=True) @@ -729,7 +729,7 @@ def reload(filename, yes, load_sysinfo): cfg_files = filename.split(',') if len(cfg_files) != num_cfg_file: - click.echo("Input {} config file[s] separated by comma for multiple files ".format(num_cfg_file)) + click.echo("Input {} config file(s) separated by comma for multiple files ".format(num_cfg_file)) return if load_sysinfo: From 3f06c98d81830fe7a3047cf7ff6ead8d39acb000 Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Fri, 1 May 2020 14:13:31 -0700 Subject: [PATCH 7/8] Minor update. --- config/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/main.py b/config/main.py index 2b7ab2ba71..10e5ac7e54 100755 --- a/config/main.py +++ b/config/main.py @@ -594,7 +594,7 @@ def config(): @click.argument('filename', required=False) def save(filename): """Export current config DB to a file on disk.\n - : Names of configuration file(s) to use, separated by comma with no spaces in between + : Names of configuration file(s) to save, separated by comma with no spaces in between """ num_asic = _get_num_asic() cfg_files = [] @@ -642,7 +642,7 @@ def save(filename): @click.argument('filename', required=False) def load(filename, yes): """Import a previous saved config DB dump file. - : Names of configuration file(s) to use, separated by comma with no spaces in between + : Names of configuration file(s) to load, separated by comma with no spaces in between """ if filename is None: message = 'Load config from the default config file(s) ?' @@ -705,7 +705,7 @@ def load(filename, yes): @click.argument('filename', required=False) def reload(filename, yes, load_sysinfo): """Clear current configuration and import a previous saved config DB dump file. - : Names of configuration file(s) to use, separated by comma with no spaces in between + : Names of configuration file(s) to reload, separated by comma with no spaces in between """ if filename is None: message = 'Clear current config and reload config from the default config file(s) ?' From 125c2970ed4a2975f946d12d79ab47b52564bc03 Mon Sep 17 00:00:00 2001 From: Judy Joseph Date: Fri, 1 May 2020 14:27:16 -0700 Subject: [PATCH 8/8] Minor update --- config/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/main.py b/config/main.py index 10e5ac7e54..b4a53efba7 100755 --- a/config/main.py +++ b/config/main.py @@ -705,7 +705,7 @@ def load(filename, yes): @click.argument('filename', required=False) def reload(filename, yes, load_sysinfo): """Clear current configuration and import a previous saved config DB dump file. - : Names of configuration file(s) to reload, separated by comma with no spaces in between + : Names of configuration file(s) to load, separated by comma with no spaces in between """ if filename is None: message = 'Clear current config and reload config from the default config file(s) ?'