From b9cee360c2a1b64f46dbc035579bd7cbd9959878 Mon Sep 17 00:00:00 2001 From: judyjoseph <53951155+judyjoseph@users.noreply.github.com> Date: Wed, 22 Apr 2020 23:08:18 -0700 Subject: [PATCH] Namespace support in SonicV2Connector (#63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR is the changes needed to support multiple namespaces for the Multi-ASIC devices. Multi-DB namespace PR --> (Azure/SONiC#567) The changes are mainly to these classes SonicDBConfig SonicV2Connector/ConfigDBConnector A new parameter "namespace" is added to the SonicV2Connector class init , to pass the namespace name. The default value is None representing empty namespace. class SonicV2Connector(DBInterface): def init(self, use_unix_socket_path=False, namespace=None, **kwargs): If the user don't explicitly set this parameter, namespace takes None as value, and connects to the db_name in the local context, which refers to the database docker running in the current namespace wherever you are running this application/script …. (It could be either Linux host or any specific network namespace ). In this way it is backward compatible and the existing behavior of APIs in these utility classes are maintained. In the SonicDBConfig, a new API is introduced load_sonic_global_db_config() which loads the /var/run/redis/sonic-db/database_global.json file if present. This file has the mapping between the namespace name and the corresponding database_config.json file. --- src/swsssdk/configdb.py | 10 ++ src/swsssdk/dbconnector.py | 204 +++++++++++++++++++++++++------ src/swsssdk/scripts/sonic-db-cli | 52 ++++++-- test/config/database_global.json | 20 +++ test/test_moduleLoad.py | 23 +++- 5 files changed, 259 insertions(+), 50 deletions(-) create mode 100644 test/config/database_global.json diff --git a/src/swsssdk/configdb.py b/src/swsssdk/configdb.py index c2420906e351..4ccbb0dca32e 100644 --- a/src/swsssdk/configdb.py +++ b/src/swsssdk/configdb.py @@ -33,6 +33,16 @@ def __init__(self, **kwargs): # By default, connect to Redis through TCP, which does not requires root. if len(kwargs) == 0: kwargs['host'] = '127.0.0.1' + + """The ConfigDBConnector class will accept the parameter 'namespace' which is used to + load the database_config and connect to the redis DB instances in that namespace. + By default namespace is set to None, which means it connects to local redis DB instances. + + When connecting to a different namespace set the use_unix_socket_path flag to true. + Eg. ConfigDBConnector(use_unix_socket_path=True, namespace=namespace) + + 'namespace' is implicitly passed to the parent SonicV2Connector class. + """ super(ConfigDBConnector, self).__init__(**kwargs) self.handlers = {} diff --git a/src/swsssdk/dbconnector.py b/src/swsssdk/dbconnector.py index ed659b3bb84e..7f57b8a43d71 100644 --- a/src/swsssdk/dbconnector.py +++ b/src/swsssdk/dbconnector.py @@ -9,10 +9,76 @@ # FIXME: Convert to metaclasses when Py2 support is removed. Metaclasses have unique interfaces to Python2/Python3. class SonicDBConfig(object): + SONIC_DB_GLOBAL_CONFIG_FILE = "/var/run/redis/sonic-db/database_global.json" SONIC_DB_CONFIG_FILE = "/var/run/redis/sonic-db/database_config.json" + _sonic_db_config_dir = "/var/run/redis/sonic-db" + _sonic_db_global_config_init = False _sonic_db_config_init = False _sonic_db_config = {} + """This is the database_global.json parse and load API. This file has the namespace name and + the corresponding database_config.json file. The global file is significant for the + applications running in the linux host namespace, like eg: config/show cli, snmp etc which + needs to connect to databases running in other namespaces. If the "namespace" attribute is not + specified for an "include" attribute, it refers to the linux host namespace. + If the user passes namespace parameter, this API loads json file for that namespace alone. + """ + @staticmethod + def load_sonic_global_db_config(global_db_file_path=SONIC_DB_GLOBAL_CONFIG_FILE, namespace=None): + """ + Parse and load the global database config json file + """ + if SonicDBConfig._sonic_db_global_config_init == True: + return + + if os.path.isfile(global_db_file_path) == True: + global_db_config_dir = os.path.dirname(global_db_file_path) + with open(global_db_file_path, "r") as read_file: + all_ns_dbs = json.load(read_file) + for entry in all_ns_dbs['INCLUDES']: + if 'namespace' not in entry.keys(): + # If the user already invoked load_sonic_db_config() explicitly to load the + # database_config.json file for current namesapce, skip loading the file + # referenced here in the global config file. + if SonicDBConfig._sonic_db_config_init == True: + continue + ns = '' + else: + ns = entry['namespace'] + + # If API is called with a namespace parameter, load the json file only for that namespace. + if namespace is not None and ns != namespace: + continue; + + # Check if _sonic_db_config already have this namespace present + if ns in SonicDBConfig._sonic_db_config: + msg = "The database_config for this namespace '{}' is already parsed. !!".format(ns) + logger.warning(msg) + continue + + db_include_file = os.path.join(global_db_config_dir, entry['include']) + + # Not finding the database_config.json file for the namespace + if os.path.isfile(db_include_file) == False: + msg = "'{}' file is not found !!".format(db_include_file) + logger.warning(msg) + continue + + # As we load the database_config.json file for current namesapce, + # set the _sonic_db_config_init flag to True to prevent loading again + # by the API load_sonic_db_config() + if ns is '': + SonicDBConfig._sonic_db_config_init = True + + with open(db_include_file, "r") as inc_file: + SonicDBConfig._sonic_db_config[ns] = json.load(inc_file) + + # If API is called with a namespace parameter,we break here as we loaded the json file. + if namespace is not None and ns == namespace: + break; + + SonicDBConfig._sonic_db_global_config_init = True + @staticmethod def load_sonic_db_config(sonic_db_file_path=SONIC_DB_CONFIG_FILE): """ @@ -27,7 +93,8 @@ def load_sonic_db_config(sonic_db_file_path=SONIC_DB_CONFIG_FILE): logger.warning(msg) sonic_db_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config', 'database_config.json') with open(sonic_db_file_path, "r") as read_file: - SonicDBConfig._sonic_db_config = json.load(read_file) + # The database_config.json is loaded with '' as key. This refers to the local namespace. + SonicDBConfig._sonic_db_config[''] = json.load(read_file) except (OSError, IOError): msg = "Could not open sonic database config file '{}'".format(sonic_db_file_path) logger.exception(msg) @@ -35,83 +102,142 @@ def load_sonic_db_config(sonic_db_file_path=SONIC_DB_CONFIG_FILE): SonicDBConfig._sonic_db_config_init = True @staticmethod - def db_name_validation(db_name): + def namespace_validation(namespace): + # Check the namespace is valid. + if namespace is None: + msg = "invalid namespace name given as input" + logger.warning(msg) + raise RuntimeError(msg) + + # Check if the global config is loaded entirely or for the namespace + if namespace != '' and SonicDBConfig._sonic_db_global_config_init == False: + msg = "Load the global DB config first using API load_sonic_global_db_config" + logger.warning(msg) + raise RuntimeError(msg) + if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - if db_name not in SonicDBConfig._sonic_db_config["DATABASES"]: + + if namespace not in SonicDBConfig._sonic_db_config: + msg = "{} is not a valid namespace name in configuration file".format(namespace) + logger.warning(msg) + raise RuntimeError(msg) + + @staticmethod + def EMPTY_NAMESPACE(ns): + if ns is None: + return '' + else: + return ns + + @staticmethod + def db_name_validation(db_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) + if SonicDBConfig._sonic_db_config_init == False: + SonicDBConfig.load_sonic_db_config() + SonicDBConfig.namespace_validation(namespace) + db=SonicDBConfig._sonic_db_config[namespace]["DATABASES"] + if db_name not in db: msg = "{} is not a valid database name in configuration file".format(db_name) - logger.exception(msg) + logger.warning(msg) raise RuntimeError(msg) @staticmethod - def inst_name_validation(inst_name): + def inst_name_validation(inst_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - if inst_name not in SonicDBConfig._sonic_db_config["INSTANCES"]: + SonicDBConfig.namespace_validation(namespace) + instances = SonicDBConfig._sonic_db_config[namespace]["INSTANCES"] + if inst_name not in instances: msg = "{} is not a valid instance name in configuration file".format(inst_name) - logger.exception(msg) + logger.warning(msg) raise RuntimeError(msg) @staticmethod - def get_dblist(): + def get_dblist(namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) + if SonicDBConfig._sonic_db_config_init == False: + SonicDBConfig.load_sonic_db_config() + SonicDBConfig.namespace_validation(namespace) + return SonicDBConfig._sonic_db_config[namespace]["DATABASES"].keys() + + @staticmethod + def get_ns_list(): if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - return SonicDBConfig._sonic_db_config["DATABASES"].keys() + return SonicDBConfig._sonic_db_config.keys() @staticmethod - def get_instance(db_name): + def get_instance(db_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - SonicDBConfig.db_name_validation(db_name) - inst_name = SonicDBConfig._sonic_db_config["DATABASES"][db_name]["instance"] - SonicDBConfig.inst_name_validation(inst_name) - return SonicDBConfig._sonic_db_config["INSTANCES"][inst_name] + SonicDBConfig.db_name_validation(db_name, namespace) + inst_name = SonicDBConfig._sonic_db_config[namespace]["DATABASES"][db_name]["instance"] + SonicDBConfig.inst_name_validation(inst_name, namespace) + return SonicDBConfig._sonic_db_config[namespace]["INSTANCES"][inst_name] @staticmethod - def get_instancelist(): + def get_instancelist(namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - return SonicDBConfig._sonic_db_config["INSTANCES"] + SonicDBConfig.namespace_validation(namespace) + return SonicDBConfig._sonic_db_config[namespace]["INSTANCES"] @staticmethod - def get_socket(db_name): + def get_socket(db_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - SonicDBConfig.db_name_validation(db_name) - return SonicDBConfig.get_instance(db_name)["unix_socket_path"] + return SonicDBConfig.get_instance(db_name, namespace)["unix_socket_path"] @staticmethod - def get_hostname(db_name): + def get_hostname(db_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - SonicDBConfig.db_name_validation(db_name) - return SonicDBConfig.get_instance(db_name)["hostname"] + return SonicDBConfig.get_instance(db_name, namespace)["hostname"] @staticmethod - def get_port(db_name): + def get_port(db_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - SonicDBConfig.db_name_validation(db_name) - return SonicDBConfig.get_instance(db_name)["port"] + return SonicDBConfig.get_instance(db_name, namespace)["port"] @staticmethod - def get_dbid(db_name): + def get_dbid(db_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - SonicDBConfig.db_name_validation(db_name) - return SonicDBConfig._sonic_db_config["DATABASES"][db_name]["id"] + SonicDBConfig.db_name_validation(db_name, namespace) + return SonicDBConfig._sonic_db_config[namespace]["DATABASES"][db_name]["id"] @staticmethod - def get_separator(db_name): + def get_separator(db_name, namespace=None): + namespace = SonicDBConfig.EMPTY_NAMESPACE(namespace) if SonicDBConfig._sonic_db_config_init == False: SonicDBConfig.load_sonic_db_config() - SonicDBConfig.db_name_validation(db_name) - return SonicDBConfig._sonic_db_config["DATABASES"][db_name]["separator"] + SonicDBConfig.db_name_validation(db_name, namespace) + return SonicDBConfig._sonic_db_config[namespace]["DATABASES"][db_name]["separator"] class SonicV2Connector(DBInterface): - def __init__(self, use_unix_socket_path=False, **kwargs): + def __init__(self, use_unix_socket_path=False, namespace=None, **kwargs): super(SonicV2Connector, self).__init__(**kwargs) self.use_unix_socket_path = use_unix_socket_path + + """If the user don't give the namespace as input, it refers to the local namespace + where this application is run. (It could be a network namespace or linux host namesapce) + """ + self.namespace = namespace + + # The TCP connection to a DB in another namespace in not supported. + if namespace is not None and use_unix_socket_path == False: + message = "TCP connectivity to the DB instance in a different namespace is not implemented!" + raise NotImplementedError(message) + for db_name in self.get_db_list(): # set a database name as a constant value attribute. setattr(self, db_name, db_name) @@ -133,25 +259,25 @@ def close(self, db_name): super(SonicV2Connector, self).close(db_id) def get_db_list(self): - return SonicDBConfig.get_dblist() + return SonicDBConfig.get_dblist(self.namespace) def get_db_instance(self, db_name): - return SonicDBConfig.get_instance(db_name) + return SonicDBConfig.get_instance(db_name, self.namespace) def get_db_socket(self, db_name): - return SonicDBConfig.get_socket(db_name) + return SonicDBConfig.get_socket(db_name, self.namespace) def get_db_hostname(self, db_name): - return SonicDBConfig.get_hostname(db_name) + return SonicDBConfig.get_hostname(db_name, self.namespace) def get_db_port(self, db_name): - return SonicDBConfig.get_port(db_name) + return SonicDBConfig.get_port(db_name, self.namespace) def get_dbid(self, db_name): - return SonicDBConfig.get_dbid(db_name) + return SonicDBConfig.get_dbid(db_name, self.namespace) def get_db_separator(self, db_name): - return SonicDBConfig.get_separator(db_name) + return SonicDBConfig.get_separator(db_name, self.namespace) def get_redis_client(self, db_name): db_id = self.get_dbid(db_name) diff --git a/src/swsssdk/scripts/sonic-db-cli b/src/swsssdk/scripts/sonic-db-cli index fa04a8eb9a77..56822456a7c8 100755 --- a/src/swsssdk/scripts/sonic-db-cli +++ b/src/swsssdk/scripts/sonic-db-cli @@ -6,7 +6,19 @@ import redis import argparse from multiprocessing import Pool -def ping_single_instance(inst_info): +def ping_unix_path_single_instance(inst_info): + inst_hostname = inst_info['hostname'] + inst_unix_socket_path = inst_info['unix_socket_path'] + r = redis.Redis(host=inst_hostname, unix_socket_path=inst_unix_socket_path) + rsp = False + msg = 'Could not connect to Redis at {}:{}: Connection refused'.format(inst_hostname, inst_unix_socket_path) + try: + rsp = r.ping() + except redis.exceptions.ConnectionError as e: + pass + return 'PONG' if rsp else msg + +def ping_tcp_single_instance(inst_info): inst_hostname = inst_info['hostname'] inst_port = inst_info['port'] r = redis.Redis(host=inst_hostname, port=inst_port) @@ -18,8 +30,14 @@ def ping_single_instance(inst_info): pass return 'PONG' if rsp else msg -def ping_all_instances(): - db_insts = swsssdk.SonicDBConfig.get_instancelist() +def ping_all_instances(namespace, use_unix_socket=False): + ping_single_instance = ping_unix_path_single_instance + # Use the tcp connectivity if namespace is local and unixsocket cmd_option is present. + if namespace is None: + if not use_unix_socket: + ping_single_instance = ping_tcp_single_instance + + db_insts = swsssdk.SonicDBConfig.get_instancelist(namespace) # ping all redis instances together # TODO: if one of the ping failed, it could fail quickly and not necessary to wait all other pings p = Pool(len(db_insts)) @@ -35,8 +53,14 @@ def ping_all_instances(): print('PONG') sys.exit(0) -def execute_cmd(dbname, cmd): - dbconn = swsssdk.SonicV2Connector(use_unix_socket_path=False) +def execute_cmd(dbname, cmd, namespace, use_unix_socket=False): + if namespace is None: + if use_unix_socket: + dbconn = swsssdk.SonicV2Connector(use_unix_socket_path=True) + else: + dbconn = swsssdk.SonicV2Connector(use_unix_socket_path=False) + else: + dbconn = swsssdk.SonicV2Connector(use_unix_socket_path=True, namespace=namespace) try: dbconn.connect(dbname) except RuntimeError: @@ -64,22 +88,30 @@ def main(): formatter_class=argparse.RawTextHelpFormatter, epilog= """ -Example 1: sonic-db-cli CONFIG_DB keys * -Example 2: sonic-db-cli APPL_DB HGETALL VLAN_TABLE:Vlan10 +**sudo** needed for commands accesing a different namespace [-n], or using unixsocket connection [-s] + +Example 1: sonic-db-cli -n asic0 CONFIG_DB keys \* +Example 2: sonic-db-cli -n asic2 APPL_DB HGETALL VLAN_TABLE:Vlan10 Example 3: sonic-db-cli APPL_DB HGET VLAN_TABLE:Vlan10 mtu -Example 4: sonic-db-cli APPL_DB EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2 +Example 4: sonic-db-cli -n asic3 APPL_DB EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2 Example 5: sonic-db-cli PING +Example 6: sonic-db-cli -s PING """) + parser.add_argument('-s', '--unixsocket', help="Override use of tcp_port and use unixsocket", action='store_true') + parser.add_argument('-n', '--namespace', type=str, help="Namespace string to use asic0/asic1.../asicn", default=None) parser.add_argument('db_or_op', type=str, help='Database name Or Unary operation(only PING supported)') parser.add_argument('cmd', nargs='*', type=str, help='Command to execute in database') args = parser.parse_args() if args.db_or_op: + # Load the database config for the namespace + if args.namespace is not None: + swsssdk.SonicDBConfig.load_sonic_global_db_config(namespace=args.namespace) if args.cmd: - execute_cmd(args.db_or_op, args.cmd) + execute_cmd(args.db_or_op, args.cmd, args.namespace, args.unixsocket) elif args.db_or_op == 'PING': - ping_all_instances() + ping_all_instances(args.namespace, args.unixsocket) # TODO next PR will support 'SAVE' and 'FLUSHALL' # elif args.db_or_op == 'SAVE': # elif args.db_or_op == 'FLUSHALL': diff --git a/test/config/database_global.json b/test/config/database_global.json new file mode 100644 index 000000000000..c4fa84eb85eb --- /dev/null +++ b/test/config/database_global.json @@ -0,0 +1,20 @@ +{ + "INCLUDES" : [ + { + "include" : "database_config.json" + }, + { + "namespace" : "asic0", + "include" : "database_config.json" + }, + { + "namespace" : "asic1", + "include" : "database_config.json" + }, + { + "namespace" : "asic2", + "include" : "database_config.json" + } + ], + "VERSION" : "1.0" +} diff --git a/test/test_moduleLoad.py b/test/test_moduleLoad.py index 0e81b15c0123..937ae3f79d05 100644 --- a/test/test_moduleLoad.py +++ b/test/test_moduleLoad.py @@ -12,4 +12,25 @@ def test__db_map_attributes(self): import swsssdk db = swsssdk.SonicV2Connector() self.assertTrue(all(hasattr(db, db_name) for db_name in db.get_db_list())) - pass + + # This is the test to check if the global config file extraction of namespace is correct. + def test__namespace_list(self): + import swsssdk + dbConfig = swsssdk.SonicDBConfig() + filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), './config', 'database_global.json') + dbConfig.load_sonic_global_db_config(global_db_file_path=filepath) + ns_input = ['', 'asic0', 'asic1', 'asic2'] + ns_list = list(dbConfig.get_ns_list()) + ns_input.sort() + ns_list.sort() + self.assertEqual(ns_input, ns_list) + + # This is the test to check if the global config file and get the correct DB in a namespace + def test__namespace_list(self): + import swsssdk + dbConfig = swsssdk.SonicDBConfig() + filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), './config', 'database_global.json') + dbConfig.load_sonic_global_db_config(global_db_file_path=filepath) + for namespace in list(dbConfig.get_ns_list()): + self.assertEqual(dbConfig.get_dbid('PFC_WD_DB', namespace), 5) + self.assertEqual(dbConfig.get_dbid('APPL_DB', namespace), 0)