mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
confd: add support for container resource limits
Add resource-limit and resource-usage containers to YANG model. Podman, and later conmon, enforce CPU and memory limits in a delegated cgroupsv2 hierarchy managed by Finit. Resource usage is queried from 'podman inspect', which has more nodes than what is currently possible to limit. Requires Finit 4.15, or later. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -5,4 +5,5 @@
|
||||
# 'podman load', must not have a timeout.
|
||||
sysv log:prio:local1,tag:%i kill:30 pid:!/run/container:%i.pid \
|
||||
pre:0,/usr/sbin/container cleanup:0,/usr/sbin/container \
|
||||
cgroup.system,delegate \
|
||||
[2345] <!> :%i container -n %i -- container %i
|
||||
|
||||
@@ -411,10 +411,20 @@ create()
|
||||
logging="--log-driver syslog"
|
||||
fi
|
||||
|
||||
# Build resource limit arguments
|
||||
resource=""
|
||||
if [ -n "$memory" ]; then
|
||||
resource="$resource --memory=$memory"
|
||||
fi
|
||||
if [ -n "$cpu_limit" ]; then
|
||||
resource="$resource --cpu-quota=$cpu_limit"
|
||||
fi
|
||||
|
||||
# When we get here we've already fetched, or pulled, the image
|
||||
args="$args --read-only --replace --quiet --cgroup-parent=containers $caps"
|
||||
args="$args --read-only --replace --quiet $caps"
|
||||
args="$args --cgroups=enabled --cgroupns=host --cgroup-parent=system/container@$name"
|
||||
args="$args --restart=$restart --systemd=false --tz=local $privileged"
|
||||
args="$args $vol $mount $hostname $entrypoint $env $port $logging"
|
||||
args="$args $vol $mount $hostname $entrypoint $env $port $logging $resource"
|
||||
pidfile=/run/container:${name}.pid
|
||||
|
||||
[ -n "$quiet" ] || log "---------------------------------------"
|
||||
@@ -716,6 +726,8 @@ options:
|
||||
--log-path PATH Path for k8s-file log pipe
|
||||
-m, --mount HOST:DEST Bind mount a read-only file inside a container
|
||||
--manual Do not start container automatically after creation
|
||||
--memory BYTES Memory limit in bytes (supports K/M/G suffix)
|
||||
--cpu-limit LIMIT CPU limit in millicores (1000m = 100% of 1 core)
|
||||
-n, --name NAME Alternative way of supplying name to start/stop/restart
|
||||
--privileged Give container extended privileges
|
||||
-p, --publish PORT Publish ports when creating container
|
||||
@@ -836,6 +848,14 @@ while [ "$1" != "" ]; do
|
||||
--manual)
|
||||
manual=true
|
||||
;;
|
||||
--memory)
|
||||
shift
|
||||
memory="$1"
|
||||
;;
|
||||
--cpu-limit)
|
||||
shift
|
||||
cpu_limit="$1"
|
||||
;;
|
||||
-n | --name)
|
||||
shift
|
||||
name="$1"
|
||||
|
||||
@@ -294,6 +294,25 @@ static int add(const char *name, struct lyd_node *cif)
|
||||
fprintf(fp, " --checksum sha512:%s", string);
|
||||
}
|
||||
|
||||
/* Add resource limits for Podman to enforce via cgroups */
|
||||
node = lydx_get_descendant(lyd_child(cif), "resource-limit", NULL);
|
||||
if (node) {
|
||||
struct lyd_node *mem_node, *cpu_node;
|
||||
|
||||
/* Memory limit in KiB, Podman accepts with 'k' suffix */
|
||||
mem_node = lydx_get_descendant(lyd_child(node), "memory", NULL);
|
||||
if (mem_node)
|
||||
fprintf(fp, " --memory %sk", lyd_get_value(mem_node));
|
||||
|
||||
/* CPU limit in millicores, convert to quota (microseconds per 100ms) */
|
||||
cpu_node = lydx_get_descendant(lyd_child(node), "cpu", NULL);
|
||||
if (cpu_node) {
|
||||
uint32_t millicores = strtoul(lyd_get_value(cpu_node), NULL, 10);
|
||||
uint32_t quota = millicores * 100; /* 1000m → 100000µs, 2000m → 200000µs */
|
||||
fprintf(fp, " --cpu-limit %u", quota);
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(fp, " create %s %s", name, image);
|
||||
|
||||
if ((string = lydx_get_cattr(cif, "command")))
|
||||
|
||||
@@ -22,6 +22,13 @@ module infix-containers {
|
||||
prefix infix-sys;
|
||||
}
|
||||
|
||||
revision 2025-12-09 {
|
||||
description "Add resource management:
|
||||
- Add resource-limit container with memory and cpu configuration.
|
||||
- Add resource-usage operational data for live resource usage statistics.";
|
||||
reference "internal";
|
||||
}
|
||||
|
||||
revision 2025-10-12 {
|
||||
description "Two major changes:
|
||||
- Add dedicated 'ident' type for container and volume names.
|
||||
@@ -341,6 +348,86 @@ module infix-containers {
|
||||
}
|
||||
}
|
||||
|
||||
container resource-limit {
|
||||
description "Resource limits for the container.";
|
||||
|
||||
leaf memory {
|
||||
description "Maximum memory limit in kibibytes, default: unlimited.";
|
||||
type uint64;
|
||||
units "KiB";
|
||||
}
|
||||
|
||||
leaf cpu {
|
||||
description "CPU limit in millicores, default: unlimited.
|
||||
|
||||
Millicores represent thousandths of a CPU core:
|
||||
500 = 0.5 cores (50% of one core)
|
||||
1000 = 1.0 cores (one full core)
|
||||
2000 = 2.0 cores (two full cores)
|
||||
3500 = 3.5 cores
|
||||
|
||||
This is converted to cgroup cpu.quota internally.";
|
||||
type uint32;
|
||||
units "millicores";
|
||||
}
|
||||
}
|
||||
|
||||
container resource-usage {
|
||||
description "Runtime container resource usage statistics.";
|
||||
config false;
|
||||
|
||||
leaf memory {
|
||||
description "Used memory in kibibytes.";
|
||||
type uint64;
|
||||
units "KiB";
|
||||
}
|
||||
|
||||
leaf cpu {
|
||||
description "CPU usage percentage.";
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "percent";
|
||||
}
|
||||
|
||||
container block-io {
|
||||
description "Block I/O statistics";
|
||||
|
||||
leaf read {
|
||||
description "Block I/O read in kibibytes.";
|
||||
type uint64;
|
||||
units "KiB";
|
||||
}
|
||||
|
||||
leaf write {
|
||||
description "Block I/O write in kibibytes.";
|
||||
type uint64;
|
||||
units "KiB";
|
||||
}
|
||||
}
|
||||
|
||||
container net-io {
|
||||
description "Network I/O statistics";
|
||||
|
||||
leaf received {
|
||||
description "Network I/O received in kibibytes.";
|
||||
type uint64;
|
||||
units "KiB";
|
||||
}
|
||||
|
||||
leaf sent {
|
||||
description "Network I/O sent in kibibytes.";
|
||||
type uint64;
|
||||
units "KiB";
|
||||
}
|
||||
}
|
||||
|
||||
leaf pids {
|
||||
description "Number of processes/threads.";
|
||||
type uint32;
|
||||
}
|
||||
}
|
||||
|
||||
list mount {
|
||||
description "Files, content, and directories to mount inside container.";
|
||||
key name;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- sh -*-
|
||||
MODULES=(
|
||||
"infix-interfaces -e containers"
|
||||
"infix-containers@2025-10-12.yang"
|
||||
"infix-containers@2025-12-09.yang"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
"""Operational data provider for infix-containers YANG model.
|
||||
|
||||
Collects container status, network info, resource limits from cgroups,
|
||||
and runtime statistics via podman commands.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
|
||||
from .common import LOG
|
||||
from .host import HOST
|
||||
|
||||
@@ -46,6 +54,165 @@ def network(ps, inspect):
|
||||
return net
|
||||
|
||||
|
||||
def parse_size_kib(size_str):
|
||||
"""Parse size string like '1.5MB' or '512kB' to KiB (kibibytes)."""
|
||||
if not size_str:
|
||||
return 0
|
||||
|
||||
size_str = size_str.strip().upper()
|
||||
|
||||
# Extract numeric part and unit
|
||||
match = re.match(r'([0-9.]+)\s*([KMGT]?I?B)?', size_str)
|
||||
if not match:
|
||||
return 0
|
||||
|
||||
value = float(match.group(1))
|
||||
unit = match.group(2) if match.group(2) else 'B'
|
||||
|
||||
# Convert to KiB (kibibytes)
|
||||
multipliers = {
|
||||
'B': 1/1024,
|
||||
'KB': 1000/1024, 'KIB': 1,
|
||||
'MB': (1000**2)/1024, 'MIB': 1024,
|
||||
'GB': (1000**3)/1024, 'GIB': 1024**2,
|
||||
'TB': (1000**4)/1024, 'TIB': 1024**3,
|
||||
}
|
||||
|
||||
return int(value * multipliers.get(unit, 1))
|
||||
|
||||
|
||||
def parse_cgroup_memory(mem_str):
|
||||
"""Parse cgroup memory.max value (bytes) to KiB."""
|
||||
if not mem_str or mem_str == "max":
|
||||
return 0
|
||||
try:
|
||||
mem_bytes = int(mem_str)
|
||||
return mem_bytes // 1024
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def parse_cgroup_cpu(cpu_str):
|
||||
"""Parse cgroup cpu.max value to millicores."""
|
||||
if not cpu_str:
|
||||
return 0
|
||||
parts = cpu_str.split()
|
||||
if len(parts) != 2 or parts[0] == "max":
|
||||
return 0
|
||||
try:
|
||||
quota = int(parts[0])
|
||||
period = int(parts[1])
|
||||
# Convert to millicores: (quota/period) * 1000
|
||||
return (quota * 1000) // period
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def read_cgroup_limits(inspect):
|
||||
"""Read resource limits from cgroup files for a container."""
|
||||
if not inspect or not isinstance(inspect, dict):
|
||||
return None
|
||||
|
||||
cgroup_path = inspect.get("State", {}).get("CgroupPath")
|
||||
if not cgroup_path:
|
||||
return None
|
||||
|
||||
cgroup_base = f"/sys/fs/cgroup{cgroup_path}"
|
||||
mem_val = 0
|
||||
cpu_val = 0
|
||||
|
||||
try:
|
||||
# Read memory limit (in bytes, convert to KiB)
|
||||
mem_max_path = os.path.join(cgroup_base, "memory.max")
|
||||
if os.path.exists(mem_max_path):
|
||||
with open(mem_max_path, 'r') as f:
|
||||
mem_str = f.read().strip()
|
||||
mem_val = parse_cgroup_memory(mem_str)
|
||||
|
||||
# Read CPU limit (quota and period in microseconds, convert to millicores)
|
||||
cpu_max_path = os.path.join(cgroup_base, "cpu.max")
|
||||
if os.path.exists(cpu_max_path):
|
||||
with open(cpu_max_path, 'r') as f:
|
||||
cpu_str = f.read().strip()
|
||||
cpu_val = parse_cgroup_cpu(cpu_str)
|
||||
except Exception as e:
|
||||
LOG.error(f"failed reading cgroup limits: {e}")
|
||||
return None
|
||||
|
||||
if mem_val > 0 or cpu_val > 0:
|
||||
result = {}
|
||||
if mem_val > 0:
|
||||
result["memory"] = f"{mem_val}"
|
||||
if cpu_val > 0:
|
||||
result["cpu"] = cpu_val
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resource_stats(name):
|
||||
"""Get resource usage stats for a running container using podman stats."""
|
||||
cmd = ['podman', 'stats', '--no-stream', '--format', 'json', '--no-reset', name]
|
||||
try:
|
||||
stats = HOST.run_json(cmd, default=[])
|
||||
if not stats or len(stats) == 0:
|
||||
return None
|
||||
|
||||
stat = stats[0]
|
||||
rusage = {}
|
||||
|
||||
# Memory usage - parse used memory, convert to KiB
|
||||
# Encode as string for uint64 compatibility
|
||||
mem_usage_str = stat.get("mem_usage", "")
|
||||
if "/" in mem_usage_str:
|
||||
mem_used_str = mem_usage_str.split("/")[0].strip()
|
||||
mem_used_kib = parse_size_kib(mem_used_str)
|
||||
rusage["memory"] = f"{mem_used_kib}"
|
||||
|
||||
# CPU percentage - format as decimal64 with 2 fractional digits
|
||||
cpu_perc = stat.get("cpu_percent", "0%").rstrip("%")
|
||||
try:
|
||||
rusage["cpu"] = "{:.2f}".format(float(cpu_perc))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
block_io = stat.get("block_io", "0B / 0B")
|
||||
if "/" in block_io:
|
||||
block_read_str, block_write_str = block_io.split("/")
|
||||
block_read_kib = parse_size_kib(block_read_str.strip())
|
||||
block_write_kib = parse_size_kib(block_write_str.strip())
|
||||
|
||||
rusage["block-io"] = {}
|
||||
if block_read_kib > 0:
|
||||
rusage["block-io"]["read"] = f"{block_read_kib}"
|
||||
if block_write_kib > 0:
|
||||
rusage["block-io"]["write"] = f"{block_write_kib}"
|
||||
|
||||
net_io = stat.get("net_io", "0B / 0B")
|
||||
if "/" in net_io:
|
||||
net_rx_str, net_tx_str = net_io.split("/")
|
||||
net_rx_kib = parse_size_kib(net_rx_str.strip())
|
||||
net_tx_kib = parse_size_kib(net_tx_str.strip())
|
||||
|
||||
rusage["net-io"] = {}
|
||||
if net_rx_kib > 0:
|
||||
rusage["net-io"]["received"] = f"{net_rx_kib}"
|
||||
if net_tx_kib > 0:
|
||||
rusage["net-io"]["sent"] = f"{net_tx_kib}"
|
||||
|
||||
pids = stat.get("pids", "0")
|
||||
try:
|
||||
rusage["pids"] = int(pids)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return rusage if rusage else None
|
||||
|
||||
except Exception as e:
|
||||
LOG.error(f"failed getting stats for {name}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def container(ps):
|
||||
out = {
|
||||
"name": ps["Names"][0],
|
||||
@@ -70,6 +237,15 @@ def container(ps):
|
||||
if net:
|
||||
out["network"] = net
|
||||
|
||||
limits = read_cgroup_limits(inspect)
|
||||
if limits:
|
||||
out["resource-limit"] = limits
|
||||
|
||||
if out["running"]:
|
||||
rusage = resource_stats(out["name"])
|
||||
if rusage:
|
||||
out["resource-usage"] = rusage
|
||||
|
||||
return out
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user