diff --git a/board/common/rootfs/usr/bin/show-legacy b/board/common/rootfs/usr/bin/show-legacy old mode 100755 new mode 100644 diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 5f0c5da6..cf9694ff 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -139,6 +139,7 @@ BR2_PACKAGE_CURIOS_HTTPD=y BR2_PACKAGE_CURIOS_NFTABLES=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y +BR2_PACKAGE_SHOW=y BR2_PACKAGE_FACTORY=y BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index d083752a..bc863cfe 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -119,6 +119,7 @@ BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y +BR2_PACKAGE_SHOW=y BR2_PACKAGE_FACTORY=y BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y diff --git a/configs/r2s_defconfig b/configs/r2s_defconfig index 32d88cdb..fb2af3e4 100644 --- a/configs/r2s_defconfig +++ b/configs/r2s_defconfig @@ -176,6 +176,7 @@ INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_CONFD=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y +BR2_PACKAGE_SHOW=y BR2_PACKAGE_FACTORY=y BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index d7208654..88e92cd3 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -169,6 +169,7 @@ BR2_PACKAGE_CONFD=y # BR2_PACKAGE_CONFD_TEST_MODE is not set BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y +BR2_PACKAGE_SHOW=y BR2_PACKAGE_FACTORY=y BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 4f43477c..7f864b6d 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -141,6 +141,7 @@ BR2_PACKAGE_CURIOS_HTTPD=y BR2_PACKAGE_CURIOS_NFTABLES=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y +BR2_PACKAGE_SHOW=y BR2_PACKAGE_FACTORY=y BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 76b2c364..6cc50e42 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -123,6 +123,7 @@ BR2_PACKAGE_CONFD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y +BR2_PACKAGE_SHOW=y BR2_PACKAGE_FACTORY=y BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y BR2_PACKAGE_FINIT_PLUGIN_HOOK_SCRIPTS=y diff --git a/package/Config.in b/package/Config.in index 628d1f93..c76c670b 100644 --- a/package/Config.in +++ b/package/Config.in @@ -34,6 +34,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/python-libyang/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/python-statd/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/python-yangdoc/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/skeleton-init-finit/Config.in" +source "$BR2_EXTERNAL_INFIX_PATH/package/show/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/tetris/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/libyang-cpp/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/sysrepo-cpp/Config.in" diff --git a/package/show/Config.in b/package/show/Config.in new file mode 100644 index 00000000..2cc1dc23 --- /dev/null +++ b/package/show/Config.in @@ -0,0 +1,6 @@ +config BR2_PACKAGE_SHOW + bool "show" + help + Tool to retrieve and present operational data from sysrepo. + + https://github.com/kernelkit/infix diff --git a/package/show/show.mk b/package/show/show.mk new file mode 100644 index 00000000..3db961be --- /dev/null +++ b/package/show/show.mk @@ -0,0 +1,19 @@ +################################################################################ +# +# show +# +################################################################################ + +SHOW_VERSION = 1.0 +SHOW_LICENSE = MIT +SHOW_LICENSE_FILES = LICENSE +SHOW_SITE_METHOD = local +SHOW_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/show +SHOW_REDISTRIBUTE = NO + +define SHOW_INSTALL_TARGET_CMDS + $(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) -C $(@D) \ + DESTDIR="$(TARGET_DIR)" install +endef + +$(eval $(generic-package)) diff --git a/src/show/LICENSE b/src/show/LICENSE new file mode 100644 index 00000000..fa7d9811 --- /dev/null +++ b/src/show/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Richard Alpe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/show/Makefile b/src/show/Makefile new file mode 100644 index 00000000..341b708b --- /dev/null +++ b/src/show/Makefile @@ -0,0 +1,10 @@ +.PHONY: all clean distclean install + +all: + +clean: + +distclean: clean + +install: + install -D show.py $(DESTDIR)/bin/show diff --git a/src/show/show.py b/src/show/show.py new file mode 100755 index 00000000..5e5d6161 --- /dev/null +++ b/src/show/show.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 + +import re +import subprocess +import json +from typing import List +import os +import shlex +import argparse + +RAW_OUTPUT = False + +def run_sysrepocfg(xpath: str) -> dict: + if not isinstance(xpath, str) or not xpath.startswith("/"): + print("Invalid XPATH. It must be a valid string starting with '/'.") + return {} + + safe_xpath = shlex.quote(xpath) + + try: + result = subprocess.run([ + "sysrepocfg", "-f", "json", "-X", "-d", "operational", "-x", safe_xpath + ], capture_output=True, text=True, check=True) + json_data = json.loads(result.stdout) + return json_data + except subprocess.CalledProcessError as e: + print(f"Error running sysrepocfg: {e}") + return {} + except json.JSONDecodeError as e: + print(f"Error parsing JSON output: {e}") + return {} + +def cli_pretty(json_data: dict, command: str, *args: str): + if not command or not all(isinstance(arg, str) for arg in args): + print("Invalid command or arguments. All arguments must be strings.") + return + + safe_args = [shlex.quote(arg) for arg in args] + + try: + json_input = json.dumps(json_data) # Keep as string, not bytes + result = subprocess.run([ + "/usr/libexec/statd/cli-pretty", command, *safe_args + ], input=json_input, capture_output=True, text=True, check=True) + print(result.stdout, end="") + except subprocess.CalledProcessError as e: + print(f"Error running cli-pretty: {e}") + +def dhcp(args: List[str]) -> None: + data = run_sysrepocfg("/infix-dhcp-server:dhcp-server") + if not data: + print("No interface data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty(data, "show-dhcp-server") + +def hardware(args: List[str]) -> None: + data = run_sysrepocfg("/ietf-hardware:hardware") + if not data: + print("No hardware data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty(data, "show-hardware") + +def ntp(args: List[str]) -> None: + data = run_sysrepocfg("/system-state/ntp") + if not data: + print("No ntp data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty(data, "show-ntp") + +def is_valid_interface_name(interface_name: str) -> bool: + """ + Validates a Linux network interface name. + """ + if len(interface_name) > 15: + return False + + pattern = r'^[a-zA-Z0-9._-]+$' + return bool(re.match(pattern, interface_name)) + +def interface(args: List[str]) -> None: + data = run_sysrepocfg("/ietf-interfaces:interfaces") + if not data: + print("No interface data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + + if len(args) == 0: + cli_pretty(data, "show-interfaces") + elif len(args) == 1: + iface = args[0] + if is_valid_interface_name(iface): + cli_pretty(data, f"show-interfaces", "-n", iface) + else: + print(f"Invalid interface name: {iface}") + else: + print("Too many arguments provided. Only one interface name is expected.") + +def stp(args: List[str]) -> None: + data = run_sysrepocfg("/ietf-interfaces:interfaces") + if not data: + print("No interface data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty(data, "show-bridge-stp") + +def software(args: List[str]) -> None: + data = run_sysrepocfg("/ietf-system:system-state/infix-system:software") + if not data: + print("No software data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + + if len(args) == 0: + cli_pretty(data, "show-software") + print(".....") + elif len(args) == 1: + name = args[0] + if name not in ("primary", "secondary"): + print(f"Invalid software name: {name}") + return + + cli_pretty(data, f"show-software", "-n", name) + else: + print("Too many arguments provided. Only one name is expected.") + +def routes(args: List[str]): + ip_version = args[0] if args and args[0] in ["ipv4", "ipv6"] else "ipv4" + + data = run_sysrepocfg("/ietf-routing:routing/ribs") + if not data: + print("No route data retrieved.") + return + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty(data, "show-routing-table", "-i", ip_version) + + +def execute_command(command: str, args: List[str]): + command_mapping = { + 'dhcp': dhcp, + 'hardware': hardware, + 'interface': interface, + 'ntp': ntp, + 'routes': routes, + 'software' : software, + 'stp': stp, + } + + if command in command_mapping: + command_mapping[command](args) + else: + print(f"Unknown command: {command}") + + +def main(): + global RAW_OUTPUT + + parser = argparse.ArgumentParser(description="Show operational data") + parser.add_argument('command', help="Command to execute") + parser.add_argument('args', nargs=argparse.REMAINDER, help="Additional arguments for the command") + parser.add_argument('-r', '--raw', action='store_true', help="Print raw JSON output from Sysrepo") + + args = parser.parse_args() + RAW_OUTPUT = args.raw + + execute_command(args.command, args.args) + + +if __name__ == "__main__": + main()