diff --git a/config/main.py b/config/main.py index 0d9d134b19..c54ce39940 100755 --- a/config/main.py +++ b/config/main.py @@ -2030,6 +2030,48 @@ def platform(): if asic_type == 'mellanox': platform.add_command(mlnx.mlnx) +# 'firmware' subgroup ("config platform firmware ...") +@platform.group() +def firmware(): + """Firmware configuration tasks""" + pass + +# 'install' subcommand ("config platform firmware install") +@firmware.command( + context_settings=dict( + ignore_unknown_options=True, + allow_extra_args=True + ), + add_help_option=False +) +@click.argument('args', nargs=-1, type=click.UNPROCESSED) +def install(args): + """Install platform firmware""" + cmd = "fwutil install {}".format(" ".join(args)) + + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) + +# 'update' subcommand ("config platform firmware update") +@firmware.command( + context_settings=dict( + ignore_unknown_options=True, + allow_extra_args=True + ), + add_help_option=False +) +@click.argument('args', nargs=-1, type=click.UNPROCESSED) +def update(args): + """Update platform firmware""" + cmd = "fwutil update {}".format(" ".join(args)) + + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) + # # 'watermark' group ("show watermark telemetry interval") # diff --git a/data/etc/bash_completion.d/fwutil b/data/etc/bash_completion.d/fwutil new file mode 100644 index 0000000000..60ec589a6a --- /dev/null +++ b/data/etc/bash_completion.d/fwutil @@ -0,0 +1,8 @@ +_fwutil_completion() { + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ + COMP_CWORD=$COMP_CWORD \ + _FWUTIL_COMPLETE=complete $1 ) ) + return 0 +} + +complete -F _fwutil_completion -o default fwutil; diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 472eaa00d4..b91d4b9aec 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -73,6 +73,9 @@ * [NTP](#ntp) * [NTP show commands](#ntp-show-commands) * [NTP config commands](#ntp-config-commands) +* [Platform Component Firmware](#platform-component-firmware) + * [Platform Component Firmware show commands](#platform-component-firmware-show-commands) + * [Platform Component Firmware config commands](#platform-component-firmware-config-commands) * [Platform Specific Commands](#platform-specific-commands) * [PortChannels](#portchannels) * [PortChannel Show commands](#portchannel-show-commands) @@ -3788,6 +3791,172 @@ This command is used to delete a configured NTP server IP address. Go Back To [Beginning of the document](#) or [Beginning of this section](#NTP) +## Platform Component Firmware + +### Platform Component Firmware show commands + +**show platform firmware** + +This command displays platform components firmware status information. + +- Usage: +```bash +show platform firmware +``` + +- Example: +```bash +root@sonic:/home/admin# show platform firmware +Chassis Module Component Version Description +--------- -------- ----------- ----------------------- --------------------------------------- +Chassis1 N/A BIOS 0ACLH004_02.02.007_9600 BIOS - Basic Input/Output System + CPLD 5.3.3.1 CPLD - includes all CPLDs in the switch +``` + +### Platform Component Firmware config commands + +**config platform firmware install** + +This command is used to install a platform component firmware. +Both modular and non modular chassis platforms are supported. + +- Usage: +```bash +config platform firmware install chassis component fw [-y|--yes] +config platform firmware install module component fw [-y|--yes] +``` + +- Example: +```bash +root@sonic:/home/admin# config platform firmware install chassis component BIOS fw /etc/mlnx/fw/sn3800/chassis1/bios.bin +New firmware will be installed, continue? [y/N]: y +Installing firmware: + /etc/mlnx/fw/sn3800/chassis1/bios.bin + +root@sonic:/home/admin# config platform firmware install module Module1 component BIOS fw http://mellanox.com/fw/sn3800/module1/bios.bin +New firmware will be installed, continue? [y/N]: y +Downloading firmware: + [##################################################] 100% +Installing firmware: + /tmp/bios.bin +``` + +Supported options: +1. -y|--yes - automatic yes to prompts. Assume "yes" as answer to all prompts and run non-interactively + +**config platform firmware update** + +This command is used for automatic FW update of all available platform components. +Both modular and non modular chassis platforms are supported. + +Automatic FW update requires `platform_components.json` to be created and placed at: +sonic-buildimage/device///platform_components.json + +Example: +1. Non modular chassis platform +```json +{ + "chassis": { + "Chassis1": { + "component": { + "BIOS": { + "firmware": "/etc//fw//chassis1/bios.bin", + "version": "0ACLH003_02.02.010", + "info": "Cold reboot is required" + }, + "CPLD": { + "firmware": "/etc//fw//chassis1/cpld.bin", + "version": "10", + "info": "Power cycle is required" + }, + "FPGA": { + "firmware": "/etc//fw//chassis1/fpga.bin", + "version": "5", + "info": "Power cycle is required" + } + } + } + } +} +``` + +2. Modular chassis platform +```json +{ + "chassis": { + "Chassis1": { + "component": { + "BIOS": { + "firmware": "/etc//fw//chassis1/bios.bin", + "version": "0ACLH003_02.02.010", + "info": "Cold reboot is required" + }, + "CPLD": { + "firmware": "/etc//fw//chassis1/cpld.bin", + "version": "10", + "info": "Power cycle is required" + }, + "FPGA": { + "firmware": "/etc//fw//chassis1/fpga.bin", + "version": "5", + "info": "Power cycle is required" + } + } + } + }, + "module": { + "Module1": { + "component": { + "CPLD": { + "firmware": "/etc//fw//module1/cpld.bin", + "version": "10", + "info": "Power cycle is required" + }, + "FPGA": { + "firmware": "/etc//fw//module1/fpga.bin", + "version": "5", + "info": "Power cycle is required" + } + } + } + } +} +``` + +Note: FW update will be skipped if component definition is not provided (e.g., 'BIOS': { }) + +- Usage: +```bash +config platform firmware update [-y|--yes] [-f|--force] [-i|--image=current|next] +``` + +- Example: +```bash +root@sonic:/home/admin# config platform firmware update +Chassis Module Component Firmware Version Status Info +--------- -------- ----------- ------------------------------------- ------------------------------------------------- ------------------ ----------------------- +Chassis1 N/A BIOS /etc/mlnx/fw/sn3800/chassis1/bios.bin 0ACLH004_02.02.007_9600 / 0ACLH004_02.02.007_9600 up-to-date Cold reboot is required + CPLD /etc/mlnx/fw/sn3800/chassis1/cpld.bin 5.3.3.1 / 5.3.3.2 update is required Power cycle is required +New firmware will be installed, continue? [y/N]: y + +Summary: + +Chassis Module Component Status +--------- -------- ----------- ---------- +Chassis1 N/A BIOS up-to-date + CPLD success +``` + +Supported options: +1. -y|--yes - automatic yes to prompts. Assume "yes" as answer to all prompts and run non-interactively +2. -f|--force - install FW regardless the current version +3. -i|--image - update FW using current/next SONiC image + +Note: the default option is --image=current + +Go Back To [Beginning of the document](#) or [Beginning of this section](#platform-component-firmware) + + ## Platform Specific Commands There are few commands that are platform specific. Mellanox has used this feature and implemented Mellanox specific commands as follows. diff --git a/fwutil/__init__.py b/fwutil/__init__.py new file mode 100755 index 0000000000..6ed6f1a885 --- /dev/null +++ b/fwutil/__init__.py @@ -0,0 +1,5 @@ +try: + from sonic_platform.platform import Platform + from . import main +except ImportError as e: + raise ImportError("Required module not found: {}".format(str(e))) diff --git a/fwutil/lib.py b/fwutil/lib.py new file mode 100755 index 0000000000..3f6ecba4f3 --- /dev/null +++ b/fwutil/lib.py @@ -0,0 +1,827 @@ +#!/usr/bin/env python +# +# lib.py +# +# Core library for command-line interface for interacting with platform components within SONiC +# + +try: + import click + import os + import json + import urllib + import subprocess + import sonic_device_util + from collections import OrderedDict + from urlparse import urlparse + from tabulate import tabulate + from log import LogHelper + from . import Platform +except ImportError as e: + raise ImportError("Required module not found: {}".format(str(e))) + +# ========================= Constants ========================================== + +TAB = " " +EMPTY = "" +NA = "N/A" +NEWLINE = "\n" + +# ========================= Variables ========================================== + +log_helper = LogHelper() + +# ========================= Helper classes ===================================== + +class URL(object): + """ + URL + """ + HTTP_PREFIX = [ "http://", "https://" ] + HTTP_CODE_BASE = 100 + HTTP_4XX_CLIENT_ERRORS = 4 + + PB_LABEL = " " + PB_INFO_SEPARATOR = " | " + PB_FULL_TERMINAL_WIDTH = 0 + + TMP_PATH = "/tmp" + + def __init__(self, url): + self.__url = url + self.__pb = None + self.__bytes_num = 0 + + def __str__(self): + return self.__url + + def __reporthook(self, count, block_size, total_size): + if self.__pb is None: + self.__pb = click.progressbar( + label=self.PB_LABEL, + length=total_size, + show_eta=True, + show_percent=True, + info_sep=self.PB_INFO_SEPARATOR, + width=self.PB_FULL_TERMINAL_WIDTH + ) + + self.__pb.update(count * block_size - self.__bytes_num) + self.__bytes_num = count * block_size + + def __pb_reset(self): + if self.__pb: + self.__pb.render_finish() + self.__pb = None + + self.__bytes_num = 0 + + def __validate(self): + # Check basic URL syntax + if not self.__url.startswith(tuple(self.HTTP_PREFIX)): + raise RuntimeError("URL is malformed: did not match expected prefix " + str(self.HTTP_PREFIX)) + + response_code = None + + # Check URL existence + try: + urlfile = urllib.urlopen(self.__url) + response_code = urlfile.getcode() + except IOError: + raise RuntimeError("Did not receive a response from remote machine") + + # Check for a 4xx response code which indicates a nonexistent URL + if response_code / self.HTTP_CODE_BASE == self.HTTP_4XX_CLIENT_ERRORS: + raise RuntimeError("Image file not found on remote machine") + + def get_url(self): + return self.__url + + def is_url(self): + if self.__url.startswith(tuple(self.HTTP_PREFIX)): + return True + + return False + + def retrieve(self): + filename, headers = None, None + + self.__validate() + + result = urlparse(self.__url) + basename = os.path.basename(result.path) + name, extension = os.path.splitext(basename) + + if not extension: + raise RuntimeError("Filename is malformed: did not find an extension") + + try: + filename, headers = urllib.urlretrieve( + self.__url, + "{}/{}".format(self.TMP_PATH, basename), + self.__reporthook + ) + finally: + self.__pb_reset() + + return filename, headers + + url = property(fget=get_url) + + +class PlatformDataProvider(object): + """ + PlatformDataProvider + """ + def __init__(self): + self.__platform = Platform() + self.__chassis = self.__platform.get_chassis() + + self.chassis_component_map = self.__get_chassis_component_map() + self.module_component_map = self.__get_module_component_map() + + def __get_chassis_component_map(self): + chassis_component_map = OrderedDict() + + chassis_name = self.__chassis.get_name() + chassis_component_map[chassis_name] = OrderedDict() + + component_list = self.chassis.get_all_components() + for component in component_list: + component_name = component.get_name() + chassis_component_map[chassis_name][component_name] = component + + return chassis_component_map + + def __get_module_component_map(self): + module_component_map = OrderedDict() + + module_list = self.__chassis.get_all_modules() + for module in module_list: + module_name = module.get_name() + module_component_map[module_name] = OrderedDict() + + component_list = module.get_all_components() + for component in component_list: + component_name = component.get_name() + module_component_map[module_name][component_name] = component + + return module_component_map + + def get_platform(self): + return self.__platform + + def get_chassis(self): + return self.__chassis + + def is_modular_chassis(self): + return len(self.module_component_map) > 0 + + def is_chassis_has_components(self): + return self.__chassis.get_num_components() > 0 + + platform = property(fget=get_platform) + chassis = property(fget=get_chassis) + + +class SquashFs(object): + """ + SquashFs + """ + OS_PREFIX = "SONiC-OS-" + + FS_PATH_TEMPLATE = "/host/image-{}/fs.squashfs" + FS_MOUNTPOINT_TEMPLATE = "/tmp/image-{}-fs" + + def __init__(self): + current_image = self.__get_current_image() + next_image = self.__get_next_image() + + if current_image == next_image: + raise RuntimeError("Next boot image is not set") + + image_stem = next_image.lstrip(self.OS_PREFIX) + + self.fs_path = self.FS_PATH_TEMPLATE.format(image_stem) + self.fs_mountpoint = self.FS_MOUNTPOINT_TEMPLATE.format(image_stem) + + def __get_current_image(self): + cmd = "sonic_installer list | grep 'Current: ' | cut -f2 -d' '" + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) + + return output.rstrip(NEWLINE) + + def __get_next_image(self): + cmd = "sonic_installer list | grep 'Next: ' | cut -f2 -d' '" + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) + + return output.rstrip(NEWLINE) + + def mount_next_image_fs(self): + if os.path.ismount(self.fs_mountpoint): + self.umount_next_image_fs() + + os.mkdir(self.fs_mountpoint) + cmd = "mount -t squashfs {} {}".format(self.fs_path, self.fs_mountpoint) + subprocess.check_call(cmd, shell=True) + + return self.fs_mountpoint + + def umount_next_image_fs(self): + if os.path.ismount(self.fs_mountpoint): + cmd = "umount -rf {}".format(self.fs_mountpoint) + subprocess.check_call(cmd, shell=True) + + if os.path.exists(self.fs_mountpoint): + os.rmdir(self.fs_mountpoint) + + +class PlatformComponentsParser(object): + """ + PlatformComponentsParser + """ + PLATFORM_COMPONENTS_FILE = "platform_components.json" + PLATFORM_COMPONENTS_PATH_TEMPLATE = "{}/usr/share/sonic/device/{}/{}" + + CHASSIS_KEY = "chassis" + MODULE_KEY = "module" + COMPONENT_KEY = "component" + FIRMWARE_KEY = "firmware" + VERSION_KEY = "version" + INFO_KEY = "info" + + UTF8_ENCODING = "utf-8" + + def __init__(self, is_modular_chassis): + self.__is_modular_chassis = is_modular_chassis + self.__chassis_component_map = OrderedDict() + self.__module_component_map = OrderedDict() + + def __get_platform_type(self): + return sonic_device_util.get_platform_info( + sonic_device_util.get_machine_info() + ) + + def __get_platform_components_path(self, root_path): + return self.PLATFORM_COMPONENTS_PATH_TEMPLATE.format( + root_path, + self.__get_platform_type(), + self.PLATFORM_COMPONENTS_FILE + ) + + def __is_str(self, obj): + return isinstance(obj, unicode) or isinstance(obj, str) + + def __is_dict(self, obj): + return isinstance(obj, dict) + + def __parser_fail(self, msg): + raise RuntimeError("Failed to parse \"{}\": {}".format(self.PLATFORM_COMPONENTS_FILE, msg)) + + def __parser_platform_fail(self, msg): + self.__parser_fail("invalid platform schema: {}".format(msg)) + + def __parser_chassis_fail(self, msg): + self.__parser_fail("invalid chassis schema: {}".format(msg)) + + def __parser_module_fail(self, msg): + self.__parser_fail("invalid module schema: {}".format(msg)) + + def __parser_component_fail(self, msg): + self.__parser_fail("invalid component schema: {}".format(msg)) + + def __parse_component_section(self, section, component, is_module_component=False): + if not self.__is_dict(component): + self.__parser_component_fail("dictionary is expected: key={}".format(self.COMPONENT_KEY)) + + if not component: + return + + missing_key = None + + for key1, value1 in component.items(): + if not self.__is_dict(value1): + self.__parser_component_fail("dictionary is expected: key={}".format(key1)) + + if not is_module_component: + self.__chassis_component_map[section][key1] = OrderedDict() + else: + self.__module_component_map[section][key1] = OrderedDict() + + if value1: + if len(value1) != 3: + self.__parser_component_fail("unexpected number of records: key={}".format(key1)) + + if self.FIRMWARE_KEY not in value1: + missing_key = self.FIRMWARE_KEY + break + elif self.VERSION_KEY not in value1: + missing_key = self.VERSION_KEY + break + elif self.INFO_KEY not in value1: + missing_key = self.INFO_KEY + break + + for key2, value2 in value1.items(): + if not self.__is_str(value2): + self.__parser_component_fail("string is expected: key={}".format(key2)) + + if not is_module_component: + self.__chassis_component_map[section][key1] = value1 + else: + self.__module_component_map[section][key1] = value1 + + if missing_key is not None: + self.__parser_component_fail("\"{}\" key hasn't been found".format(missing_key)) + + def __parse_chassis_section(self, chassis): + self.__chassis_component_map = OrderedDict() + + if not self.__is_dict(chassis): + self.__parser_chassis_fail("dictionary is expected: key={}".format(self.CHASSIS_KEY)) + + if not chassis: + self.__parser_chassis_fail("dictionary is empty: key={}".format(self.CHASSIS_KEY)) + + if len(chassis) != 1: + self.__parser_chassis_fail("unexpected number of records: key={}".format(self.CHASSIS_KEY)) + + for key, value in chassis.items(): + if not self.__is_dict(value): + self.__parser_chassis_fail("dictionary is expected: key={}".format(key)) + + if not value: + self.__parser_chassis_fail("dictionary is empty: key={}".format(key)) + + if self.COMPONENT_KEY not in value: + self.__parser_chassis_fail("\"{}\" key hasn't been found".format(self.COMPONENT_KEY)) + + if len(value) != 1: + self.__parser_chassis_fail("unexpected number of records: key={}".format(key)) + + self.__chassis_component_map[key] = OrderedDict() + self.__parse_component_section(key, value[self.COMPONENT_KEY]) + + def __parse_module_section(self, module): + self.__module_component_map = OrderedDict() + + if not self.__is_dict(module): + self.__parser_module_fail("dictionary is expected: key={}".format(self.MODULE_KEY)) + + if not module: + self.__parser_module_fail("dictionary is empty: key={}".format(self.MODULE_KEY)) + + for key, value in module.items(): + if not self.__is_dict(value): + self.__parser_module_fail("dictionary is expected: key={}".format(key)) + + if not value: + self.__parser_module_fail("dictionary is empty: key={}".format(key)) + + if self.COMPONENT_KEY not in value: + self.__parser_module_fail("\"{}\" key hasn't been found".format(self.COMPONENT_KEY)) + + if len(value) != 1: + self.__parser_module_fail("unexpected number of records: key={}".format(key)) + + self.__module_component_map[key] = OrderedDict() + self.__parse_component_section(key, value[self.COMPONENT_KEY], True) + + def __deunicodify_hook(self, pairs): + new_pairs = [ ] + + for key, value in pairs: + if isinstance(key, unicode): + key = key.encode(self.UTF8_ENCODING) + + if isinstance(value, unicode): + value = value.encode(self.UTF8_ENCODING) + + new_pairs.append((key, value)) + + return OrderedDict(new_pairs) + + def get_chassis_component_map(self): + return self.__chassis_component_map + + def get_module_component_map(self): + return self.__module_component_map + + def parse_platform_components(self, root_path=None): + platform_components_path = None + + if root_path is None: + platform_components_path = self.__get_platform_components_path(EMPTY) + else: + platform_components_path = self.__get_platform_components_path(root_path) + + with open(platform_components_path) as platform_components: + data = json.load(platform_components, object_pairs_hook=self.__deunicodify_hook) + + if not self.__is_dict(data): + self.__parser_platform_fail("dictionary is expected: key=root") + + if not data: + self.__parser_platform_fail("dictionary is empty: key=root") + + if self.CHASSIS_KEY not in data: + self.__parser_platform_fail("\"{}\" key hasn't been found".format(self.CHASSIS_KEY)) + + if not self.__is_modular_chassis: + if len(data) != 1: + self.__parser_platform_fail("unexpected number of records: key=root") + + self.__parse_chassis_section(data[self.CHASSIS_KEY]) + + if self.__is_modular_chassis: + if self.MODULE_KEY not in data: + self.__parser_platform_fail("\"{}\" key hasn't been found".format(self.MODULE_KEY)) + + if len(data) != 2: + self.__parser_platform_fail("unexpected number of records: key=root") + + self.__parse_module_section(data[self.MODULE_KEY]) + + chassis_component_map = property(fget=get_chassis_component_map) + module_component_map = property(fget=get_module_component_map) + + +class ComponentUpdateProvider(PlatformDataProvider): + """ + ComponentUpdateProvider + """ + STATUS_HEADER = [ "Chassis", "Module", "Component", "Firmware", "Version", "Status", "Info" ] + RESULT_HEADER = [ "Chassis", "Module", "Component", "Status" ] + FORMAT = "simple" + + FW_STATUS_UPDATE_SUCCESS = "success" + FW_STATUS_UPDATE_FAILURE = "failure" + FW_STATUS_UPDATE_REQUIRED = "update is required" + FW_STATUS_UP_TO_DATE = "up-to-date" + + SECTION_CHASSIS = "Chassis" + SECTION_MODULE = "Module" + + def __init__(self, root_path=None): + PlatformDataProvider.__init__(self) + + self.__root_path = root_path + + self.__pcp = PlatformComponentsParser(self.is_modular_chassis()) + self.__pcp.parse_platform_components(root_path) + + self.__validate_platform_schema(self.__pcp) + + def __diff_keys(self, keys1, keys2): + return set(keys1) ^ set(keys2) + + def __validate_component_map(self, section, pdp_map, pcp_map): + diff_keys = self.__diff_keys(pdp_map.keys(), pcp_map.keys()) + + if diff_keys: + raise RuntimeError( + "{} names mismatch: keys={}".format( + section, + str(list(diff_keys)) + ) + ) + + for key in pdp_map.keys(): + diff_keys = self.__diff_keys(pdp_map[key].keys(), pcp_map[key].keys()) + + if diff_keys: + raise RuntimeError( + "{} component names mismatch: keys={}".format( + section, + str(list(diff_keys)) + ) + ) + + def __validate_platform_schema(self, pcp): + self.__validate_component_map( + self.SECTION_CHASSIS, + self.chassis_component_map, + pcp.chassis_component_map + ) + + self.__validate_component_map( + self.SECTION_MODULE, + self.module_component_map, + pcp.module_component_map + ) + + def get_status(self, force): + status_table = [ ] + + append_chassis_name = self.is_chassis_has_components() + append_module_na = not self.is_modular_chassis() + + for chassis_name, chassis_component_map in self.chassis_component_map.items(): + for chassis_component_name, chassis_component in chassis_component_map.items(): + component = self.__pcp.chassis_component_map[chassis_name][chassis_component_name] + + firmware_path = NA + firmware_version_current = chassis_component.get_firmware_version() + firmware_version_available = NA + firmware_version = firmware_version_current + + status = self.FW_STATUS_UP_TO_DATE + info = NA + + if append_chassis_name: + append_chassis_name = False + else: + chassis_name = EMPTY + + if append_module_na: + module_name = NA + append_module_na = False + else: + module_name = EMPTY + + if component: + firmware_path = component[self.__pcp.FIRMWARE_KEY] + firmware_version_available = component[self.__pcp.VERSION_KEY] + firmware_version = "{} / {}".format(firmware_version_current, firmware_version_available) + info = component[self.__pcp.INFO_KEY] + + if self.__root_path is not None: + firmware_path = self.__root_path + firmware_path + + if force or firmware_version_current != firmware_version_available: + status = self.FW_STATUS_UPDATE_REQUIRED + + status_table.append( + [ + chassis_name, + module_name, + chassis_component_name, + firmware_path, + firmware_version, + status, + info + ] + ) + + append_chassis_name = not self.is_chassis_has_components() + + if self.is_modular_chassis(): + for module_name, module_component_map in self.module_component_map.items(): + append_module_name = True + for module_component_name, module_component in module_component_map.items(): + component = self.__pcp.module_component_map[module_name][module_component_name] + + firmware_path = NA + firmware_version_current = module_component.get_firmware_version() + firmware_version_available = NA + firmware_version = firmware_version_current + + status = self.FW_STATUS_UP_TO_DATE + info = NA + + if append_chassis_name: + chassis_name = self.chassis.get_name() + append_chassis_name = False + else: + chassis_name = EMPTY + + if append_module_name: + append_module_name = False + else: + module_name = EMPTY + + if component: + firmware_path = component[self.__pcp.FIRMWARE_KEY] + firmware_version_available = component[self.__pcp.VERSION_KEY] + firmware_version = "{} / {}".format(firmware_version_current, firmware_version_available) + info = component[self.__pcp.INFO_KEY] + + if self.__root_path is not None: + firmware_path = self.__root_path + firmware_path + + if force or firmware_version_current != firmware_version_available: + status = self.FW_STATUS_UPDATE_REQUIRED + + status_table.append( + [ + chassis_name, + module_name, + module_component_name, + firmware_path, + firmware_version, + status, + info + ] + ) + + return tabulate(status_table, self.STATUS_HEADER, tablefmt=self.FORMAT) + + def update_firmware(self, force): + status_table = [ ] + + append_chassis_name = self.is_chassis_has_components() + append_module_na = not self.is_modular_chassis() + + for chassis_name, chassis_component_map in self.chassis_component_map.items(): + for chassis_component_name, chassis_component in chassis_component_map.items(): + component = self.__pcp.chassis_component_map[chassis_name][chassis_component_name] + component_path = "{}/{}".format( + chassis_name, + chassis_component_name + ) + + firmware_version_current = chassis_component.get_firmware_version() + firmware_version_available = NA + + status = self.FW_STATUS_UP_TO_DATE + + if append_chassis_name: + append_chassis_name = False + else: + chassis_name = EMPTY + + if append_module_na: + module_name = NA + append_module_na = False + else: + module_name = EMPTY + + if component: + firmware_path = component[self.__pcp.FIRMWARE_KEY] + firmware_version_available = component[self.__pcp.VERSION_KEY] + + if self.__root_path is not None: + firmware_path = self.__root_path + firmware_path + + if force or firmware_version_current != firmware_version_available: + result = False + + try: + click.echo("Installing firmware:") + click.echo(TAB + firmware_path) + + log_helper.log_fw_install_start(component_path, firmware_path) + + if not os.path.exists(firmware_path): + raise RuntimeError("Path \"{}\" does not exist".format(firmware_path)) + + result = chassis_component.install_firmware(firmware_path) + log_helper.log_fw_install_end(component_path, firmware_path, result) + except Exception as e: + log_helper.log_fw_install_end(component_path, firmware_path, False, e) + log_helper.print_error(str(e)) + + status = self.FW_STATUS_UPDATE_SUCCESS if result else self.FW_STATUS_UPDATE_FAILURE + + status_table.append( + [ + chassis_name, + module_name, + chassis_component_name, + status, + ] + ) + + append_chassis_name = not self.is_chassis_has_components() + + if self.is_modular_chassis(): + for module_name, module_component_map in self.module_component_map.items(): + append_module_name = True + + for module_component_name, module_component in module_component_map.items(): + component = self.__pcp.module_component_map[module_name][module_component_name] + component_path = "{}/{}/{}".format( + self.chassis.get_name(), + module_name, + module_component_name + ) + + firmware_version_current = module_component.get_firmware_version() + firmware_version_available = NA + + status = self.FW_STATUS_UP_TO_DATE + + if append_chassis_name: + chassis_name = self.chassis.get_name() + append_chassis_name = False + else: + chassis_name = EMPTY + + if append_module_name: + append_module_name = False + else: + module_name = EMPTY + + if component: + firmware_path = component[self.__pcp.FIRMWARE_KEY] + firmware_version_available = component[self.__pcp.VERSION_KEY] + + if self.__root_path is not None: + firmware_path = self.__root_path + firmware_path + + if force or firmware_version_current != firmware_version_available: + result = False + + try: + click.echo("Installing firmware:") + click.echo(TAB + firmware_path) + + log_helper.log_fw_install_start(component_path, firmware_path) + + if not os.path.exists(firmware_path): + raise RuntimeError("Path \"{}\" does not exist".format(firmware_path)) + + result = module_component.install_firmware(firmware_path) + log_helper.log_fw_install_end(component_path, firmware_path, result) + except Exception as e: + log_helper.log_fw_install_end(component_path, firmware_path, False, e) + log_helper.print_error(str(e)) + + status = self.FW_STATUS_UPDATE_SUCCESS if result else self.FW_STATUS_UPDATE_FAILURE + + status_table.append( + [ + chassis_name, + module_name, + module_component_name, + status, + ] + ) + + return tabulate(status_table, self.RESULT_HEADER, tablefmt=self.FORMAT) + + +class ComponentStatusProvider(PlatformDataProvider): + """ + ComponentStatusProvider + """ + HEADER = [ "Chassis", "Module", "Component", "Version", "Description" ] + FORMAT = "simple" + + def __init__(self): + PlatformDataProvider.__init__(self) + + def get_status(self): + status_table = [ ] + + append_chassis_name = self.is_chassis_has_components() + append_module_na = not self.is_modular_chassis() + + for chassis_name, chassis_component_map in self.chassis_component_map.items(): + for chassis_component_name, chassis_component in chassis_component_map.items(): + firmware_version = chassis_component.get_firmware_version() + description = chassis_component.get_description() + + if append_chassis_name: + append_chassis_name = False + else: + chassis_name = EMPTY + + if append_module_na: + module_name = NA + append_module_na = False + else: + module_name = EMPTY + + status_table.append( + [ + chassis_name, + module_name, + chassis_component_name, + firmware_version, + description + ] + ) + + append_chassis_name = not self.is_chassis_has_components() + + if self.is_modular_chassis(): + for module_name, module_component_map in self.module_component_map.items(): + append_module_name = True + + for module_component_name, module_component in module_component_map.items(): + firmware_version = module_component.get_firmware_version() + description = module_component.get_description() + + if append_chassis_name: + chassis_name = self.chassis.get_name() + append_chassis_name = False + else: + chassis_name = EMPTY + + if append_module_name: + append_module_name = False + else: + module_name = EMPTY + + status_table.append( + [ + chassis_name, + module_name, + module_component_name, + firmware_version, + description + ] + ) + + return tabulate(status_table, self.HEADER, tablefmt=self.FORMAT) diff --git a/fwutil/log.py b/fwutil/log.py new file mode 100755 index 0000000000..0580e4bb27 --- /dev/null +++ b/fwutil/log.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# +# log.py +# +# Logging library for command-line interface for interacting with platform components within SONiC +# + +try: + import click + import syslog +except ImportError as e: + raise ImportError("Required module not found: {}".format(str(e))) + +# ========================= Constants ========================================== + +SYSLOG_IDENTIFIER = "fwutil" + +# ========================= Helper classes ===================================== + +class SyslogLogger(object): + """ + SyslogLogger + """ + def __init__(self, identifier): + self.__syslog = syslog + + self.__syslog.openlog( + ident=identifier, + logoption=self.__syslog.LOG_NDELAY, + facility=self.__syslog.LOG_USER + ) + + def __del__(self): + self.__syslog.closelog() + + def log_error(self, msg): + self.__syslog.syslog(self.__syslog.LOG_ERR, msg) + + def log_warning(self, msg): + self.__syslog.syslog(self.__syslog.LOG_WARNING, msg) + + def log_notice(self, msg): + self.__syslog.syslog(self.__syslog.LOG_NOTICE, msg) + + def log_info(self, msg): + self.__syslog.syslog(self.__syslog.LOG_INFO, msg) + + def log_debug(self, msg): + self.__syslog.syslog(self.__syslog.LOG_DEBUG, msg) + + +logger = SyslogLogger(SYSLOG_IDENTIFIER) + + +class LogHelper(object): + """ + LogHelper + """ + FW_ACTION_DOWNLOAD = "download" + FW_ACTION_INSTALL = "install" + + STATUS_SUCCESS = "success" + STATUS_FAILURE = "failure" + + def __log_fw_action_start(self, action, component, firmware): + caption = "Firmware {} started".format(action) + template = "{}: component={}, firmware={}" + + logger.log_info( + template.format( + caption, + component, + firmware + ) + ) + + def __log_fw_action_end(self, action, component, firmware, status, exception=None): + caption = "Firmware {} ended".format(action) + + status_template = "{}: component={}, firmware={}, status={}" + exception_template = "{}: component={}, firmware={}, status={}, exception={}" + + if status: + logger.log_info( + status_template.format( + caption, + component, + firmware, + self.STATUS_SUCCESS + ) + ) + else: + if exception is None: + logger.log_error( + status_template.format( + caption, + component, + firmware, + self.STATUS_FAILURE + ) + ) + else: + logger.log_error( + exception_template.format( + caption, + component, + firmware, + self.STATUS_FAILURE, + str(exception) + ) + ) + + def log_fw_download_start(self, component, firmware): + self.__log_fw_action_start(self.FW_ACTION_DOWNLOAD, component, firmware) + + def log_fw_download_end(self, component, firmware, status, exception=None): + self.__log_fw_action_end(self.FW_ACTION_DOWNLOAD, component, firmware, status, exception) + + def log_fw_install_start(self, component, firmware): + self.__log_fw_action_start(self.FW_ACTION_INSTALL, component, firmware) + + def log_fw_install_end(self, component, firmware, status, exception=None): + self.__log_fw_action_end(self.FW_ACTION_INSTALL, component, firmware, status, exception) + + def print_error(self, msg): + click.echo("Error: {}.".format(msg)) diff --git a/fwutil/main.py b/fwutil/main.py new file mode 100755 index 0000000000..c1443627c1 --- /dev/null +++ b/fwutil/main.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +# +# main.py +# +# Command-line utility for interacting with platform components within SONiC +# + +try: + import click + import os + from lib import PlatformDataProvider, ComponentStatusProvider, ComponentUpdateProvider + from lib import URL, SquashFs + from log import LogHelper +except ImportError as e: + raise ImportError("Required module not found: {}".format(str(e))) + +# ========================= Constants ========================================== + +VERSION = '1.0.0.0' + +CHASSIS_NAME_CTX_KEY = "chassis_name" +MODULE_NAME_CTX_KEY = "module_name" +COMPONENT_CTX_KEY = "component" +COMPONENT_PATH_CTX_KEY = "component_path" +URL_CTX_KEY = "url" + +TAB = " " +PATH_SEPARATOR = "/" +IMAGE_NEXT = "next" +HELP = "?" + +EXIT_SUCCESS = 0 +EXIT_FAILURE = 1 + +ROOT_UID = 0 + +# ========================= Variables ========================================== + +pdp = PlatformDataProvider() +log_helper = LogHelper() + +# ========================= Helper functions =================================== + +def cli_show_help(ctx): + click.echo(ctx.get_help()) + ctx.exit(EXIT_SUCCESS) + + +def cli_abort(ctx, msg): + click.echo("Error: " + msg + ". Aborting...") + ctx.abort() + + +def cli_init(ctx): + if os.geteuid() != ROOT_UID: + cli_abort(ctx, "Root privileges are required") + + ctx.ensure_object(dict) + +# ========================= CLI commands and groups ============================ + +# 'fwutil' command main entrypoint +@click.group() +@click.pass_context +def cli(ctx): + """fwutil - Command-line utility for interacting with platform components""" + + cli_init(ctx) + + +# 'install' group +@cli.group() +@click.pass_context +def install(ctx): + """Install platform firmware""" + ctx.obj[COMPONENT_PATH_CTX_KEY] = [ ] + + +# 'chassis' subgroup +@click.group() +@click.pass_context +def chassis(ctx): + """Install chassis firmware""" + ctx.obj[CHASSIS_NAME_CTX_KEY] = pdp.chassis.get_name() + ctx.obj[COMPONENT_PATH_CTX_KEY].append(pdp.chassis.get_name()) + + +def validate_module(ctx, param, value): + if value == HELP: + cli_show_help(ctx) + + if not pdp.is_modular_chassis(): + ctx.fail("Unsupported platform: non modular chassis.") + + if value not in pdp.module_component_map: + ctx.fail("Invalid value for \"{}\": Module \"{}\" does not exist.".format(param.metavar, value)) + + return value + + +# 'module' subgroup +@click.group() +@click.argument('module_name', metavar='', callback=validate_module) +@click.pass_context +def module(ctx, module_name): + """Install module firmware""" + ctx.obj[MODULE_NAME_CTX_KEY] = module_name + ctx.obj[COMPONENT_PATH_CTX_KEY].append(pdp.chassis.get_name()) + ctx.obj[COMPONENT_PATH_CTX_KEY].append(module_name) + + +def validate_component(ctx, param, value): + if value == HELP: + cli_show_help(ctx) + + if CHASSIS_NAME_CTX_KEY in ctx.obj: + chassis_name = ctx.obj[CHASSIS_NAME_CTX_KEY] + if value in pdp.chassis_component_map[chassis_name]: + ctx.obj[COMPONENT_CTX_KEY] = pdp.chassis_component_map[chassis_name][value] + return value + + if MODULE_NAME_CTX_KEY in ctx.obj: + module_name = ctx.obj[MODULE_NAME_CTX_KEY] + if value in pdp.module_component_map[module_name]: + ctx.obj[COMPONENT_CTX_KEY] = pdp.module_component_map[module_name][value] + return value + + ctx.fail("Invalid value for \"{}\": Component \"{}\" does not exist.".format(param.metavar, value)) + + +# 'component' subgroup +@click.group() +@click.argument('component_name', metavar='', callback=validate_component) +@click.pass_context +def component(ctx, component_name): + """Install component firmware""" + ctx.obj[COMPONENT_PATH_CTX_KEY].append(component_name) + + +def install_fw(ctx, fw_path): + component = ctx.obj[COMPONENT_CTX_KEY] + component_path = PATH_SEPARATOR.join(ctx.obj[COMPONENT_PATH_CTX_KEY]) + + status = False + + try: + click.echo("Installing firmware:") + click.echo(TAB + fw_path) + log_helper.log_fw_install_start(component_path, fw_path) + status = component.install_firmware(fw_path) + log_helper.log_fw_install_end(component_path, fw_path, status) + except Exception as e: + log_helper.log_fw_install_end(component_path, fw_path, False, e) + cli_abort(ctx, str(e)) + + if not status: + log_helper.print_error("Firmware install failed") + ctx.exit(EXIT_FAILURE) + + +def download_fw(ctx, url): + filename, headers = None, None + + component_path = PATH_SEPARATOR.join(ctx.obj[COMPONENT_PATH_CTX_KEY]) + + try: + click.echo("Downloading firmware:") + log_helper.log_fw_download_start(component_path, str(url)) + filename, headers = url.retrieve() + log_helper.log_fw_download_end(component_path, str(url), True) + except Exception as e: + log_helper.log_fw_download_end(component_path, str(url), False, e) + cli_abort(ctx, str(e)) + + return filename + + +def validate_fw(ctx, param, value): + if value == HELP: + cli_show_help(ctx) + + url = URL(value) + + if not url.is_url(): + path = click.Path(exists=True) + path.convert(value, param, ctx) + else: + ctx.obj[URL_CTX_KEY] = url + + return value + + +# 'fw' subcommand +@component.command() +@click.option('-y', '--yes', 'yes', is_flag=True, show_default=True, help="Assume \"yes\" as answer to all prompts and run non-interactively") +@click.argument('fw_path', metavar='', callback=validate_fw) +@click.pass_context +def fw(ctx, yes, fw_path): + """Install firmware from local binary or URL""" + if not yes: + click.confirm("New firmware will be installed, continue?", abort=True) + + url = None + + if URL_CTX_KEY in ctx.obj: + url = ctx.obj[URL_CTX_KEY] + fw_path = download_fw(ctx, url) + + try: + install_fw(ctx, fw_path) + finally: + if url is not None and os.path.exists(fw_path): + os.remove(fw_path) + + +# 'update' subgroup +@cli.command() +@click.option('-y', '--yes', 'yes', is_flag=True, show_default=True, help="Assume \"yes\" as answer to all prompts and run non-interactively") +@click.option('-f', '--force', 'force', is_flag=True, show_default=True, help="Install firmware regardless the current version") +@click.option('-i', '--image', 'image', type=click.Choice(["current", "next"]), default="current", show_default=True, help="Update firmware using current/next image") +@click.pass_context +def update(ctx, yes, force, image): + """Update platform firmware""" + aborted = False + + try: + squashfs = None + + try: + cup = None + + if image == IMAGE_NEXT: + squashfs = SquashFs() + fs_path = squashfs.mount_next_image_fs() + cup = ComponentUpdateProvider(fs_path) + else: + cup = ComponentUpdateProvider() + + click.echo(cup.get_status(force)) + + if not yes: + click.confirm("New firmware will be installed, continue?", abort=True) + + result = cup.update_firmware(force) + + click.echo() + click.echo("Summary:") + click.echo() + + click.echo(result) + except click.Abort: + aborted = True + except Exception as e: + aborted = True + click.echo("Error: " + str(e) + ". Aborting...") + + if image == IMAGE_NEXT and squashfs is not None: + squashfs.umount_next_image_fs() + except Exception as e: + cli_abort(ctx, str(e)) + + if aborted: + ctx.abort() + + +# 'show' subgroup +@cli.group() +def show(): + """Display platform info""" + pass + + +# 'status' subcommand +@show.command() +@click.pass_context +def status(ctx): + """Show platform components status""" + try: + csp = ComponentStatusProvider() + click.echo(csp.get_status()) + except Exception as e: + cli_abort(ctx, str(e)) + + +# 'version' subcommand +@show.command() +def version(): + """Show utility version""" + click.echo("fwutil version {0}".format(VERSION)) + +install.add_command(chassis) +install.add_command(module) + +chassis.add_command(component) +module.add_command(component) + +# ========================= CLI entrypoint ===================================== + +if __name__ == '__main__': + cli() diff --git a/setup.py b/setup.py index 766707e451..47bd6e2fb0 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ 'ssdutil', 'pfc', 'psuutil', + 'fwutil', 'pddf_fanutil', 'pddf_psuutil', 'pddf_thermalutil', @@ -120,6 +121,7 @@ 'ssdutil = ssdutil.main:ssdutil', 'pfc = pfc.main:cli', 'psuutil = psuutil.main:cli', + 'fwutil = fwutil.main:cli', 'pddf_fanutil = pddf_fanutil.main:cli', 'pddf_psuutil = pddf_psuutil.main:cli', 'pddf_thermalutil = pddf_thermalutil.main:cli', diff --git a/show/main.py b/show/main.py index e15dd2465c..4a52306044 100755 --- a/show/main.py +++ b/show/main.py @@ -1684,6 +1684,13 @@ def temperature(): cmd = 'tempershow' run_command(cmd) +# 'firmware' subcommand ("show platform firmware") +@platform.command() +def firmware(): + """Show firmware status information""" + cmd = "fwutil show status" + run_command(cmd) + # # 'logging' command ("show logging") #