diff --git a/python/vyos/utils/convert.py b/python/vyos/utils/convert.py index c02f0071e..41e65081f 100644 --- a/python/vyos/utils/convert.py +++ b/python/vyos/utils/convert.py @@ -1,200 +1,205 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 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) + year = 60 * 60 * 24 * 365.25 week = 60 * 60 * 24 * 7 day = 60 * 60 * 24 hour = 60 * 60 - remainder = 0 - result = "" + result = [] + + years = s // year + if years > 0: + result.append(f'{int(years)}y') + s = int(s % year) weeks = s // week if weeks > 0: - result = "{0}w".format(weeks) + result.append(f'{weeks}w') s = s % week days = s // day if days > 0: - result = "{0}{1}{2}d".format(result, separator, days) + result.append(f'{days}d') s = s % day hours = s // hour if hours > 0: - result = "{0}{1}{2}h".format(result, separator, hours) + result.append(f'{hours}h') s = s % hour minutes = s // 60 if minutes > 0: - result = "{0}{1}{2}m".format(result, separator, minutes) + result.append(f'{minutes}m') s = s % 60 seconds = s if seconds > 0: - result = "{0}{1}{2}s".format(result, separator, seconds) + result.append(f'{seconds}s') - return result + return separator.join(result) 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/uptime.py b/src/op_mode/uptime.py index d6adf6f4d..059a4c3f6 100755 --- a/src/op_mode/uptime.py +++ b/src/op_mode/uptime.py @@ -1,82 +1,82 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program 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 this program. If not, see <http://www.gnu.org/licenses/>. import sys import vyos.opmode def _get_uptime_seconds(): from re import search from vyos.utils.file import read_file data = read_file("/proc/uptime") seconds = search("([0-9\.]+)\s", data).group(1) return int(float(seconds)) def _get_load_averages(): from re import search from vyos.utils.process import cmd from vyos.cpu import get_core_count data = cmd("uptime") matches = search(r"load average:\s*(?P<one>[0-9\.]+)\s*,\s*(?P<five>[0-9\.]+)\s*,\s*(?P<fifteen>[0-9\.]+)\s*", data) core_count = get_core_count() res = {} res[1] = float(matches["one"]) / core_count res[5] = float(matches["five"]) / core_count res[15] = float(matches["fifteen"]) / core_count return res def _get_raw_data(): from vyos.utils.convert import seconds_to_human res = {} res["uptime_seconds"] = _get_uptime_seconds() - res["uptime"] = seconds_to_human(_get_uptime_seconds()) + res["uptime"] = seconds_to_human(_get_uptime_seconds(), separator=' ') res["load_average"] = _get_load_averages() return res def _get_formatted_output(data): out = "Uptime: {}\n\n".format(data["uptime"]) avgs = data["load_average"] out += "Load averages:\n" out += "1 minute: {:.01f}%\n".format(avgs[1]*100) out += "5 minutes: {:.01f}%\n".format(avgs[5]*100) out += "15 minutes: {:.01f}%\n".format(avgs[15]*100) return out def show(raw: bool): uptime_data = _get_raw_data() if raw: return uptime_data else: return _get_formatted_output(uptime_data) if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1)