docker: inject /dev/shm size and host devices via HostConfig from env

Heavy NOS containers (e.g. Cisco XRd) need /dev/shm larger than Docker's
64 MB default and host device nodes such as /dev/fuse. Add two opt-in
environment variables, consumed host-side and applied as native Docker
HostConfig keys at create time:

  GNS3_SHM_SIZE (MB)  -> HostConfig.ShmSize (bytes)
  GNS3_DEVICES        -> HostConfig.Devices in `docker run --device` syntax
                         (host[:container[:perm]]; Docker resolves major/minor
                         from the host node itself)

Native HostConfig (rather than remount/mknod inside init.sh) is used so this
works for vendor NOS nodes that skip init.sh (console_type=docker_exec) --
the path XRd must take, since GNS3's init.sh wrapper crashes XRd's glibc
loader. It applies whether or not init.sh runs, needs no schema/API/UI
change (reuses the `environment` field), and only takes effect when the vars
are set, so ordinary nodes keep default Docker behaviour.

GNS3_-prefixed user env vars stay dropped from the container environment
(only consumed here host-side), keeping GNS3-injected vars safe.
This commit is contained in:
YueGuobin 2026-08-13 22:39:48 +08:00
parent f65d9b17c6
commit 86d30f34b4
No known key found for this signature in database
2 changed files with 86 additions and 0 deletions

View File

@ -491,6 +491,25 @@ class DockerVM(BaseNode):
"Entrypoint": image_infos.get("Config", {"Entrypoint": []}).get("Entrypoint"),
}
# Optional /dev/shm size and host device mappings requested through the
# environment (GNS3_SHM_SIZE in MB, GNS3_DEVICES). These are native Docker
# HostConfig keys applied at create time, so they work whether or not
# init.sh runs -- heavy NOS containers such as Cisco XRd (which skips
# init.sh via the vendor/docker_exec path) rely on them. Only injected
# when set, so ordinary nodes keep the default Docker behaviour.
if self._environment:
for line in self._environment.splitlines():
line = line.strip()
if line.startswith("GNS3_SHM_SIZE="):
try:
params["HostConfig"]["ShmSize"] = int(line.split("=", 1)[1].strip()) * (1024 * 1024)
except ValueError:
pass
elif line.startswith("GNS3_DEVICES="):
devices = self._format_devices(line.split("=", 1)[1])
if devices:
params["HostConfig"]["Devices"] = devices
if params["Entrypoint"] is None:
params["Entrypoint"] = []
if self._start_command:
@ -625,6 +644,37 @@ class DockerVM(BaseNode):
raise DockerError(f"Can't apply `ExtraHosts`, wrong format: {extra_hosts}")
return "\n".join([f"{h[1]}\t{h[0]}" for h in hosts])
def _format_devices(self, devices_value):
"""
Parse a GNS3_DEVICES value into Docker HostConfig Devices entries.
Mirrors `docker run --device`: items are whitespace/comma-separated and
each is ``host[:container[:permissions]]`` (e.g. /dev/fuse,
/dev/fuse:/dev/fuse:rwm). Docker resolves type/major/minor from the host
node itself, so the device must exist on the host -- the host-readiness
check warns when /dev/fuse is missing (load the fuse module).
"""
formatted = []
for raw in devices_value.replace(",", " ").split():
parts = raw.split(":")
if len(parts) == 1:
on_host = in_container = parts[0]
permissions = "rwm"
elif len(parts) == 2:
on_host, in_container = parts
permissions = "rwm"
elif len(parts) == 3:
on_host, in_container, permissions = parts
else:
continue
formatted.append({
"PathOnHost": on_host,
"PathInContainer": in_container,
"CgroupPermissions": permissions,
})
return formatted
async def update(self):
"""
Destroy and recreate the container with the new settings

View File

@ -271,6 +271,42 @@ async def test_create_with_extra_hosts(compute_project, manager):
assert "GNS3_EXTRA_HOSTS=199.199.199.1\ttest\n199.199.199.1\ttest2" in called_kwargs["data"]["Env"]
assert vm._extra_hosts == extra_hosts
@pytest.mark.asyncio
async def test_create_applies_env_host_config(compute_project, manager):
"""
GNS3_SHM_SIZE / GNS3_DEVICES are applied as native Docker HostConfig keys
(ShmSize, Devices) at create time -- not forwarded as container env vars --
so they work even for vendor nodes that skip init.sh. Other GNS3_-prefixed
vars stay dropped from the container environment.
"""
environment = (
"GNS3_SHM_SIZE=1024\n"
"GNS3_DEVICES=/dev/fuse\n"
"GNS3_EVIL=should-be-dropped\n" # GNS3_ -> never forwarded as env
"FOO=bar" # normal var -> forwarded
)
response = {"Id": "e90e34656806", "Warnings": []}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", environment=environment)
await vm.create()
data = mock.call_args[1]["data"]
host_config = data["HostConfig"]
assert host_config["ShmSize"] == 1024 * 1024 * 1024
assert host_config["Devices"] == [
{"PathOnHost": "/dev/fuse", "PathInContainer": "/dev/fuse", "CgroupPermissions": "rwm"}
]
env = data["Env"]
assert "FOO=bar" in env
assert not any(
e.startswith(("GNS3_SHM_SIZE=", "GNS3_DEVICES=", "GNS3_EVIL="))
for e in env
), "GNS3_ user vars must not leak into the container environment"
@pytest.mark.asyncio
async def test_create_with_colon_in_project_name(compute_project, manager):