Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/confd/yang/confd.inc
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ MODULES=(
"[email protected]"
"[email protected]"
"[email protected]"
"infix-system@2025-10-18.yang"
"infix-system@2025-12-02.yang"
"[email protected]"
"[email protected]"
"[email protected]"
Expand Down
27 changes: 27 additions & 0 deletions src/confd/yang/confd/infix-system.yang
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ module infix-system {
contact "[email protected]";
description "Infix augments and deviations to ietf-system.";

revision 2025-12-02 {
description "Extend services with runtime statistics:
- Add statistics container with memory-usage, uptime, restart-count";
reference "internal";
}
revision 2025-10-18 {
description "New system-state status:
- Add system resource usage: memory, loadavg, filesystem usage
Expand Down Expand Up @@ -508,6 +513,28 @@ module infix-system {
description
"Detailed current status of the process.";
}

container statistics {
description "Service resource usage and runtime statistics";
config false;

leaf memory-usage {
type uint64;
units "bytes";
description "Current memory usage in bytes";
}

leaf uptime {
type uint64;
units "seconds";
description "Time service has been running";
}

leaf restart-count {
type uint32;
description "Number of service restarts";
}
}
}
}

Expand Down
File renamed without changes.
134 changes: 116 additions & 18 deletions src/statd/python/cli_pretty/cli_pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,6 @@ class PadNtpSource:
poll = 14


class PadService:
name = 16
status = 8
pid = 8
description = 40


class PadWifiScan:
ssid = 40
encryption = 30
Expand All @@ -217,6 +210,34 @@ class PadDiskUsage:
avail = 12
percent = 6


def format_memory_bytes(bytes_val):
"""Convert bytes to human-readable format"""
if bytes_val == 0:
return " "
elif bytes_val < 1024:
return f"{bytes_val}B"
elif bytes_val < 1024 * 1024:
return f"{bytes_val // 1024}K"
elif bytes_val < 1024 * 1024 * 1024:
return f"{bytes_val // (1024 * 1024):.1f}M"
else:
return f"{bytes_val // (1024 * 1024 * 1024):.1f}G"


def format_uptime_seconds(seconds):
"""Convert seconds to compact time format"""
if seconds == 0:
return " "
elif seconds < 60:
return f"{seconds}s"
elif seconds < 3600:
return f"{seconds // 60}m"
elif seconds < 86400:
return f"{seconds // 3600}h"
else:
return f"{seconds // 86400}d"

@classmethod
def table_width(cls):
"""Total width of disk usage table"""
Expand Down Expand Up @@ -259,6 +280,69 @@ def table_width(cls):
return cls.zone_locked + cls.zone_name + cls.zone_type + cls.zone_data \
+ cls.zone_services

class Column:
"""Column definition for SimpleTable"""
def __init__(self, name, length, align='left', formatter=None):
self.name = name # Header text
self.width = length # Max visible data length (excluding ANSI codes)
self.align = align # 'left' or 'right' (defaults to 'left')
self.formatter = formatter # Optional function to format values

class SimpleTable:
"""Simple table formatter that handles ANSI colors correctly"""

def __init__(self, columns):
self.columns = columns

@staticmethod
def visible_width(text):
"""Return visible character count, excluding ANSI escape sequences"""
ansi_pattern = r'\x1b\[[0-9;]*m'
clean_text = re.sub(ansi_pattern, '', str(text))
return len(clean_text)

def _format_column(self, value, column):
"""Format a single column value with proper alignment

The column length specifies max expected data length.
Framework automatically adds 1 space for column separation.
"""
if column.formatter:
value = column.formatter(value)

value_str = str(value)
visible_len = self.visible_width(value_str)

if column.align == 'right':
padding = column.width - visible_len
return ' ' * max(0, padding) + value_str + ' '
else: # left alignment (default)
padding = column.width - visible_len
return value_str + ' ' * max(0, padding) + ' '

def header(self, styled=True):
"""Generate formatted header row"""
header_parts = []
for column in self.columns:
if column.align == 'right':
header_parts.append(f"{column.name:>{column.width}} ")
else: # left alignment (default)
header_parts.append(f"{column.name:{column.width}} ")

header_str = ''.join(header_parts)
return Decore.invert(header_str) if styled else header_str

def row(self, *values):
"""Generate formatted data row"""
if len(values) != len(self.columns):
raise ValueError(f"Expected {len(self.columns)} values, got {len(values)}")

row_parts = []
for value, column in zip(values, self.columns):
row_parts.append(self._format_column(value, column))

return ''.join(row_parts).rstrip()


class Decore():
@staticmethod
Expand Down Expand Up @@ -1661,17 +1745,27 @@ def show_services(json):
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
services = services_data.get("service", [])

hdr = (f"{'NAME':<{PadService.name}}"
f"{'STATUS':<{PadService.status}}"
f"{'PID':>{PadService.pid -1}}"
f" {'DESCRIPTION'}")
print(Decore.invert(hdr))
# This is the first usage of simple table. I assume this will be
# copied so I left a lot of comments. If you copy it feel free
# to be less verbose..
service_table = SimpleTable([
Column('NAME', 15, 'left'), # Max service name length
Column('STATUS', 10, 'left'), # Max status text length (e.g., "running")
Column('PID', 7, 'right'), # Max PID digits
Column('MEM', 6, 'right'), # Max memory string (e.g., "123.4M")
Column('UP', 4, 'right'), # Max uptime string (e.g., "3d")
Column('RST', 3, 'right'), # Max restart count digits
Column('DESCRIPTION', 30) # Last column needs no padding
])

print(service_table.header())

for svc in services:
name = svc.get('name', '')
status = svc.get('status', '')
pid = svc.get('pid', 0)
description = svc.get('description', '')
stats = svc.get('statistics', {})

if status in ('running', 'active', 'done'):
status_str = Decore.green(status)
Expand All @@ -1680,13 +1774,17 @@ def show_services(json):
else:
status_str = Decore.yellow(status)

pid_str = str(pid) if pid > 0 else '-'
pid_str = str(pid) if pid > 0 else ' '

row = f"{name:<{PadService.name}}"
row += f"{status_str:<{PadService.status + 9}}"
row += f"{pid_str:>{PadService.pid}}"
row += f" {description}"
print(row)
memory_bytes = int(stats.get('memory-usage', 0))
uptime_secs = int(stats.get('uptime', 0))
restart_count = stats.get('restart-count', 0)

memory_str = format_memory_bytes(memory_bytes)
uptime_str = format_uptime_seconds(uptime_secs)

print(service_table.row(name, status_str, pid_str, memory_str,
uptime_str, restart_count, description))


def show_hardware(json):
Expand Down
7 changes: 6 additions & 1 deletion src/statd/python/yanger/ietf_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,12 @@ def add_services(out):
"pid": d["pid"],
"name": d["identity"],
"status": d["status"],
"description": d["description"]
"description": d["description"],
"statistics": {
"memory-usage": str(d.get("memory", 0)),
"uptime": str(d.get("uptime", 0)),
"restart-count": int(d.get("restarts", 0))
}
}
services.append(entry)

