Merge pull request #1004 from kernelkit/new-show-command

Add new show command

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-04-07 14:34:35 +02:00
committed by GitHub
17 changed files with 332 additions and 70 deletions
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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"
+6
View File
@@ -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
+19
View File
@@ -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))
+49 -68
View File
@@ -239,35 +239,33 @@
<SWITCH name="optional" min="0">
<COMMAND name="fdb" help="Show unicast forwarding database">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
bridge -color fdb |pager
</ACTION>
bridge -color fdb |pager
</ACTION>
</COMMAND>
<COMMAND name="mdb" help="Show mulficast forwarding database">
<SWITCH name="optional" min="0">
<COMMAND name="detailed" help="Detailed (full) output">
<SWITCH name="optional" min="0">
<COMMAND name="detailed" help="Detailed (full) output">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
bridge -color -d mdb |pager
</ACTION>
</COMMAND>
</SWITCH>
bridge -color -d mdb |pager
</ACTION>
</COMMAND>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
sysrepocfg -d operational -X -f json -x /ietf-interfaces:interfaces | \
/usr/libexec/statd/cli-pretty show-bridge-mdb |pager
</ACTION>
show --raw interface | /usr/libexec/statd/cli-pretty show-bridge-mdb |pager
</ACTION>
</COMMAND>
<COMMAND name="stp" help="Show spanning tree status">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
sysrepocfg -d operational -X -f json -x /ietf-interfaces:interfaces | \
/usr/libexec/statd/cli-pretty show-bridge-stp |pager
</ACTION>
show --raw interface | /usr/libexec/statd/cli-pretty show-bridge-stp |pager
</ACTION>
</COMMAND>
<COMMAND name="vlan" help="Show VLAN table">
<ACTION sym="script">bridge -color vlan</ACTION>
<SWITCH name="optional" min="0">
<COMMAND name="detailed" help="Detailed (full) output">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
bridge -color -d vlan |pager
</ACTION>
bridge -color -d vlan |pager
</ACTION>
</COMMAND>
</SWITCH>
</COMMAND>
@@ -292,8 +290,7 @@
<COMMAND name="ntp" help="Show NTP (client) status">
<ACTION sym="script">
sysrepocfg -d operational -X -f json -x /system-state/ntp | \
/usr/libexec/statd/cli-pretty "show-ntp"
show ntp
</ACTION>
<SWITCH name="optional" min="0">
<COMMAND name="tracking" help="Show NTP tracking">
@@ -307,11 +304,11 @@
<SWITCH name="optional" min="0">
<COMMAND name="statistics" help="Show DHCP server statistics">
<ACTION sym="script" in="tty" out="tty" interrupt="true">show dhcp stat</ACTION>
<ACTION sym="script" in="tty" out="tty" interrupt="true">show dhcp stat</ACTION>
</COMMAND>
<COMMAND name="detailed" help="Detailed (full operational) output">
<ACTION sym="script" in="tty" out="tty" interrupt="true">show dhcp detail</ACTION>
<ACTION sym="script" in="tty" out="tty" interrupt="true">show dhcp detail</ACTION>
</COMMAND>
</SWITCH>
@@ -325,51 +322,43 @@
</COMMAND>
</SWITCH>
<ACTION sym="script">
tmp=$(mktemp)
sysrepocfg -d operational -X -f json -x /ietf-system:system-state/infix-system:software | \
jq -r '.[][].slot |= sort_by(.name)' > "$tmp"
if [ -n "$KLISH_PARAM_name" ]; then
cat "$tmp" | /usr/libexec/statd/cli-pretty "show-software" -n "$KLISH_PARAM_name"
else
cat "$tmp" | /usr/libexec/statd/cli-pretty "show-software"
fi
rm "$tmp"
show software "$KLISH_PARAM_name"
</ACTION>
</COMMAND>
<COMMAND name="ospf" help="Show OSPF status">
<PARAM name="name" ptype="/STRING" help="neighbor, interfaces or bfd" min="0" max="1">
<COMPL>
<ACTION sym="printl">neighbor</ACTION>
<ACTION sym="printl">interfaces</ACTION>
<ACTION sym="printl">routes</ACTION>
<ACTION sym="printl">database</ACTION>
<ACTION sym="printl">bfd</ACTION>
<ACTION sym="printl">neighbor</ACTION>
<ACTION sym="printl">interfaces</ACTION>
<ACTION sym="printl">routes</ACTION>
<ACTION sym="printl">database</ACTION>
<ACTION sym="printl">bfd</ACTION>
</COMPL>
</PARAM>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
if [ -z "$KLISH_PARAM_name" ]; then
doas vtysh -c "show ip ospf" |pager
doas vtysh -c "show-legacy ip ospf" |pager
elif [ "$KLISH_PARAM_name" == "neighbor" ];then
doas vtysh -c "show ip ospf neighbor" |pager
doas vtysh -c "show-legacy ip ospf neighbor" |pager
elif [ "$KLISH_PARAM_name" == "interfaces" ];then
doas vtysh -c "show ip ospf interface" |pager
doas vtysh -c "show-legacy ip ospf interface" |pager
elif [ "$KLISH_PARAM_name" == "routes" ];then
doas vtysh -c "show ip ospf route" |pager
doas vtysh -c "show-legacy ip ospf route" |pager
elif [ "$KLISH_PARAM_name" == "database" ];then
doas vtysh -c "show ip ospf database" |pager
doas vtysh -c "show-legacy ip ospf database" |pager
elif [ "$KLISH_PARAM_name" == "bfd" ];then
doas vtysh -c "show bfd peers" |pager
doas vtysh -c "show-legacy bfd peers" |pager
fi
</ACTION>
</COMMAND>
<COMMAND name="routes" help="Show routing table">
<PARAM name="ip" ptype="/STRING" help="ipv4 or ipv6" min="0" max="1">
<COMPL>
<ACTION sym="printl">ipv4</ACTION>
<ACTION sym="printl">ipv6</ACTION>
</COMPL>
<COMPL>
<ACTION sym="printl">ipv4</ACTION>
<ACTION sym="printl">ipv6</ACTION>
</COMPL>
</PARAM>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
case $KLISH_PARAM_ip in
@@ -380,14 +369,13 @@
KLISH_PARAM_ip=ipv6
;;
esac
show -p $KLISH_PARAM_ip route |pager
show-legacy -p $KLISH_PARAM_ip route |pager
</ACTION>
</COMMAND>
<COMMAND name="hardware" help="Show hardware information">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
sysrepocfg -f json -X -d operational -x "/ietf-hardware:hardware" | \
/usr/libexec/statd/cli-pretty "show-hardware" |pager
show hardware |pager
</ACTION>
</COMMAND>
@@ -400,14 +388,7 @@
</COMMAND>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
if [ -n "$KLISH_PARAM_name" ]; then
doas sysrepocfg -f json -X -d operational -x \
"/ietf-interfaces:interfaces/interface[name=\"$KLISH_PARAM_name\"]" | \
/usr/libexec/statd/cli-pretty "show-interfaces" -n "$KLISH_PARAM_name" |pager
else
doas sysrepocfg -f json -X -d operational -m ietf-interfaces | \
/usr/libexec/statd/cli-pretty "show-interfaces" |pager
fi
show interface "$KLISH_PARAM_name" |pager
</ACTION>
</COMMAND>
@@ -419,26 +400,26 @@
<SWITCH name="optional" min="0">
<COMMAND name="brief" help="Simplified (human-readable) output" mode="switch">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
ip -color -br addr | sort -V | sed 's/@dsa[0-9]/ /' |pager
</ACTION>
ip -color -br addr | sort -V | sed 's/@dsa[0-9]/ /' |pager
</ACTION>
</COMMAND>
<COMMAND name="detailed" help="Detailed (full) output" mode="switch">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
ip -color -d addr |pager
</ACTION>
ip -color -d addr |pager
</ACTION>
</COMMAND>
<COMMAND name="multicast" help="Show IGMP/MLD status" mode="switch">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
mctl |pager
</ACTION>
mctl |pager
</ACTION>
</COMMAND>
<COMMAND name="route" help="Show IPv4 routing table" mode="switch">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
show -p ip route |pager
</ACTION>
show-legacy -p ip route |pager
</ACTION>
</COMMAND>
</SWITCH>
</COMMAND>
@@ -451,8 +432,8 @@
<SWITCH name="optional" min="0">
<COMMAND name="route" help="Show IPv6 routing table" mode="switch">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
show -p ipv6 route |pager
</ACTION>
show-legacy -p ipv6 route |pager
</ACTION>
</COMMAND>
</SWITCH>
</COMMAND>
@@ -530,11 +511,11 @@
if [ -n "$KLISH_PARAM_split" ]; then
if echo $KLISH_PARAM_addrlen |grep -q ':'; then
split="-S $KLISH_PARAM_split"
else
else
split="-s $KLISH_PARAM_split"
fi
fi
else
split=""
split=""
fi
netcalc $split $KLISH_PARAM_addrlen $KLISH_PARAM_netmask
</ACTION>
+21
View File
@@ -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.
+11
View File
@@ -0,0 +1,11 @@
.PHONY: all clean distclean install
all:
clean:
distclean: clean
install:
install -D show.py $(DESTDIR)/bin/show
install -D bash_completion.d/show $(DESTDIR)/etc/bash_completion.d/show
+27
View File
@@ -0,0 +1,27 @@
_show_completions() {
local cur prev commands interface_subs routes_subs
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
commands="dhcp interface ntp routes software stp"
if [[ $COMP_CWORD -eq 1 ]]; then
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
return 0
fi
case "$prev" in
interface)
COMPREPLY=( $(compgen -W "$(ls /sys/class/net/)" -- "$cur") )
;;
routes)
COMPREPLY=( $(compgen -W "ipv4 ipv6" -- "$cur") )
;;
software)
COMPREPLY=( $(compgen -W "primary secondary" -- "$cur") )
;;
esac
}
complete -F _show_completions show
+190
View File
@@ -0,0 +1,190 @@
#!/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 or not args[0]: # Treat "" as no arg.
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 or not args[0]: # Treat "" as no arg.
cli_pretty(data, "show-software")
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()
+1 -1
View File
@@ -1193,7 +1193,7 @@ def show_software(json, name):
f"{'VERSION':<{PadSoftware.version}}"
f"{'DATE':<{PadSoftware.date}}")
print(Decore.invert(hdr))
for _s in slots:
for _s in reversed(slots):
slot = Software(_s)
if slot.is_rootfs():
slot.print()
+1 -1
View File
@@ -2,5 +2,5 @@
primary secondary net
NAME STATE VERSION DATE 
secondary inactive 94cd526 2025-01-15T10:00:56Z
primary booted 94cd526 2025-01-15T10:00:56Z
secondary inactive 94cd526 2025-01-15T10:00:56Z