mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-07 02:25:21 +03:00
fix: alias long unix-socket NIO paths through the runtime dir
AF_UNIX sun_path caps at 107 bytes and node volume directories (projects/<uuid>/project-files/docker/<uuid>/tmp) exceed it — uBridge rejects the NIO with 'invalid file path size'. When the wiring path is too long, create <XDG_RUNTIME_DIR>/gns3/unixio-<node-id> as a symlink to the real volume directory (same trick as the uBridge control socket) and reference the alias in the uBridge commands; the symlink is removed when uBridge stops. Found during E2E with iol-xe:17-18-02.
This commit is contained in:
parent
e272ad915b
commit
42e26717e6
@ -32,6 +32,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from gns3server.utils.asyncio import wait_for_file_creation
|
||||
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
|
||||
@ -103,6 +104,7 @@ class VendorDockerVM(DockerVM):
|
||||
self._stop_timeout = 60
|
||||
self._unix_socket_nio = False
|
||||
self._unix_socket_dir = "/tmp"
|
||||
self._unix_socket_aliases = set()
|
||||
if self._environment:
|
||||
for _line in self._environment.splitlines():
|
||||
_line = _line.strip().rstrip(",")
|
||||
@ -326,6 +328,50 @@ class VendorDockerVM(DockerVM):
|
||||
"""
|
||||
return os.path.join(self.working_dir, os.path.relpath(self._unix_socket_dir, "/"))
|
||||
|
||||
def _unix_socket_wiring_dir(self):
|
||||
"""
|
||||
Directory to reference in the uBridge unix-NIO commands.
|
||||
|
||||
AF_UNIX paths are capped at 107 bytes (sun_path minus the NUL), and a
|
||||
node volume directory (projects/<uuid>/project-files/docker/<uuid>/tmp)
|
||||
alone is ~120 — uBridge would reject the NIO with "invalid file path
|
||||
size". Long directories are therefore aliased through a symlink in the
|
||||
runtime directory (same trick as the uBridge control socket). uBridge
|
||||
resolves the symlink; the socket files themselves always live in the
|
||||
persisted volume, and the container-side /tmp paths are unaffected.
|
||||
"""
|
||||
|
||||
host_dir = self._unix_socket_host_dir
|
||||
if len(host_dir) + len("/c00.sock") <= 107:
|
||||
return host_dir
|
||||
|
||||
runtime_dir = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir()
|
||||
alias = os.path.join(runtime_dir, "gns3", f"unixio-{self.id}")
|
||||
try:
|
||||
os.makedirs(os.path.dirname(alias), mode=0o700, exist_ok=True)
|
||||
if os.path.lexists(alias):
|
||||
os.unlink(alias)
|
||||
os.symlink(host_dir, alias)
|
||||
except OSError as e:
|
||||
raise DockerError(
|
||||
f"Could not create unix-socket alias '{alias}' for container '{self._name}': {e}"
|
||||
)
|
||||
self._unix_socket_aliases.add(alias)
|
||||
return alias
|
||||
|
||||
async def _stop_ubridge(self):
|
||||
"""
|
||||
Override: also drop the short-path aliases created for the unix-socket
|
||||
NIO (the sockets themselves are removed by the image's agent / the
|
||||
next start's cleanup).
|
||||
"""
|
||||
|
||||
await super()._stop_ubridge()
|
||||
for alias in self._unix_socket_aliases:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(alias)
|
||||
self._unix_socket_aliases.clear()
|
||||
|
||||
async def _add_ubridge_connection(self, nio, adapter_number):
|
||||
"""
|
||||
Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the
|
||||
@ -361,8 +407,9 @@ class VendorDockerVM(DockerVM):
|
||||
self._bridges.add(bridge_name)
|
||||
|
||||
host_dir = self._unix_socket_host_dir
|
||||
local_sock = os.path.join(host_dir, f"c{adapter_number:02d}.sock")
|
||||
remote_sock = os.path.join(host_dir, f"s{adapter_number:02d}.sock")
|
||||
wiring_dir = self._unix_socket_wiring_dir()
|
||||
local_sock = os.path.join(wiring_dir, f"c{adapter_number:02d}.sock")
|
||||
remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock")
|
||||
|
||||
# A c-socket left over from a previous ubridge run would fail its bind.
|
||||
with contextlib.suppress(OSError):
|
||||
|
||||
@ -355,16 +355,17 @@ async def test_add_ubridge_connection_unix_wiring(compute_project, manager):
|
||||
|
||||
sent = [c for c in vm._ubridge_hypervisor.method_calls if "send" in str(c)]
|
||||
flat = "\n".join(str(c) for c in sent)
|
||||
wiring_dir = vm._unix_socket_wiring_dir() # may alias host_dir when long
|
||||
assert call.send("bridge create bridge0") in sent
|
||||
assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(host_dir, "c00.sock")}" '
|
||||
f'"{os.path.join(host_dir, "s00.sock")}"') in sent
|
||||
assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(wiring_dir, "c00.sock")}" '
|
||||
f'"{os.path.join(wiring_dir, "s00.sock")}"') in sent
|
||||
assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat
|
||||
assert "bridge start bridge0" in flat
|
||||
# the TAP/namespace path must not be used at all
|
||||
assert "add_nio_tap" not in flat
|
||||
assert "move_to_ns" not in flat
|
||||
assert "set_mac_addr" not in flat
|
||||
assert vm._ethernet_adapters[0].host_ifc == os.path.join(host_dir, "c00.sock")
|
||||
assert vm._ethernet_adapters[0].host_ifc == os.path.join(wiring_dir, "c00.sock")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -483,5 +484,37 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage
|
||||
|
||||
await vm._add_ubridge_connection(None, 0)
|
||||
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
|
||||
assert f'"{os.path.join(host_dir, "s00.sock")}"' in flat
|
||||
assert f'"{os.path.join(vm._unix_socket_wiring_dir(), "s00.sock")}"' in flat
|
||||
assert "add_nio_tap" not in flat
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_long_socket_path_aliased_through_runtime_dir(compute_project, manager, monkeypatch, tmp_path):
|
||||
"""
|
||||
Node volume paths exceed sun_path's 107 bytes; the wiring must alias them
|
||||
through a short symlink so uBridge accepts the NIO (and remove the alias
|
||||
when uBridge stops).
|
||||
"""
|
||||
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
long_dir = str(tmp_path / "a-very-long-project-directory-name" / ("x" * 40) / "tmp")
|
||||
os.makedirs(long_dir, exist_ok=True)
|
||||
open(os.path.join(long_dir, "s00.sock"), "w").close()
|
||||
runtime_dir = tmp_path / "run"
|
||||
monkeypatch.setenv("XDG_RUNTIME_DIR", str(runtime_dir))
|
||||
|
||||
vm = _make_vm(compute_project, manager)
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
with patch.object(type(vm), "_unix_socket_host_dir", new_callable=PropertyMock,
|
||||
return_value=long_dir):
|
||||
await vm._add_ubridge_connection(None, 0)
|
||||
alias = os.path.join(str(runtime_dir), "gns3", f"unixio-{vm.id}")
|
||||
assert os.path.islink(alias) and os.readlink(alias) == long_dir
|
||||
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
|
||||
assert f'"{os.path.join(alias, "s00.sock")}"' in flat
|
||||
assert long_dir not in flat # uBridge only ever sees the short path
|
||||
|
||||
vm._ubridge_hypervisor.stop = AsyncioMagicMock()
|
||||
await vm._stop_ubridge()
|
||||
assert not os.path.lexists(alias)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user