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