import glob import json import os import re import subprocess import yaml from natsort import natsorted from sonic_py_common.general import getstatusoutput_noshell_pipe from swsscommon.swsscommon import ConfigDBConnector, SonicV2Connector USR_SHARE_SONIC_PATH = "/usr/share/sonic" HOST_DEVICE_PATH = USR_SHARE_SONIC_PATH + "/device" CONTAINER_PLATFORM_PATH = USR_SHARE_SONIC_PATH + "/platform" MACHINE_CONF_PATH = "/host/machine.conf" SONIC_VERSION_YAML_PATH = "/etc/sonic/sonic_version.yml" # Port configuration file names PORT_CONFIG_FILE = "port_config.ini" PLATFORM_JSON_FILE = "platform.json" # HwSKU configuration file name HWSKU_JSON_FILE = 'hwsku.json' # Multi-NPU constants # TODO: Move Multi-ASIC-related functions and constants to a "multi_asic.py" module NPU_NAME_PREFIX = "asic" NAMESPACE_PATH_GLOB = "/run/netns/*" ASIC_CONF_FILENAME = "asic.conf" PLATFORM_ENV_CONF_FILENAME = "platform_env.conf" FRONTEND_ASIC_SUB_ROLE = "FrontEnd" BACKEND_ASIC_SUB_ROLE = "BackEnd" # Chassis STATE_DB keys CHASSIS_INFO_TABLE = 'CHASSIS_INFO|chassis {}' CHASSIS_INFO_CARD_NUM_FIELD = 'module_num' CHASSIS_INFO_SERIAL_FIELD = 'serial' CHASSIS_INFO_MODEL_FIELD = 'model' CHASSIS_INFO_REV_FIELD = 'revision' # Cacheable Objects sonic_ver_info = {} hw_info_dict = {} def get_localhost_info(field, config_db=None): try: # TODO: enforce caller to provide config_db explicitly and remove its default value if not config_db: config_db = ConfigDBConnector() config_db.connect() metadata = config_db.get_table('DEVICE_METADATA') if 'localhost' in metadata and field in metadata['localhost']: return metadata['localhost'][field] except Exception: pass return None def get_hostname(): return get_localhost_info('hostname') def get_machine_info(): """ Retreives data from the machine configuration file Returns: A dictionary containing the key/value pairs as found in the machine configuration file """ if not os.path.isfile(MACHINE_CONF_PATH): return None machine_vars = {} with open(MACHINE_CONF_PATH) as machine_conf_file: for line in machine_conf_file: tokens = line.split('=') if len(tokens) < 2: continue machine_vars[tokens[0]] = tokens[1].strip() return machine_vars def get_platform(**kwargs): """ Retrieve the device's platform identifier Args: config_db: a connected ConfigDBConector object. If explicit None provided, this function will not read ConfigDB. This is useful before SonicDBConfig initializing. If not provided, this function may implicitly ready ConfigDB. Otherwise, this function will use it to read ConfigDB Returns: A string containing the device's platform identifier """ # If we are running in a virtual switch Docker container, the environment # variable 'PLATFORM' will be defined and will contain the platform # identifier. platform_env = os.getenv("PLATFORM") if platform_env: return platform_env # If 'PLATFORM' env variable is not defined, we try to read the platform # identifier from machine.conf. This is critical for sonic-config-engine, # because it is responsible for populating this value in Config DB. machine_info = get_machine_info() if machine_info: if 'onie_platform' in machine_info: return machine_info['onie_platform'] elif 'aboot_platform' in machine_info: return machine_info['aboot_platform'] # If we fail to read from machine.conf, we may be running inside a Docker # container in SONiC, where the /host directory is not mounted. In this # case the value should already be populated in Config DB so we finally # try reading it from there. if 'config_db' in kwargs: config_db = kwargs['config_db'] if config_db is None: return None else: config_db = None return get_localhost_info('platform', config_db=config_db) def get_hwsku(): """ Retrieve the device's hardware SKU identifier Returns: A string containing the device's hardware SKU identifier """ return get_localhost_info('hwsku') def get_platform_and_hwsku(): """ Convenience function which retrieves both the device's platform identifier and hardware SKU identifier Returns: A tuple of two strings, the first containing the device's platform identifier, the second containing the device's hardware SKU identifier """ platform = get_platform() hwsku = get_hwsku() return (platform, hwsku) def get_asic_conf_file_path(): """ Retrieves the path to the ASIC configuration file on the device Returns: A string containing the path to the ASIC configuration file on success, None on failure """ def asic_conf_path_candidates(): yield os.path.join(CONTAINER_PLATFORM_PATH, ASIC_CONF_FILENAME) # Note: this function is critical for is_multi_asic() and SonicDBConfig initializing # No explicit reading ConfigDB platform = get_platform(config_db=None) if platform: yield os.path.join(HOST_DEVICE_PATH, platform, ASIC_CONF_FILENAME) for asic_conf_file_path in asic_conf_path_candidates(): if os.path.isfile(asic_conf_file_path): return asic_conf_file_path return None def get_platform_env_conf_file_path(): """ Retrieves the path to the PLATFORM ENV configuration file on the device Returns: A string containing the path to the PLATFORM ENV configuration file on success, None on failure """ platform_env_conf_path_candidates = [] platform_env_conf_path_candidates.append(os.path.join(CONTAINER_PLATFORM_PATH, PLATFORM_ENV_CONF_FILENAME)) platform = get_platform() if platform: platform_env_conf_path_candidates.append(os.path.join(HOST_DEVICE_PATH, platform, PLATFORM_ENV_CONF_FILENAME)) for platform_env_conf_file_path in platform_env_conf_path_candidates: if os.path.isfile(platform_env_conf_file_path): return platform_env_conf_file_path return None def get_path_to_platform_dir(): """ Retreives the paths to the device's platform directory Returns: A string containing the path to the platform directory of the device """ # Get platform platform = get_platform() # Determine whether we're running in a container or on the host platform_path_host = os.path.join(HOST_DEVICE_PATH, platform) if os.path.isdir(CONTAINER_PLATFORM_PATH): platform_path = CONTAINER_PLATFORM_PATH elif os.path.isdir(platform_path_host): platform_path = platform_path_host else: raise OSError("Failed to locate platform directory") return platform_path def get_path_to_hwsku_dir(): """ Retreives the path to the device's hardware SKU data directory Returns: A string, containing the path to the hardware SKU directory of the device """ # Get Platform path first platform_path = get_path_to_platform_dir() # Get hwsku hwsku = get_hwsku() hwsku_path = os.path.join(platform_path, hwsku) return hwsku_path def get_paths_to_platform_and_hwsku_dirs(): """ Retreives the paths to the device's platform and hardware SKU data directories Returns: A tuple of two strings, the first containing the path to the platform directory of the device, the second containing the path to the hardware SKU directory of the device """ # Get Platform path first platform_path = get_path_to_platform_dir() # Get hwsku hwsku = get_hwsku() hwsku_path = os.path.join(platform_path, hwsku) return (platform_path, hwsku_path) def get_path_to_port_config_file(hwsku=None, asic=None): """ Retrieves the path to the device's port configuration file Args: hwsku: a string, it is allowed to be passed in args because when loading the initial configuration on the device, the HwSKU is not yet present in ConfigDB. asic: a string , asic argument should be passed on multi-ASIC devices only, it should be omitted on single-ASIC platforms. Returns: A string containing the path the the device's port configuration file """ """ This platform check is performed to make sure we return a None in case of unit-tests within sonic-cfggen where platform is not expected to be present because tests are not run on actual Hardware/Container. TODO: refactor sonic-cfggen such that we can remove this check """ platform = get_platform() if not platform: return None if hwsku: platform_path = get_path_to_platform_dir() hwsku_path = os.path.join(platform_path, hwsku) else: (platform_path, hwsku_path) = get_paths_to_platform_and_hwsku_dirs() port_config_candidates = [] # Check for 'hwsku.json' file presence first hwsku_json_file = os.path.join(hwsku_path, HWSKU_JSON_FILE) # if 'hwsku.json' file is available, Check for 'platform.json' file presence, # if 'platform.json' is available, APPEND it. Otherwise, SKIP it. """ This length check for interfaces in platform.json is performed to make sure the cfggen does not fail if port configuration information is not present TODO: once platform.json has all the necessary port config information remove this check """ if os.path.isfile(hwsku_json_file): if os.path.isfile(os.path.join(platform_path, PLATFORM_JSON_FILE)): json_file = os.path.join(platform_path, PLATFORM_JSON_FILE) platform_data = json.loads(open(json_file).read()) interfaces = platform_data.get('interfaces', None) if interfaces is not None and len(interfaces) > 0: port_config_candidates.append(os.path.join(platform_path, PLATFORM_JSON_FILE)) # Check for 'port_config.ini' file presence in a few locations if asic: port_config_candidates.append(os.path.join(hwsku_path, asic, PORT_CONFIG_FILE)) else: port_config_candidates.append(os.path.join(hwsku_path, PORT_CONFIG_FILE)) for candidate in port_config_candidates: if os.path.isfile(candidate): return candidate return None def get_sonic_version_info(): if not os.path.isfile(SONIC_VERSION_YAML_PATH): return None global sonic_ver_info if sonic_ver_info: return sonic_ver_info with open(SONIC_VERSION_YAML_PATH) as stream: if yaml.__version__ >= "5.1": sonic_ver_info = yaml.full_load(stream) else: sonic_ver_info = yaml.load(stream) return sonic_ver_info def get_sonic_version_file(): if not os.path.isfile(SONIC_VERSION_YAML_PATH): return None return SONIC_VERSION_YAML_PATH # Get hardware information def get_platform_info(config_db=None): """ This function is used to get the HW info helper function """ global hw_info_dict if hw_info_dict: return hw_info_dict from .multi_asic import get_num_asics version_info = get_sonic_version_info() hw_info_dict['platform'] = get_platform() hw_info_dict['hwsku'] = get_hwsku() if version_info: hw_info_dict['asic_type'] = version_info.get('asic_type') hw_info_dict['asic_count'] = get_num_asics() try: # TODO: enforce caller to provide config_db explicitly and remove its default value if not config_db: config_db = ConfigDBConnector() config_db.connect() metadata = config_db.get_table('DEVICE_METADATA')["localhost"] switch_type = metadata.get('switch_type') if switch_type: hw_info_dict['switch_type'] = switch_type except Exception: pass return hw_info_dict def get_chassis_info(): """ This function is used to get the Chassis serial / model / rev number """ chassis_info_dict = {} try: # Init statedb connection db = SonicV2Connector() db.connect(db.STATE_DB) table = CHASSIS_INFO_TABLE.format(1) chassis_info_dict['serial'] = db.get(db.STATE_DB, table, CHASSIS_INFO_SERIAL_FIELD) chassis_info_dict['model'] = db.get(db.STATE_DB, table, CHASSIS_INFO_MODEL_FIELD) chassis_info_dict['revision'] = db.get(db.STATE_DB, table, CHASSIS_INFO_REV_FIELD) except Exception: pass return chassis_info_dict def is_yang_config_validation_enabled(config_db): return get_localhost_info('yang_config_validation', config_db) == 'enable' # # Multi-NPU functionality # def get_num_npus(): asic_conf_file_path = get_asic_conf_file_path() if asic_conf_file_path is None: return 1 with open(asic_conf_file_path) as asic_conf_file: for line in asic_conf_file: tokens = line.split('=') if len(tokens) < 2: continue if tokens[0].lower() == 'num_asic': num_npus = tokens[1].strip() return int(num_npus) def is_multi_npu(): num_npus = get_num_npus() return (num_npus > 1) def is_voq_chassis(): switch_type = get_platform_info().get('switch_type') return True if switch_type and (switch_type == 'voq' or switch_type == 'fabric') else False def is_packet_chassis(): switch_type = get_platform_info().get('switch_type') return True if switch_type and switch_type == 'chassis-packet' else False def is_chassis(): return is_voq_chassis() or is_packet_chassis() def is_supervisor(): platform_env_conf_file_path = get_platform_env_conf_file_path() if platform_env_conf_file_path is None: return False with open(platform_env_conf_file_path) as platform_env_conf_file: for line in platform_env_conf_file: tokens = line.split('=') if len(tokens) < 2: continue if tokens[0].lower() == 'supervisor': val = tokens[1].strip() if val == '1': return True return False # Check if this platform has macsec capability. def is_macsec_supported(): supported = 0 platform_env_conf_file_path = get_platform_env_conf_file_path() # platform_env.conf file not present for platform if platform_env_conf_file_path is None: return supported # Else open the file check for keyword - macsec_enabled - with open(platform_env_conf_file_path) as platform_env_conf_file: for line in platform_env_conf_file: tokens = line.split('=') if len(tokens) < 2: continue if tokens[0].lower() == 'macsec_enabled': supported = tokens[1].strip() break return int(supported) def get_device_runtime_metadata(): chassis_metadata = {} if is_chassis(): chassis_metadata = {'CHASSIS_METADATA': {'module_type' : 'supervisor' if is_supervisor() else 'linecard', 'chassis_type': 'voq' if is_voq_chassis() else 'packet'}} port_metadata = {'ETHERNET_PORTS_PRESENT': True if get_path_to_port_config_file(hwsku=None, asic="0" if is_multi_npu() else None) else False} macsec_support_metadata = {'MACSEC_SUPPORTED': True if is_macsec_supported() else False} runtime_metadata = {} runtime_metadata.update(chassis_metadata) runtime_metadata.update(port_metadata) runtime_metadata.update(macsec_support_metadata) return {'DEVICE_RUNTIME_METADATA': runtime_metadata } def get_npu_id_from_name(npu_name): if npu_name.startswith(NPU_NAME_PREFIX): return npu_name[len(NPU_NAME_PREFIX):] else: return None def get_namespaces(): """ In a multi NPU platform, each NPU is in a Linux Namespace. This method returns list of all the Namespace present on the device """ ns_list = [] for path in glob.glob(NAMESPACE_PATH_GLOB): ns = os.path.basename(path) ns_list.append(ns) return natsorted(ns_list) def get_all_namespaces(config_db=None): """ 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. """ front_ns = [] back_ns = [] num_npus = get_num_npus() if is_multi_npu(): for npu in range(num_npus): namespace = "{}{}".format(NPU_NAME_PREFIX, npu) # TODO: enforce caller to provide config_db explicitly and remove its default value if not config_db: 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_ASIC_SUB_ROLE: front_ns.append(namespace) elif metadata['localhost']['sub_role'] == BACKEND_ASIC_SUB_ROLE: back_ns.append(namespace) return {'front_ns':front_ns, 'back_ns':back_ns} def _valid_mac_address(mac): return bool(re.match("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", mac)) def run_command(cmd): proc = subprocess.Popen(cmd, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (out, err) = proc.communicate() return (out, err) def run_command_pipe(cmd0, cmd1, cmd2): exitcodes, out = getstatusoutput_noshell_pipe(cmd0, cmd1, cmd2) if exitcodes == [0, 0, 0]: err = None else: err = out return (out, err) def get_system_mac(namespace=None): hw_mac_entry_outputs = [] syseeprom_cmd = ["sudo", "decode-syseeprom", "-m"] iplink_cmd0 = ["ip", 'link', 'show', 'eth0'] iplink_cmd1 = ['grep', 'ether'] iplink_cmd2 = ['awk', '{print $2}'] version_info = get_sonic_version_info() if (version_info['asic_type'] == 'mellanox'): # With Mellanox ONIE release(2019.05-5.2.0012) and above # "onie_base_mac" was added to /host/machine.conf: # onie_base_mac=e4:1d:2d:44:5e:80 # So we have another way to get the mac address besides decode syseeprom # By this can mitigate the dependency on the hw-management service base_mac_key = "onie_base_mac" machine_vars = get_machine_info() if machine_vars is not None and base_mac_key in machine_vars: mac = machine_vars[base_mac_key] mac = mac.strip() if _valid_mac_address(mac): return mac (mac, err) = run_command(syseeprom_cmd) hw_mac_entry_outputs.append((mac, err)) elif (version_info['asic_type'] == 'marvell'): # Try valid mac in eeprom, else fetch it from eth0 platform = get_platform() machine_key = "onie_machine" machine_vars = get_machine_info() (mac, err) = run_command(syseeprom_cmd) hw_mac_entry_outputs.append((mac, err)) if machine_vars is not None and machine_key in machine_vars: hwsku = machine_vars[machine_key] profile_cmd0 = ['cat', HOST_DEVICE_PATH + '/' + platform + '/' + hwsku + '/profile.ini'] profile_cmd1 = ['grep', 'switchMacAddress'] profile_cmd2 = ['cut', '-f2', '-d', '='] (mac, err) = run_command_pipe(profile_cmd0, profile_cmd1, profile_cmd2) else: profile_cmd = ["false"] (mac, err) = run_command(profile_cmd) hw_mac_entry_outputs.append((mac, err)) (mac, err) = run_command_pipe(iplink_cmd0, iplink_cmd1, iplink_cmd2) hw_mac_entry_outputs.append((mac, err)) elif (version_info['asic_type'] == 'cisco-8000'): # Try to get valid MAC from profile.ini first, else fetch it from syseeprom or eth0 platform = get_platform() if namespace is not None: profile_cmd0 = ['cat', HOST_DEVICE_PATH + '/' + platform + '/profile.ini'] profile_cmd1 = ['grep', str(namespace)+'switchMacAddress'] profile_cmd2 = ['cut', '-f2', '-d', '='] (mac, err) = run_command_pipe(profile_cmd0, profile_cmd1, profile_cmd2) else: profile_cmd = ["false"] (mac, err) = run_command(profile_cmd) hw_mac_entry_outputs.append((mac, err)) (mac, err) = run_command(syseeprom_cmd) hw_mac_entry_outputs.append((mac, err)) (mac, err) = run_command_pipe(iplink_cmd0, iplink_cmd1, iplink_cmd2) hw_mac_entry_outputs.append((mac, err)) else: mac_address_cmd = ["cat", "/sys/class/net/eth0/address"] if namespace is not None: mac_address_cmd = ['sudo', 'ip', 'netns', 'exec', str(namespace)] + mac_address_cmd (mac, err) = run_command(mac_address_cmd) hw_mac_entry_outputs.append((mac, err)) for (mac, err) in hw_mac_entry_outputs: if err: continue mac = mac.strip() if _valid_mac_address(mac): break if not _valid_mac_address(mac): return None # Align last byte of MAC if necessary if version_info and version_info['asic_type'] == 'centec': last_byte = mac[-2:] aligned_last_byte = format(int(int(last_byte, 16) + 1), '02x') mac = mac[:-2] + aligned_last_byte return mac def get_system_routing_stack(): """ Retrieves the routing stack being utilized on this device Returns: A string containing the name of the routing stack in use on the device """ cmd0 = ['sudo', 'docker', 'ps'] cmd1 = ['grep', 'bgp'] cmd2 = ['awk', '{print$2}'] cmd3 = ['cut', '-d', '-', '-f3'] cmd4 = ['cut', '-d', ':', '-f1'] try: _, result = getstatusoutput_noshell_pipe(cmd0, cmd1, cmd2, cmd3, cmd4) except OSError as e: raise OSError("Cannot detect routing stack") return result # Check if System warm reboot or Container warm restart is enabled. def is_warm_restart_enabled(container_name): state_db = SonicV2Connector(host='127.0.0.1') state_db.connect(state_db.STATE_DB, False) TABLE_NAME_SEPARATOR = '|' prefix = 'WARM_RESTART_ENABLE_TABLE' + TABLE_NAME_SEPARATOR # Get the system warm reboot enable state _hash = '{}{}'.format(prefix, 'system') wr_system_state = state_db.get(state_db.STATE_DB, _hash, "enable") wr_enable_state = True if wr_system_state == "true" else False # Get the container warm reboot enable state _hash = '{}{}'.format(prefix, container_name) wr_container_state = state_db.get(state_db.STATE_DB, _hash, "enable") wr_enable_state |= True if wr_container_state == "true" else False state_db.close(state_db.STATE_DB) return wr_enable_state # Check if System fast reboot is enabled. def is_fast_reboot_enabled(): fb_system_state = 0 cmd = ['sonic-db-cli', 'STATE_DB', 'get', "FAST_REBOOT|system"] proc = subprocess.Popen(cmd, universal_newlines=True, stdout=subprocess.PIPE) (stdout, stderr) = proc.communicate() if proc.returncode != 0: log.log_error("Error running command '{}'".format(cmd)) elif stdout: fb_system_state = stdout.rstrip('\n') return fb_system_state