mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user