mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 21:13:00 +02:00
Merge pull request #1327 from kernelkit/container-limits
Container limits
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"
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
From 927acdbd7e19b1a9a0065ebdb88b663ec6626b88 Mon Sep 17 00:00:00 2001
|
||||
From: Joachim Wiberg <troglobit@gmail.com>
|
||||
Date: Tue, 14 Oct 2025 11:49:57 +0200
|
||||
Subject: [PATCH] Increase MAX_ID_LEN to support longer service identifiers
|
||||
Organization: Wires
|
||||
|
||||
Allow service IDs up to 64 characters to support SHA-256 hashes,
|
||||
UUIDs, and other long unique identifiers. This increases memory
|
||||
usage by ~98 bytes per service instance, which is negligible for
|
||||
typical deployments.
|
||||
|
||||
The IDENT column width in initctl output adapts dynamically based
|
||||
on actual ID lengths in use, so short IDs remain unaffected.
|
||||
|
||||
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||
---
|
||||
src/svc.h | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/svc.h b/src/svc.h
|
||||
index 74cc3ca..1a7e6f2 100644
|
||||
--- a/src/svc.h
|
||||
+++ b/src/svc.h
|
||||
@@ -94,7 +94,7 @@ typedef enum {
|
||||
SVC_NOTIFY_S6,
|
||||
} svc_notify_t;
|
||||
|
||||
-#define MAX_ID_LEN 16
|
||||
+#define MAX_ID_LEN 65
|
||||
#define MAX_ARG_LEN 64
|
||||
#define MAX_CMD_LEN 256
|
||||
#define MAX_IDENT_LEN (MAX_ARG_LEN + MAX_ID_LEN + 1)
|
||||
--
|
||||
2.43.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# From https://github.com/troglobit/finit/releases/
|
||||
sha256 7c128119129324050ff7e5b56d0f33fa152fe254d035c0d0c6f72dc75d6786f3 finit-4.14.tar.gz
|
||||
sha256 7ccbcead4e3e6734c81a8c5445f4a27738f19a4ab367d702513a201db9b618c7 finit-4.15-rc1.tar.gz
|
||||
|
||||
# Locally calculated
|
||||
sha256 868cb6c5414933a48db11186042cfe65c87480d326734bc6cf0e4b19b4a2e52a LICENSE
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#
|
||||
################################################################################
|
||||
|
||||
FINIT_VERSION = 4.14
|
||||
FINIT_VERSION = 4.15-rc1
|
||||
FINIT_SITE = https://github.com/troglobit/finit/releases/download/$(FINIT_VERSION)
|
||||
FINIT_LICENSE = MIT
|
||||
FINIT_LICENSE_FILES = LICENSE
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ image::topology.svg[Container basic topology, align=center, scaledwidth=75%]
|
||||
. Create container 'web' from bundled OCI image
|
||||
. Verify container 'web' has started
|
||||
. Verify container 'web' is reachable on http://container-host.local:91
|
||||
. Verify resource constraints and usage are available
|
||||
. Stop container 'web'
|
||||
. Verify container 'web' is stopped
|
||||
. Restart container 'web'
|
||||
|
||||
@@ -11,10 +11,10 @@ import infamy
|
||||
from infamy.util import until, curl
|
||||
|
||||
|
||||
def _verify(server):
|
||||
def _verify(server, silent=False):
|
||||
# TODO: Should really use mDNS here....
|
||||
url = f"http://[{server}]:91/index.html"
|
||||
response = curl(url)
|
||||
response = curl(url, silent=silent)
|
||||
return response is not None and "It works" in response
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ with infamy.Test() as test:
|
||||
"command": "/usr/sbin/httpd -f -v -p 91",
|
||||
"network": {
|
||||
"host": True
|
||||
},
|
||||
"resource-limit": {
|
||||
"memory": 512, # 512 KiB
|
||||
"cpu": 50000 # 50% of one CPU
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -57,7 +61,23 @@ with infamy.Test() as test:
|
||||
until(lambda: c.running(NAME), attempts=60)
|
||||
|
||||
with test.step("Verify container 'web' is reachable on http://container-host.local:91"):
|
||||
until(lambda: _verify(addr), attempts=10)
|
||||
until(lambda: _verify(addr, silent=True), attempts=10)
|
||||
|
||||
with test.step("Verify resource constraints and usage are available"):
|
||||
data = target.get_data("/infix-containers:containers")
|
||||
containers = data.get("containers", {}).get("container", [])
|
||||
web = next((c for c in containers if c["name"] == NAME), None)
|
||||
|
||||
limits = web.get("resource-limit", {})
|
||||
assert limits.get("memory") == 512, "Memory limit not set correctly"
|
||||
assert limits.get("cpu") == 50000, "CPU limit not set correctly"
|
||||
|
||||
rusage = web.get("resource-usage", {})
|
||||
assert rusage is not None, "Resource usage data not available"
|
||||
|
||||
mem_used = rusage.get("memory")
|
||||
assert mem_used is not None, "Memory usage not reported"
|
||||
print(f"Container using {mem_used} KiB memory")
|
||||
|
||||
with test.step("Stop container 'web'"):
|
||||
c = infamy.Container(target)
|
||||
@@ -71,5 +91,5 @@ with infamy.Test() as test:
|
||||
|
||||
with test.step("Verify container 'web' is reachable on http://container-host.local:91"):
|
||||
# Wait for it to restart and respond, or fail
|
||||
until(lambda: _verify(addr), attempts=60)
|
||||
until(lambda: _verify(addr, silent=True), attempts=60)
|
||||
test.succeed()
|
||||
|
||||
+4
-2
@@ -99,12 +99,13 @@ def warn(msg):
|
||||
RST = "\033[0m"
|
||||
print(f"{YELLOW}warn - {msg}{RST}")
|
||||
|
||||
def curl(url, timeout=10):
|
||||
def curl(url, timeout=10, silent=False):
|
||||
"""Fetch a URL and return its response body as a UTF-8 string.
|
||||
|
||||
Args:
|
||||
url (str): The full URL to fetch.
|
||||
timeout (int): Request timeout in seconds.
|
||||
silent (bool): If True, suppress warning on failure (for retry scenarios).
|
||||
|
||||
Returns:
|
||||
str | None: Response body as text, or None if the request failed.
|
||||
@@ -115,5 +116,6 @@ def curl(url, timeout=10):
|
||||
with urllib.request.urlopen(url, timeout=timeout) as response:
|
||||
return response.read().decode('utf-8', errors='replace')
|
||||
except (urllib.error.URLError, ConnectionResetError, UnicodeEncodeError) as e:
|
||||
print(f"[WARN] curl: failed to fetch {url}: {e}")
|
||||
if not silent:
|
||||
print(f"[WARN] curl: failed to fetch {url}: {e}")
|
||||
return ""
|
||||
Reference in New Issue
Block a user