diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 804039216..0f98363ad 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -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 diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index c9e701306..8938ceaa9 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -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):