Expand Down
48 changes: 24 additions & 24 deletions test/case/statd/system/cli/show-services
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
NAME STATUS PID DESCRIPTION
udevd running 1185 Device event daemon (udev)
dbus running 2248 D-Bus message bus daemon
confd running 3039 Configuration daemon
netopeer running 3548 NETCONF server
dnsmasq running 2249 DHCP/DNS proxy
tty:hvc0 running 3559 Getty on hvc0
iitod running 2340 LED daemon
klishd running 3560 CLI backend daemon
mdns-alias running 3617 mDNS alias advertiser
mstpd stopped - Spanning Tree daemon
rauc running 3564 Software update service
resolvconf done - Update DNS configuration
statd running 3472 Status daemon
staticd running 3653 Static routing daemon
syslogd running 2241 System log daemon
watchdogd running 2242 System watchdog daemon
zebra running 3587 Zebra routing daemon
mdns running 3616 Avahi mDNS-SD daemon
chronyd running 3618 Chrony NTP v3/v4 daemon
lldpd running 3633 LLDP daemon (IEEE 802.1ab)
nginx running 3635 Web server
rousette running 3636 RESTCONF server
sshd running 3641 OpenSSH daemon
NAME STATUS PID MEM UP RST DESCRIPTION 
udevd running 1185 23m 0 Device event daemon (udev)
dbus running 2248 23m 0 D-Bus message bus daemon
confd running 3039 23m 0 Configuration daemon
netopeer running 3548 23m 0 NETCONF server
dnsmasq running 2249 23m 0 DHCP/DNS proxy
tty:hvc0 running 3559 23m 0 Getty on hvc0
iitod running 2340 23m 0 LED daemon
klishd running 3560 23m 0 CLI backend daemon
mdns-alias running 3617 23m 0 mDNS alias advertiser
mstpd stopped 0 Spanning Tree daemon
rauc running 3564 23m 0 Software update service
resolvconf done 2 Update DNS configuration
statd running 3472 23m 0 Status daemon
staticd running 3653 23m 0 Static routing daemon
syslogd running 2241 23m 0 System log daemon
watchdogd running 2242 23m 0 System watchdog daemon
zebra running 3587 23m 0 Zebra routing daemon
mdns running 3616 23m 0 Avahi mDNS-SD daemon
chronyd running 3618 23m 0 Chrony NTP v3/v4 daemon
lldpd running 3633 23m 0 LLDP daemon (IEEE 802.1ab)
nginx running 3635 23m 0 Web server
rousette running 3636 23m 0 RESTCONF server
sshd running 3641 23m 0 OpenSSH daemon
Loading