mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-27 22:30:11 +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 logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from gns3server.utils.asyncio import wait_for_file_creation
|
from gns3server.utils.asyncio import wait_for_file_creation
|
||||||
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
|
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
|
||||||
@ -103,6 +104,7 @@ class VendorDockerVM(DockerVM):
|
|||||||
self._stop_timeout = 60
|
self._stop_timeout = 60
|
||||||
self._unix_socket_nio = False
|
self._unix_socket_nio = False
|
||||||
self._unix_socket_dir = "/tmp"
|
self._unix_socket_dir = "/tmp"
|
||||||
|
self._unix_socket_aliases = set()
|
||||||
if self._environment:
|
if self._environment:
|
||||||
for _line in self._environment.splitlines():
|
for _line in self._environment.splitlines():
|
||||||
_line = _line.strip().rstrip(",")
|
_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, "/"))
|
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):
|
async def _add_ubridge_connection(self, nio, adapter_number):
|
||||||
"""
|
"""
|
||||||
Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the
|
Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the
|
||||||
@ -361,8 +407,9 @@ class VendorDockerVM(DockerVM):
|
|||||||
self._bridges.add(bridge_name)
|
self._bridges.add(bridge_name)
|
||||||
|
|
||||||
host_dir = self._unix_socket_host_dir
|
host_dir = self._unix_socket_host_dir
|
||||||
local_sock = os.path.join(host_dir, f"c{adapter_number:02d}.sock")
|
wiring_dir = self._unix_socket_wiring_dir()
|
||||||
remote_sock = os.path.join(host_dir, f"s{adapter_number:02d}.sock")
|
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.
|
# A c-socket left over from a previous ubridge run would fail its bind.
|
||||||
with contextlib.suppress(OSError):
|
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)]
|
sent = [c for c in vm._ubridge_hypervisor.method_calls if "send" in str(c)]
|
||||||
flat = "\n".join(str(c) for c in sent)
|
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("bridge create bridge0") in sent
|
||||||
assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(host_dir, "c00.sock")}" '
|
assert call.send(f'bridge add_nio_unix bridge0 "{os.path.join(wiring_dir, "c00.sock")}" '
|
||||||
f'"{os.path.join(host_dir, "s00.sock")}"') in sent
|
f'"{os.path.join(wiring_dir, "s00.sock")}"') in sent
|
||||||
assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat
|
assert "add_nio_udp bridge0 4242 127.0.0.1 4343" in flat
|
||||||
assert "bridge start bridge0" in flat
|
assert "bridge start bridge0" in flat
|
||||||
# the TAP/namespace path must not be used at all
|
# the TAP/namespace path must not be used at all
|
||||||
assert "add_nio_tap" not in flat
|
assert "add_nio_tap" not in flat
|
||||||
assert "move_to_ns" not in flat
|
assert "move_to_ns" not in flat
|
||||||
assert "set_mac_addr" 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
|
@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)
|
await vm._add_ubridge_connection(None, 0)
|
||||||
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
|
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
|
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