cli: user operational data for show container [name]

Leverage the operational data to present a more human-friendly view of
containers in the CLI.  Replacing the podman script wrapper.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-01-02 20:09:32 +01:00
parent 25a7050c23
commit 162e6d6662
3 changed files with 273 additions and 9 deletions
+11 -6
View File
@@ -186,14 +186,11 @@
</COMMAND>
<COMMAND name="show" help="Show system status and configuration files" mode="switch">
<COMMAND name="container" help="Show container status">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
doas container show |pager
</ACTION>
<SWITCH name="optional" min="0">
<COMMAND name="container" help="Show container status">
<SWITCH name="subcommands" min="0">
<COMMAND name="all" help="Show all containers (default only running)">
<ACTION sym="script">doas container -a show</ACTION>
<ACTION sym="script">show container</ACTION>
</COMMAND>
<COMMAND name="log" help="Show container log (jumps to end), alias to 'show log container'">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
@@ -231,7 +228,15 @@
</ACTION>
</COMMAND>
</SWITCH>
<SWITCH name="optional" min="0" max="1">
<PARAM name="name" ptype="/CONTAINERS" help="Container name for detailed view" />
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
show container $KLISH_PARAM_name |pager
</ACTION>
</COMMAND>
</COMMAND>
</VIEW>
+59
View File
@@ -173,6 +173,64 @@ def services(args: List[str]) -> None:
cli_pretty(data, f"show-services")
def container(args: List[str]) -> None:
"""Handle show container [name]
Arguments:
(none) - Show all containers in table format
name - Show detailed view of specific container
"""
data = run_sysrepocfg("/infix-containers:containers")
if not data:
print("No container data retrieved.")
return
# Fetch interface data for bridge resolution (both table and detailed views)
# Fetch operational interface data
iface_oper = run_sysrepocfg("/ietf-interfaces:interfaces")
# Also fetch config data for veth peer information (not in operational)
try:
result = subprocess.run([
"sysrepocfg", "-f", "json", "-X", "-d", "running", "-x", "/ietf-interfaces:interfaces"
], capture_output=True, text=True, check=True)
iface_config = json.loads(result.stdout)
# Merge config veth peer info into operational data
if iface_oper and iface_config:
oper_ifaces = iface_oper.get('ietf-interfaces:interfaces', {}).get('interface', [])
config_ifaces = iface_config.get('ietf-interfaces:interfaces', {}).get('interface', [])
# Create a map of config interfaces
config_map = {iface['name']: iface for iface in config_ifaces}
# Merge veth peer info from config into operational
for oper_iface in oper_ifaces:
name = oper_iface.get('name')
if name in config_map:
config_iface = config_map[name]
# Add veth peer if it exists in config but not in operational
if 'infix-interfaces:veth' in config_iface and 'infix-interfaces:veth' not in oper_iface:
oper_iface['infix-interfaces:veth'] = config_iface['infix-interfaces:veth']
data.update(iface_oper)
except (subprocess.CalledProcessError, json.JSONDecodeError):
# If config fetch fails, just use operational data
if iface_oper:
data.update(iface_oper)
if RAW_OUTPUT:
print(json.dumps(data, indent=2))
return
if len(args) == 0 or not args[0]:
cli_pretty(data, "show-container")
elif len(args) == 1:
name = args[0]
cli_pretty(data, "show-container-detail", name)
else:
print("Too many arguments provided. Expected: show container [name]")
def bfd(args: List[str]) -> None:
"""Handle show bfd [subcommand] [peer] [brief]
@@ -486,6 +544,7 @@ def execute_command(command: str, args: List[str]):
command_mapping = {
'bfd': bfd,
'boot-order': boot_order,
'container': container,
'dhcp': dhcp,
'hardware': hardware,
'interface': interface,
+203 -3
View File
@@ -579,14 +579,14 @@ class Decore():
return Decore.decorate("43", txt, "49")
@staticmethod
def title(txt, len=None, bold=True):
def title(txt, width=None, bold=True):
"""Print section header with horizontal bar line above it
Args:
txt: The header text to display
len: Length of horizontal bar line (defaults to len(txt))
width: Length of horizontal bar line (defaults to len(txt))
bold: Whether to make the text bold
"""
length = len if len is not None else len(txt)
length = width if width is not None else len(txt)
underline = "" * length
print(underline)
if bold:
@@ -2084,6 +2084,198 @@ def show_hardware(json):
sensor.print()
def resolve_container_network(network, all_ifaces):
"""Resolve container network to bridge names or network type.
Args:
network: Container network dict from operational data
all_ifaces: List of all interface dicts
Returns:
str: Network description (bridge name, "host", interface name, or "-")
"""
if network.get('host'):
return "host"
if 'interface' not in network:
return "-"
interfaces = network.get('interface', [])
if not interfaces:
return "-"
network_names = []
for iface in interfaces:
iface_name = iface.get('name', '')
iface_data = next((i for i in all_ifaces if i.get('name') == iface_name), None)
bridge_found = False
if iface_data:
# Check if this is a VETH with a peer (host-side veth)
veth = iface_data.get('infix-interfaces:veth', {})
peer_name = veth.get('peer')
if peer_name:
# Find the peer interface and check if it's a bridge port
peer_data = next((i for i in all_ifaces if i.get('name') == peer_name), None)
if peer_data:
bridge_port = peer_data.get('infix-interfaces:bridge-port', {})
bridge_name = bridge_port.get('bridge')
if bridge_name:
network_names.append(bridge_name)
bridge_found = True
# If not found via veth peer, try reverse lookup
if not bridge_found:
for host_iface in all_ifaces:
container_net = host_iface.get('infix-interfaces:container-network', {})
containers_list = container_net.get('containers', [])
if iface_name in containers_list:
bridge_port = host_iface.get('infix-interfaces:bridge-port', {})
bridge_name = bridge_port.get('bridge')
if bridge_name:
network_names.append(bridge_name)
bridge_found = True
break
if not bridge_found:
network_names.append(iface_name)
return ', '.join(network_names)
def show_container(json):
"""Display container table view with resource usage."""
containers_data = json.get("infix-containers:containers", {})
containers = containers_data.get("container", [])
if not containers:
print("No containers configured.")
return
# Get interface data for network resolution
all_ifaces_data = json.get('ietf-interfaces:interfaces', {})
all_ifaces = all_ifaces_data.get('interface', [])
# Create table with column definitions
container_table = SimpleTable([
Column('NAME'),
Column('STATUS'),
Column('NETWORK'),
Column('MEMORY (KiB)', 'right'),
Column('CPU%', 'right')
])
for container in containers:
name = container.get("name", "")
status = container.get("status", "")
# Get network information
network = container.get('network', {})
network_str = resolve_container_network(network, all_ifaces)
# Get resource limits and usage
resource_limit = container.get("resource-limit", {})
resource_usage = container.get("resource-usage", {})
mem_limit = resource_limit.get("memory")
mem_usage = resource_usage.get("memory")
cpu_usage = resource_usage.get("cpu", "0.0")
# Format memory display
if mem_usage is not None and mem_limit is not None:
memory_str = f"{mem_usage}/{mem_limit}"
elif mem_usage is not None:
memory_str = str(mem_usage)
else:
memory_str = "-"
# Format CPU display
cpu_str = str(cpu_usage)
# Color code status like in show_services
if status in ('running', 'active'):
status_str = Decore.green(status)
elif status in ('exited', 'stopped', 'created'):
status_str = Decore.yellow(status)
elif status in ('error', 'dead'):
status_str = Decore.red(status)
else:
status_str = status
container_table.row(name, status_str, network_str, memory_str, cpu_str)
container_table.print()
def show_container_detail(json, name):
"""Display detailed container view with full resource information."""
containers_data = json.get("infix-containers:containers", {})
containers = containers_data.get("container", [])
container = None
for c in containers:
if c.get("name") == name:
container = c
break
if not container:
print(f"Container '{name}' not found.")
return
print(f"Name : {container.get('name', '-')}")
print(f"Container ID : {container.get('id', '-')}")
print(f"Image : {container.get('image', '-')}")
print(f"Image ID : {container.get('image-id', '')}")
command = container.get('command')
if command:
print(f"Command : {command}")
# Get network information
network = container.get('network', {})
all_ifaces_data = json.get('ietf-interfaces:interfaces', {})
all_ifaces = all_ifaces_data.get('interface', [])
network_str = resolve_container_network(network, all_ifaces)
print(f"Network : {network_str}")
print(f"Status : {container.get('status', '-')}")
print(f"Running : {'yes' if container.get('running') else 'no'}")
resource_limit = container.get("resource-limit", {})
resource_usage = container.get("resource-usage", {})
pids = resource_usage.get("pids")
if pids is not None:
print(f"Processes : {pids}")
mem_limit = resource_limit.get("memory")
if mem_limit is not None:
print(f"Memory Limit : {mem_limit} KiB")
mem_usage = resource_usage.get("memory")
if mem_usage is not None:
mem_usage_int = int(mem_usage)
if mem_limit:
mem_limit_int = int(mem_limit)
percent = (mem_usage_int / mem_limit_int) * 100
print(f"Memory Usage : {mem_usage_int} KiB ({percent:.1f}%)")
else:
print(f"Memory Usage : {mem_usage_int} KiB")
cpu_limit = resource_limit.get("cpu")
if cpu_limit is not None:
cpu_limit_int = int(cpu_limit)
cores = cpu_limit_int / 1000.0
print(f"CPU Limit : {cpu_limit_int} millicores ({cores:.1f} cores)")
cpu_usage = resource_usage.get("cpu")
if cpu_usage is not None:
print(f"CPU Usage : {cpu_usage}%")
def show_ntp(json):
if not json.get("ietf-system:system-state"):
print("NTP client not enabled.")
@@ -4067,6 +4259,10 @@ def main():
subparsers.add_parser('show-dhcp-server', help='Show DHCP server') \
.add_argument("-s", "--stats", action="store_true", help="Show server statistics")
subparsers.add_parser('show-container', help='Show containers table')
subparsers.add_parser('show-container-detail', help='Show container details') \
.add_argument('name', help='Container name')
subparsers.add_parser('show-hardware', help='Show USB ports')
subparsers.add_parser('show-interfaces', help='Show interfaces') \
@@ -4122,6 +4318,10 @@ def main():
show_bridge_stp(json_data)
elif args.command == "show-dhcp-server":
show_dhcp_server(json_data, args.stats)
elif args.command == "show-container":
show_container(json_data)
elif args.command == "show-container-detail":
show_container_detail(json_data, args.name)
elif args.command == "show-hardware":
show_hardware(json_data)
elif args.command == "show-interfaces":