diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml
index e8c010b4..59831d7d 100644
--- a/src/klish-plugin-infix/xml/infix.xml
+++ b/src/klish-plugin-infix/xml/infix.xml
@@ -353,6 +353,12 @@
+
+
+ show system |pager
+
+
+
diff --git a/src/show/show.py b/src/show/show.py
index 42ed458c..eca68d21 100755
--- a/src/show/show.py
+++ b/src/show/show.py
@@ -190,6 +190,96 @@ def wifi(args: List[str]):
else:
print(f"Invalid interface name: {iface}")
+def system(args: List[str]) -> None:
+ # Get system state from sysrepo
+ data = run_sysrepocfg("/ietf-system:system-state")
+ if not data:
+ print("No system data retrieved.")
+ return
+
+ # Augment with runtime data
+ runtime = {}
+
+ # Get thermal zones
+ thermal_zones = []
+ try:
+ for zone in os.listdir("/sys/class/thermal"):
+ if zone.startswith("thermal_zone"):
+ try:
+ with open(f"/sys/class/thermal/{zone}/type") as f:
+ zone_type = f.read().strip()
+ with open(f"/sys/class/thermal/{zone}/temp") as f:
+ temp = int(f.read().strip()) / 1000.0
+ thermal_zones.append({"type": zone_type, "temp": temp})
+ except (FileNotFoundError, ValueError):
+ pass
+ except FileNotFoundError:
+ pass
+
+ if thermal_zones:
+ runtime["thermal"] = thermal_zones
+
+ # Get disk usage for /, /var, /cfg
+ disk_usage = []
+ for mount in ["/", "/var", "/cfg"]:
+ try:
+ result = subprocess.run(["df", "-h", mount],
+ capture_output=True, text=True, check=True)
+ lines = result.stdout.strip().split("\n")
+ if len(lines) > 1:
+ parts = lines[1].split()
+ if len(parts) >= 5:
+ disk_usage.append({
+ "mount": mount,
+ "size": parts[1],
+ "used": parts[2],
+ "available": parts[3],
+ "percent": parts[4]
+ })
+ except subprocess.CalledProcessError:
+ pass
+
+ if disk_usage:
+ runtime["disk"] = disk_usage
+
+ # Get memory info
+ try:
+ with open("/proc/meminfo") as f:
+ mem_info = {}
+ for line in f:
+ parts = line.split(":")
+ if len(parts) == 2:
+ key = parts[0].strip()
+ value = parts[1].strip()
+ if key in ["MemTotal", "MemFree", "MemAvailable"]:
+ # Convert from kB to MB
+ mem_info[key] = int(value.split()[0]) // 1024
+ runtime["memory"] = mem_info
+ except FileNotFoundError:
+ pass
+
+ # Get load average
+ try:
+ with open("/proc/loadavg") as f:
+ load_parts = f.read().strip().split()
+ if len(load_parts) >= 3:
+ runtime["load"] = {
+ "1min": load_parts[0],
+ "5min": load_parts[1],
+ "15min": load_parts[2]
+ }
+ except FileNotFoundError:
+ pass
+
+ # Add runtime data to main data structure
+ data["runtime"] = runtime
+
+ if RAW_OUTPUT:
+ print(json.dumps(data, indent=2))
+ return
+
+ cli_pretty(data, "show-system")
+
def execute_command(command: str, args: List[str]):
command_mapping = {
'dhcp': dhcp,
@@ -201,6 +291,7 @@ def execute_command(command: str, args: List[str]):
'services' : services,
'software' : software,
'stp': stp,
+ 'system': system,
'wifi': wifi
}
diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py
index 7f4ebc88..f67402ca 100755
--- a/src/statd/python/cli_pretty/cli_pretty.py
+++ b/src/statd/python/cli_pretty/cli_pretty.py
@@ -193,6 +193,19 @@ class PadLldp:
port_id = 20
+class PadDiskUsage:
+ mount = 18
+ size = 12
+ used = 12
+ avail = 12
+ percent = 6
+
+ @classmethod
+ def table_width(cls):
+ """Total width of disk usage table"""
+ return cls.mount + cls.size + cls.used + cls.avail + cls.percent
+
+
class PadFirewall:
zone_locked = 2
zone_name = 21
@@ -1589,6 +1602,121 @@ def show_ntp(json):
print(row)
+def show_system(json):
+ """System information overivew"""
+ if not json.get("ietf-system:system-state"):
+ print("Error: No system data available.")
+ sys.exit(1)
+
+ system_state = json["ietf-system:system-state"]
+ platform = system_state.get("platform", {})
+ clock = system_state.get("clock", {})
+ software = system_state.get("infix-system:software", {})
+ runtime = json.get("runtime", {})
+
+ # Calculate uptime
+ uptime_str = "Unknown"
+ if clock.get("current-datetime") and clock.get("boot-datetime"):
+ try:
+ current = datetime.fromisoformat(clock["current-datetime"].replace("Z", "+00:00"))
+ boot = datetime.fromisoformat(clock["boot-datetime"].replace("Z", "+00:00"))
+ uptime = current - boot
+ days = uptime.days
+ hours, remainder = divmod(uptime.seconds, 3600)
+ minutes, seconds = divmod(remainder, 60)
+ uptime_str = f"{days}d {hours:02d}:{minutes:02d}:{seconds:02d}"
+ except (ValueError, KeyError):
+ pass
+
+ width = PadDiskUsage.table_width()
+ print(Decore.invert(f"{'SYSTEM INFORMATION':<{width}}"))
+ print(f"{'OS Name':<20}: {platform.get('os-name', 'Unknown')}")
+ print(f"{'OS Version':<20}: {platform.get('os-version', 'Unknown')}")
+ print(f"{'Architecture':<20}: {platform.get('machine', 'Unknown')}")
+
+ booted = software.get("booted", "Unknown")
+ slots = software.get("slot", [])
+ booted_slot = None
+ for slot in slots:
+ if slot.get("state") == "booted":
+ booted_slot = slot
+ break
+
+ if booted_slot:
+ bundle = booted_slot.get("bundle", {})
+ print(f"{'Boot Partition':<20}: {booted} ({bundle.get('version', 'Unknown')})")
+ else:
+ print(f"{'Boot Partition':<20}: {booted}")
+
+ # Format current time more readably: "2025-10-18 13:23:47 +00:00"
+ current_time = clock.get('current-datetime', 'Unknown')
+ if current_time != 'Unknown':
+ try:
+ dt = datetime.fromisoformat(current_time.replace("Z", "+00:00"))
+ # Format as "YYYY-MM-DD HH:MM:SS +HH:MM" (keep UTC offset)
+ current_time = dt.strftime('%Y-%m-%d %H:%M:%S %z')
+ # Insert colon in timezone offset: +0000 -> +00:00
+ if len(current_time) >= 5 and current_time[-5] in ['+', '-']:
+ current_time = current_time[:-2] + ':' + current_time[-2:]
+ except (ValueError, AttributeError):
+ pass
+
+ print(f"{'Current Time':<20}: {current_time}")
+ print(f"{'Uptime':<20}: {uptime_str}")
+
+ Decore.title("Status", width)
+
+ load = runtime.get("load", {})
+ if load:
+ print(f"{'Load Average':<20}: {load.get('1min', '?')}, {load.get('5min', '?')}, {load.get('15min', '?')}")
+
+ memory = runtime.get("memory", {})
+ if memory:
+ total = memory.get("MemTotal", 0)
+ available = memory.get("MemAvailable", 0)
+ used = total - available
+ percent = int((used / total * 100)) if total > 0 else 0
+ print(f"{'Memory':<20}: {used} / {total} MB ({percent}% used)")
+
+ thermal = runtime.get("thermal", [])
+ if thermal:
+ for zone in thermal:
+ zone_type = zone.get("type", "Unknown").replace("-thermal", "")
+ temp = zone.get("temp", 0)
+ # Color code temperature: green < 60, yellow 60-75, red > 75
+ temp_str = f"{temp:.1f}°C"
+ if temp < 60:
+ temp_colored = Decore.green(temp_str)
+ elif temp < 75:
+ temp_colored = Decore.yellow(temp_str)
+ else:
+ temp_colored = Decore.red(temp_str)
+ print(f"{'Temperature':<20}: {temp_colored} ({zone_type})")
+
+ disk = runtime.get("disk", [])
+ # Filter out root partition (/) - it's read-only and shows confusing 100% usage
+ disk_filtered = [d for d in disk if d.get("mount") != "/"]
+ if disk_filtered:
+ Decore.title("Disk Usage", width)
+ hdr = (f"{'MOUNTPOINT':<{PadDiskUsage.mount}}"
+ f"{'SIZE':>{PadDiskUsage.size}}"
+ f"{'USED':>{PadDiskUsage.used}}"
+ f"{'AVAIL':>{PadDiskUsage.avail}}"
+ f"{'USE%':>{PadDiskUsage.percent}}")
+ print(Decore.invert(hdr))
+ for d in disk_filtered:
+ mount = d.get("mount", "?")
+ size = d.get("size", "?")
+ used = d.get("used", "?")
+ avail = d.get("available", "?")
+ percent = d.get("percent", "?")
+ print(f"{mount:<{PadDiskUsage.mount}}"
+ f"{size:>{PadDiskUsage.size}}"
+ f"{used:>{PadDiskUsage.used}}"
+ f"{avail:>{PadDiskUsage.avail}}"
+ f"{percent:>{PadDiskUsage.percent}}")
+
+
def show_dhcp_server(json, stats):
data = json.get("infix-dhcp-server:dhcp-server")
if not data:
@@ -2518,31 +2646,33 @@ def main():
subparsers.add_parser('show-bridge-stp', help='Show spanning tree state')
subparsers.add_parser('show-dhcp-server', help='Show DHCP server') \
- .add_argument("-s", "--stats", action="store_true", help="Show server statistics")
+ .add_argument("-s", "--stats", action="store_true", help="Show server statistics")
subparsers.add_parser('show-hardware', help='Show USB ports')
subparsers.add_parser('show-interfaces', help='Show interfaces') \
- .add_argument('-n', '--name', help='Interface name')
+ .add_argument('-n', '--name', help='Interface name')
subparsers.add_parser('show-lldp', help='Show LLDP neighbors')
subparsers.add_parser('show-firewall', help='Show firewall overview')
subparsers.add_parser('show-firewall-zone', help='Show firewall zones') \
- .add_argument('name', nargs='?', help='Zone name')
+ .add_argument('name', nargs='?', help='Zone name')
subparsers.add_parser('show-firewall-policy', help='Show firewall policies') \
- .add_argument('name', nargs='?', help='Policy name')
+ .add_argument('name', nargs='?', help='Policy name')
subparsers.add_parser('show-firewall-service', help='Show firewall services') \
- .add_argument('name', nargs='?', help='Service name')
+ .add_argument('name', nargs='?', help='Service name')
subparsers.add_parser('show-ntp', help='Show NTP sources')
subparsers.add_parser('show-routing-table', help='Show the routing table') \
- .add_argument('-i', '--ip', required=True, help='IPv4 or IPv6 address')
+ .add_argument('-i', '--ip', required=True, help='IPv4 or IPv6 address')
subparsers.add_parser('show-services', help='Show system services')
subparsers.add_parser('show-software', help='Show software versions') \
- .add_argument('-n', '--name', help='Slotname')
+ .add_argument('-n', '--name', help='Slotname')
+
+ subparsers.add_parser('show-system', help='Show system overview')
args = parser.parse_args()
UNIT_TEST = args.test
@@ -2575,6 +2705,8 @@ def main():
show_software(json_data, args.name)
elif args.command == "show-services":
show_services(json_data)
+ elif args.command == "show-system":
+ show_system(json_data)
else:
print(f"Error, unknown command '{args.command}'")