mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
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:
parent
86d30f34b4
commit
2bee34031c
@ -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):
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user