docker: warn about low inotify/file-max and missing FUSE at connect

Add a read-only _check_host_readiness() that runs once after the Docker
daemon connection is established. It reads /proc/sys inotify/file-max
limits and /proc/filesystems (for FUSE), and logs a warning with the exact
commands to fix when they are too low for heavy containers -- XRd wants
~4000 inotify instances per node against a stock default of 128.

The server runs unprivileged (only the setuid ubridge helper has root), so
it can only check, not set; the warning tells the admin exactly what to
raise once. Stays silent when the limits are already sufficient.
This commit is contained in:
YueGuobin 2026-08-13 22:39:48 +08:00
parent 86d30f34b4
commit 2bee34031c
No known key found for this signature in database
2 changed files with 106 additions and 0 deletions

View File

@ -59,6 +59,7 @@ class Docker(BaseManager):
self._connector = None
self._session = None
self._api_version = DOCKER_MINIMUM_API_VERSION
self._host_checked = False
def _select_node_class(self, **kwargs):
"""Select the node class based on console_type."""
@ -160,6 +161,60 @@ class Docker(BaseManager):
log.warning("Using Docker client with the minimum API version {}".format(self._api_version))
log.info("Connected to Docker daemon version {} using API version {}".format(version, self._api_version))
self._check_host_readiness()
def _check_host_readiness(self):
"""
Best-effort, read-only check of kernel settings that heavy NOS containers
(e.g. Cisco XRd) need. The server runs unprivileged (only the setuid
ubridge helper gets root), so we cannot raise these limits ourselves --
we only warn, with the exact commands to fix, when they are too low or
when FUSE support is missing. Runs at most once per process.
"""
if self._host_checked:
return
self._host_checked = True
# Thresholds recommended for running several heavy containers (sized for
# ~15 XRd-style nodes). Raising them is harmless; the stock Linux defaults
# (e.g. max_user_instances=128) are far too low and break such images.
thresholds = {
"fs.inotify.max_user_instances": 64000,
"fs.inotify.max_user_watches": 524288,
"fs.file-max": 1000000,
}
low = []
for key, minimum in thresholds.items():
try:
with open(f"/proc/sys/{key.replace('.', '/')}") as f:
current = int(f.read().strip())
except (OSError, ValueError):
return # Not Linux or unreadable -- nothing to check.
if current < minimum:
low.append((key, current, minimum))
fuse_supported = False
try:
with open("/proc/filesystems") as f:
filesystems = {parts[-1] for parts in (line.split() for line in f) if parts}
fuse_supported = "fuse" in filesystems or "fuseblk" in filesystems
except OSError:
pass
if low:
details = ", ".join(f"{k}={c} (need >={m})" for k, c, m in low)
raise_cmd = " ".join(f"{k}={m}" for k, _, m in low)
log.warning(
f"Low kernel limits for heavy Docker containers ({details}). "
f"Some NOS images (e.g. Cisco XRd) may fail to start. Raise once: "
f"'sudo sysctl -w {raise_cmd}' and persist it under /etc/sysctl.d/."
)
if not fuse_supported:
log.warning(
"FUSE filesystem support is not available in the kernel. "
"Containers that need it (e.g. Cisco XRd) will fail. Load it: 'sudo modprobe fuse'."
)
def connector(self):

View File

@ -364,3 +364,54 @@ async def test_install_busybox_no_executables():
dst_dir = Docker.resources_path()
await Docker.install_busybox(dst_dir)
assert str(e.value) == "No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH"
@pytest.mark.asyncio
async def test_check_host_readiness_warns_when_low(caplog):
import logging
from io import StringIO
docker = Docker()
files = {
"/proc/sys/fs/inotify/max_user_instances": "128",
"/proc/sys/fs/inotify/max_user_watches": "8192",
"/proc/sys/fs/file-max": "100000",
"/proc/filesystems": "nodev ext4\nnodev tmpfs\n",
}
def fake_open(path, *args, **kwargs):
return StringIO(files[path])
with patch("builtins.open", side_effect=fake_open):
with caplog.at_level(logging.WARNING, logger="gns3server.compute.docker"):
docker._check_host_readiness()
message = " ".join(r.message for r in caplog.records)
assert "max_user_instances=128" in message
assert "sudo sysctl -w" in message
assert "modprobe fuse" in message # FUSE missing -> warned
@pytest.mark.asyncio
async def test_check_host_readiness_silent_when_ok(caplog):
import logging
from io import StringIO
docker = Docker()
files = {
"/proc/sys/fs/inotify/max_user_instances": "64000",
"/proc/sys/fs/inotify/max_user_watches": "524288",
"/proc/sys/fs/file-max": "1000000",
"/proc/filesystems": "nodev ext4\nnodev fuse\n",
}
def fake_open(path, *args, **kwargs):
return StringIO(files[path])
with patch("builtins.open", side_effect=fake_open):
with caplog.at_level(logging.WARNING, logger="gns3server.compute.docker"):
docker._check_host_readiness()
assert not [r for r in caplog.records if r.levelno == logging.WARNING]