diff --git a/python/vyos/utils/convert.py b/python/vyos/utils/convert.py index 9a8a1ff7d..c02f0071e 100644 --- a/python/vyos/utils/convert.py +++ b/python/vyos/utils/convert.py @@ -1,197 +1,200 @@ # Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. def seconds_to_human(s, separator=""): """ Converts number of seconds passed to a human-readable interval such as 1w4d18h35m59s """ s = int(s) week = 60 * 60 * 24 * 7 day = 60 * 60 * 24 hour = 60 * 60 remainder = 0 result = "" weeks = s // week if weeks > 0: result = "{0}w".format(weeks) s = s % week days = s // day if days > 0: result = "{0}{1}{2}d".format(result, separator, days) s = s % day hours = s // hour if hours > 0: result = "{0}{1}{2}h".format(result, separator, hours) s = s % hour minutes = s // 60 if minutes > 0: result = "{0}{1}{2}m".format(result, separator, minutes) s = s % 60 seconds = s if seconds > 0: result = "{0}{1}{2}s".format(result, separator, seconds) return result -def bytes_to_human(bytes, initial_exponent=0, precision=2): +def bytes_to_human(bytes, initial_exponent=0, precision=2, + int_below_exponent=0): """ Converts a value in bytes to a human-readable size string like 640 KB The initial_exponent parameter is the exponent of 2, e.g. 10 (1024) for kilobytes, 20 (1024 * 1024) for megabytes. """ if bytes == 0: return "0 B" from math import log2 bytes = bytes * (2**initial_exponent) # log2 is a float, while range checking requires an int exponent = int(log2(bytes)) + if exponent < int_below_exponent: + precision = 0 if exponent < 10: value = bytes suffix = "B" elif exponent in range(10, 20): value = bytes / 1024 suffix = "KB" elif exponent in range(20, 30): value = bytes / 1024**2 suffix = "MB" elif exponent in range(30, 40): value = bytes / 1024**3 suffix = "GB" else: value = bytes / 1024**4 suffix = "TB" # Add a new case when the first machine with petabyte RAM # hits the market. size_string = "{0:.{1}f} {2}".format(value, precision, suffix) return size_string def human_to_bytes(value): """ Converts a data amount with a unit suffix to bytes, like 2K to 2048 """ from re import match as re_match res = re_match(r'^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]+)\s*$', value) if not res: raise ValueError(f"'{value}' is not a valid data amount") else: amount = float(res.group(1)) unit = res.group(2).lower() if unit == 'b': res = amount elif (unit == 'k') or (unit == 'kb'): res = amount * 1024 elif (unit == 'm') or (unit == 'mb'): res = amount * 1024**2 elif (unit == 'g') or (unit == 'gb'): res = amount * 1024**3 elif (unit == 't') or (unit == 'tb'): res = amount * 1024**4 else: raise ValueError(f"Unsupported data unit '{unit}'") # There cannot be fractional bytes, so we convert them to integer. # However, truncating causes problems with conversion back to human unit, # so we round instead -- that seems to work well enough. return round(res) def mac_to_eui64(mac, prefix=None): """ Convert a MAC address to a EUI64 address or, with prefix provided, a full IPv6 address. Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3 """ import re from ipaddress import ip_network # http://tools.ietf.org/html/rfc4291#section-2.5.1 eui64 = re.sub(r'[.:-]', '', mac).lower() eui64 = eui64[0:6] + 'fffe' + eui64[6:] eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:] if prefix is None: return ':'.join(re.findall(r'.{4}', eui64)) else: try: net = ip_network(prefix, strict=False) euil = int('0x{0}'.format(eui64), 16) return str(net[euil]) except: # pylint: disable=bare-except return def convert_data(data) -> dict | list | tuple | str | int | float | bool | None: """Filter and convert multiple types of data to types usable in CLI/API WARNING: Must not be used for anything except formatting output for API or CLI On the output allowed everything supported in JSON. Args: data (Any): input data Returns: dict | list | tuple | str | int | float | bool | None: converted data """ from base64 import b64encode # return original data for types which do not require conversion if isinstance(data, str | int | float | bool | None): return data if isinstance(data, list): list_tmp = [] for item in data: list_tmp.append(convert_data(item)) return list_tmp if isinstance(data, tuple): list_tmp = list(data) tuple_tmp = tuple(convert_data(list_tmp)) return tuple_tmp if isinstance(data, bytes | bytearray): try: return data.decode() except UnicodeDecodeError: return b64encode(data).decode() if isinstance(data, set | frozenset): list_tmp = convert_data(list(data)) return list_tmp if isinstance(data, dict): dict_tmp = {} for key, value in data.items(): dict_tmp[key] = convert_data(value) return dict_tmp # do not return anything for other types # which cannot be converted to JSON # for example: complex | range | memoryview return diff --git a/src/op_mode/image_info.py b/src/op_mode/image_info.py index ae0677196..14dca7476 100755 --- a/src/op_mode/image_info.py +++ b/src/op_mode/image_info.py @@ -1,108 +1,112 @@ #!/usr/bin/env python3 # # Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This file is part of VyOS. # # VyOS is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # VyOS is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # VyOS. If not, see <https://www.gnu.org/licenses/>. import sys from typing import List, Union -from hurry.filesize import size from tabulate import tabulate from vyos import opmode from vyos.system import disk, grub, image +from vyos.utils.convert import bytes_to_human def _format_show_images_summary(images_summary: image.BootDetails) -> str: headers: list[str] = ['Name', 'Default boot', 'Running'] table_data: list[list[str]] = list() for image_item in images_summary.get('images_available', []): name: str = image_item if images_summary.get('image_default') == name: default: str = 'Yes' else: default: str = '' if images_summary.get('image_running') == name: running: str = 'Yes' else: running: str = '' table_data.append([name, default, running]) tabulated: str = tabulate(table_data, headers) return tabulated def _format_show_images_details( images_details: list[image.ImageDetails]) -> str: headers: list[str] = [ 'Name', 'Version', 'Storage Read-Only', 'Storage Read-Write', 'Storage Total' ] table_data: list[list[Union[str, int]]] = list() for image_item in images_details: name: str = image_item.get('name') version: str = image_item.get('version') - disk_ro: int = size(image_item.get('disk_ro')) - disk_rw: int = size(image_item.get('disk_rw')) - disk_total: int = size(image_item.get('disk_total')) + disk_ro: str = bytes_to_human(image_item.get('disk_ro'), + precision=1, int_below_exponent=30) + disk_rw: str = bytes_to_human(image_item.get('disk_rw'), + precision=1, int_below_exponent=30) + disk_total: str = bytes_to_human(image_item.get('disk_total'), + precision=1, int_below_exponent=30) table_data.append([name, version, disk_ro, disk_rw, disk_total]) - tabulated: str = tabulate(table_data, headers) + tabulated: str = tabulate(table_data, headers, + colalign=('left', 'left', 'right', 'right', 'right')) return tabulated def show_images_summary(raw: bool) -> Union[image.BootDetails, str]: images_available: list[str] = grub.version_list() root_dir: str = disk.find_persistence() boot_vars: dict = grub.vars_read(f'{root_dir}/{image.CFG_VYOS_VARS}') images_summary: image.BootDetails = dict() images_summary['image_default'] = image.get_default_image() images_summary['image_running'] = image.get_running_image() images_summary['images_available'] = images_available images_summary['console_type'] = boot_vars.get('console_type') images_summary['console_num'] = boot_vars.get('console_num') if raw: return images_summary else: return _format_show_images_summary(images_summary) def show_images_details(raw: bool) -> Union[list[image.ImageDetails], str]: images: list[str] = grub.version_list() images_details: list[image.ImageDetails] = list() for image_name in images: images_details.append(image.get_details(image_name)) if raw: return images_details else: return _format_show_images_details(images_details) if __name__ == '__main__': try: res = opmode.run(sys.modules[__name__]) if res: print(res) except (ValueError, opmode.Error) as e: print(e) sys.exit(1)