mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-28 11:12:26 +02: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._connector = None
|
||||||
self._session = None
|
self._session = None
|
||||||
self._api_version = DOCKER_MINIMUM_API_VERSION
|
self._api_version = DOCKER_MINIMUM_API_VERSION
|
||||||
|
self._host_checked = False
|
||||||
|
|
||||||
def _select_node_class(self, **kwargs):
|
def _select_node_class(self, **kwargs):
|
||||||
"""Select the node class based on console_type."""
|
"""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.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))
|
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):
|
def connector(self):
|
||||||
|
|
||||||
|
|||||||
@ -364,3 +364,54 @@ async def test_install_busybox_no_executables():
|
|||||||
dst_dir = Docker.resources_path()
|
dst_dir = Docker.resources_path()
|
||||||
await Docker.install_busybox(dst_dir)
|
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"
|
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