mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-24 03:42:22 +03:00
refactor: reach unix-socket NIOs through the container root in /proc
The unix-socket NIO no longer requires the socket directory to be a persisted volume: uBridge references the sockets through /proc/<container-pid>/root<dir>/..., which is always well under the 107-byte sun_path cap (a project directory path alone exceeds it) and leaves the sockets ephemeral in the container's own filesystem. This replaces the runtime-dir symlink alias and its cleanup, and the mount-time volume enforcement. IOLDockerVM now persists only /tmp/run (the IOL working directory: startup-config + NVRAM) as a nested bind instead of the whole /tmp; stale socket cleanup is gone with it, as containers are recreated on every start.
This commit is contained in:
parent
42e26717e6
commit
165f271bb8
@ -36,10 +36,11 @@ graph LR
|
|||||||
IOL -->|"netio bus /tmp/netio<uid>/"| NETIOMUX --> SOCKETS
|
IOL -->|"netio bus /tmp/netio<uid>/"| NETIOMUX --> SOCKETS
|
||||||
end
|
end
|
||||||
subgraph Host
|
subgraph Host
|
||||||
UBRIDGE["uBridge bridgeN<br/>add_nio_unix c00.sock s00.sock<br/>+ add_nio_udp (topology)"]
|
UBRIDGE["uBridge bridgeN<br/>add_nio_unix /proc/PID/root/tmp/cNN.sock …<br/>+ add_nio_udp (topology)"]
|
||||||
VOL["project-files/docker/<node>/tmp"]
|
VOL["project-files/docker/<node>/config<br/>(+ tmp/run nested bind)"]
|
||||||
end
|
end
|
||||||
SOCKETS <-->|"raw Ethernet frames<br/>(bind volume dir)"| VOL <--> UBRIDGE
|
SOCKETS <-->|"raw Ethernet frames via<br/>the container root in /proc"| UBRIDGE
|
||||||
|
UBRIDGE -.->|"iol-config.json +<br/>persistent /tmp/run"| VOL
|
||||||
```
|
```
|
||||||
|
|
||||||
* **Console**: the runner muxes the IOS console onto PID 1 stdio (`-stdio`
|
* **Console**: the runner muxes the IOS console onto PID 1 stdio (`-stdio`
|
||||||
@ -47,21 +48,27 @@ graph LR
|
|||||||
`docker_exec` needed. The runner requires a TTY, which GNS3 always
|
`docker_exec` needed. The runner requires a TTY, which GNS3 always
|
||||||
allocates; without one the runner exits (`inappropriate ioctl for device`).
|
allocates; without one the runner exits (`inappropriate ioctl for device`).
|
||||||
* **Networking**: the runner does not touch the container's network
|
* **Networking**: the runner does not touch the container's network
|
||||||
namespace. Per interface N it creates, inside `/tmp`: a receive socket
|
namespace. Per interface N it creates, inside the container's `/tmp`: a
|
||||||
`s%02d.sock` (frames sent there are injected into guest interface N) and a
|
receive socket `s%02d.sock` (frames sent there are injected into guest
|
||||||
send-to path `c%02d.sock` (whoever binds it receives the guest's frames).
|
interface N) and a send-to path `c%02d.sock` (whoever binds it receives
|
||||||
Frames are **raw Ethernet**, one datagram per frame. Because `/tmp` is a
|
the guest's frames). Frames are **raw Ethernet**, one datagram per frame.
|
||||||
persisted volume (bind-mounted from the node directory), uBridge can bind
|
uBridge reaches both through the container's root in `/proc`
|
||||||
`cNN.sock` and send to `sNN.sock` on the host.
|
(`/proc/<container-pid>/root/tmp/…`) — a short path (AF_UNIX names are
|
||||||
|
capped at 107 bytes, which a project directory path alone would exceed)
|
||||||
|
that requires no volume mount and leaves the sockets ephemeral in the
|
||||||
|
container, fresh in every container GNS3 creates.
|
||||||
* **Licensing**: the image ships a self-consistent `/etc/hostid` + `.iourc`
|
* **Licensing**: the image ships a self-consistent `/etc/hostid` + `.iourc`
|
||||||
pair, and the runner regenerates the license from the host ID at boot —
|
pair, and the runner regenerates the license from the host ID at boot —
|
||||||
nothing to configure.
|
nothing to configure.
|
||||||
* **Persistence**: `/tmp/run/` inside the volume holds the NETMAP, the
|
* **Persistence**: `/tmp/run` (the IOL working directory) holds the NETMAP,
|
||||||
startup-config (`config`, plain IOS format) and NVRAM (`nvram_00001`), so
|
the startup-config (`config`, plain IOS format) and NVRAM (`nvram_00001`).
|
||||||
the router's configuration survives stop/start and container recreation.
|
It is the only `/tmp` path that needs to survive: GNS3 bind-mounts the
|
||||||
The generated config maps the runner to the server's uid/gid
|
node directory's `tmp/run/` at `/tmp/run`, so the router's configuration
|
||||||
(`user-id`/`group-id`), which is also what makes the `/tmp` sockets
|
survives stop/start and container recreation while everything else in
|
||||||
reachable by uBridge and all volume files owned by the server user (no
|
`/tmp` stays ephemeral. The generated config maps the runner to the
|
||||||
|
server's uid/gid (`user-id`/`group-id`), so all files it creates are owned
|
||||||
|
by the server user — which is also what lets an unprivileged uBridge
|
||||||
|
traverse `/proc/<pid>/root` to reach the container's sockets (no
|
||||||
permission-fix pass needed).
|
permission-fix pass needed).
|
||||||
|
|
||||||
## Template
|
## Template
|
||||||
@ -76,21 +83,21 @@ graph LR
|
|||||||
"adapters": 4,
|
"adapters": 4,
|
||||||
"console_type": "telnet",
|
"console_type": "telnet",
|
||||||
"environment": "GNS3_IOL_RUNNER=1",
|
"environment": "GNS3_IOL_RUNNER=1",
|
||||||
"extra_volumes": ["/config", "/tmp"],
|
"extra_volumes": ["/config"],
|
||||||
"memory": 2560
|
"memory": 2560
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`/config` and `/tmp` are auto-added even if omitted; listing them keeps the
|
`/config` and `/tmp/run` are auto-added even if omitted; listing `/config`
|
||||||
template self-documenting.
|
keeps the template self-documenting.
|
||||||
|
|
||||||
## Server mechanisms
|
## Server mechanisms
|
||||||
|
|
||||||
| Mechanism | Where | What it does |
|
| Mechanism | Where | What it does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GNS3_UNIX_SOCKET_NIO=1` | `VendorDockerVM` | `_add_ubridge_connection` override: `bridge create` + `bridge add_nio_unix <dir>/c{N:02d}.sock <dir>/s{N:02d}.sock` instead of TAP + `docker move_to_ns`. No TAP allocation, no `set_mac_addr`, namespace untouched. Fails node creation if the socket dir is not a persisted volume. |
|
| `GNS3_UNIX_SOCKET_NIO=1` | `VendorDockerVM` | `_add_ubridge_connection` override: `bridge create` + `bridge add_nio_unix /proc/<pid>/root<dir>/c{N:02d}.sock /proc/<pid>/root<dir>/s{N:02d}.sock` instead of TAP + `docker move_to_ns`. No TAP allocation, no `set_mac_addr`, namespace untouched, no volume required. |
|
||||||
| `GNS3_UNIX_SOCKET_DIR=<dir>` | `VendorDockerVM` | Socket directory (default `/tmp`). Any image whose agent exposes the `s%02d`/`c%02d` datagram pairs can use this without the IOL specifics. |
|
| `GNS3_UNIX_SOCKET_DIR=<dir>` | `VendorDockerVM` | In-container socket directory (default `/tmp`). Any image whose agent exposes the `s%02d`/`c%02d` datagram pairs can use this without the IOL specifics. |
|
||||||
| `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the two volumes; on every start writes `<node>/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048), creates `<node>/tmp/run/` (the IOL process dies without it) and removes stale `s/c??.sock` + `netio*` left by an unclean kill (`tmp/run` is never touched). |
|
| `GNS3_IOL_RUNNER=1` | `IOLDockerVM` (selected in the manager) | Forces skip-init + unix-socket NIO + the `/config` and `/tmp/run` volumes; on every start writes `<node>/config/iol-config.json` (`num-eth` = adapter count, `num-serial` = 0, memory from `GNS3_IOL_MEMORY`, default 2048) and creates `<node>/tmp/run/` (the IOL process dies without it). |
|
||||||
| `restart()` hardening | `IOLDockerVM` | The base `docker restart` would boot the runner on a stale config and leave uBridge wired to the previous run's sockets; reload becomes graceful stop (SIGTERM → NVRAM flush) + full start. |
|
| `restart()` hardening | `IOLDockerVM` | The base `docker restart` would boot the runner on a stale config and leave uBridge wired to the previous run's sockets; reload becomes graceful stop (SIGTERM → NVRAM flush) + full start. |
|
||||||
|
|
||||||
`GNS3_STOP_TIMEOUT` (default 60) controls the SIGTERM grace period on stop.
|
`GNS3_STOP_TIMEOUT` (default 60) controls the SIGTERM grace period on stop.
|
||||||
|
|||||||
@ -25,22 +25,20 @@ and muxes the IOS console onto PID 1 stdio (works with the plain ``telnet``
|
|||||||
console type; requires a TTY, which GNS3 always allocates).
|
console type; requires a TTY, which GNS3 always allocates).
|
||||||
|
|
||||||
Networking does not use the container's network namespace at all: the runner's
|
Networking does not use the container's network namespace at all: the runner's
|
||||||
netiomux exposes per-interface AF_UNIX datagram sockets in ``/tmp``
|
netiomux exposes per-interface AF_UNIX datagram sockets in the container's
|
||||||
(``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames), wired by
|
``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames),
|
||||||
the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM. Because the
|
wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM
|
||||||
netio bus directory is private to the node's ``/tmp`` volume, the application
|
(uBridge reaches them through the container's root in /proc). Because the
|
||||||
IDs are fixed constants with no cross-node collisions.
|
netio bus directory is private to the container's /tmp, the application IDs
|
||||||
|
are fixed constants with no cross-node collisions.
|
||||||
|
|
||||||
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
|
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import glob
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
|
|
||||||
from gns3server.compute.docker.docker_error import DockerHttp404Error
|
|
||||||
from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM
|
from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -58,7 +56,7 @@ class IOLDockerVM(VendorDockerVM):
|
|||||||
OOM-killer will fire.
|
OOM-killer will fire.
|
||||||
|
|
||||||
The marker itself forces ``GNS3_SKIP_INIT`` and the unix-socket NIO wiring,
|
The marker itself forces ``GNS3_SKIP_INIT`` and the unix-socket NIO wiring,
|
||||||
and auto-adds the ``/config`` and ``/tmp`` persistent volumes, so a
|
and auto-adds the ``/config`` and ``/tmp/run`` persistent volumes, so a
|
||||||
template containing only ``GNS3_IOL_RUNNER=1`` is fully configured.
|
template containing only ``GNS3_IOL_RUNNER=1`` is fully configured.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -90,13 +88,14 @@ class IOLDockerVM(VendorDockerVM):
|
|||||||
def _persistent_volume_list(self, image_info, include_network_config=True):
|
def _persistent_volume_list(self, image_info, include_network_config=True):
|
||||||
"""
|
"""
|
||||||
Override: the runner requires ``/config`` (its config file, generated
|
Override: the runner requires ``/config`` (its config file, generated
|
||||||
below) and ``/tmp`` (netiomux sockets, NETMAP, NVRAM) as persisted
|
below) and ``/tmp/run`` (its working directory: startup-config and
|
||||||
volumes — /tmp because uBridge must reach the sockets on the host.
|
NVRAM live there — NETMAP and the netiomux sockets are ephemeral and
|
||||||
Auto-add both so a minimal template cannot be misconfigured.
|
stay in the container's own /tmp). Auto-add both so a minimal
|
||||||
|
template cannot be misconfigured.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
volumes = super()._persistent_volume_list(image_info, include_network_config)
|
volumes = super()._persistent_volume_list(image_info, include_network_config)
|
||||||
for needed in (self._IOL_CONFIG_DIR, "/tmp"):
|
for needed in (self._IOL_CONFIG_DIR, self._IOL_RUN_DIR):
|
||||||
if not any(needed == v or needed.startswith(v.rstrip("/") + "/") for v in volumes):
|
if not any(needed == v or needed.startswith(v.rstrip("/") + "/") for v in volumes):
|
||||||
volumes.append(needed)
|
volumes.append(needed)
|
||||||
return volumes
|
return volumes
|
||||||
@ -119,47 +118,28 @@ class IOLDockerVM(VendorDockerVM):
|
|||||||
|
|
||||||
async def _prepare_iol_runtime(self):
|
async def _prepare_iol_runtime(self):
|
||||||
"""
|
"""
|
||||||
Regenerate the node's runtime files before the container starts.
|
Regenerate the node's runtime files before the container starts:
|
||||||
|
|
||||||
* ``<working_dir>/tmp/run/`` must exist or the IOL process dies at
|
* ``<working_dir>/tmp/run/`` must exist or the IOL process dies at
|
||||||
boot (the runner writes NETMAP there but does not create it).
|
boot (the runner writes NETMAP there but does not create it).
|
||||||
* ``<working_dir>/config/iol-config.json`` is rewritten on every
|
* ``<working_dir>/config/iol-config.json`` is rewritten on every
|
||||||
start so adapter-count and memory changes take effect.
|
start so adapter-count and memory changes take effect.
|
||||||
* Ephemeral sockets left by a previous (possibly SIGKILLed) run are
|
|
||||||
removed — the runner rebinds them on boot and would fail on a
|
|
||||||
stale file. ``tmp/run`` is never touched: startup-config and NVRAM
|
|
||||||
persist there.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
The netiomux sockets and the netio bus directory need no cleanup:
|
||||||
state = await self._get_container_state()
|
they live in the container's own /tmp, which is fresh in every
|
||||||
except DockerHttp404Error:
|
container GNS3 creates (containers are recreated on each start).
|
||||||
state = "stopped"
|
"""
|
||||||
|
|
||||||
os.makedirs(os.path.join(self.working_dir, "tmp", "run"), exist_ok=True)
|
os.makedirs(os.path.join(self.working_dir, "tmp", "run"), exist_ok=True)
|
||||||
self._write_iol_config()
|
self._write_iol_config()
|
||||||
|
|
||||||
if state == "running":
|
|
||||||
# Idempotent start of a live node: the sockets belong to the
|
|
||||||
# running runner; base start() will return early.
|
|
||||||
return
|
|
||||||
|
|
||||||
tmp_dir = os.path.join(self.working_dir, "tmp")
|
|
||||||
for pattern in ("s??.sock", "c??.sock"):
|
|
||||||
for stale in glob.glob(os.path.join(tmp_dir, pattern)):
|
|
||||||
try:
|
|
||||||
os.unlink(stale)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
for netio_dir in glob.glob(os.path.join(tmp_dir, "netio*")):
|
|
||||||
shutil.rmtree(netio_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
def _write_iol_config(self):
|
def _write_iol_config(self):
|
||||||
"""
|
"""
|
||||||
Write the runner's config file on the host side of the /config volume.
|
Write the runner's config file on the host side of the /config volume.
|
||||||
The runner drops to user-id/group-id after its setup, so everything it
|
The runner drops to user-id/group-id after its setup, so everything it
|
||||||
creates inside the volumes is owned by the server user (which is also
|
creates is owned by the server user — which is also what lets an
|
||||||
what makes the /tmp sockets reachable by uBridge).
|
unprivileged uBridge traverse /proc/<pid>/root to reach the
|
||||||
|
container's sockets.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
|||||||
@ -32,7 +32,6 @@ 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
|
||||||
@ -70,16 +69,16 @@ class VendorDockerVM(DockerVM):
|
|||||||
socket files instead of a TAP interface moved into the container's
|
socket files instead of a TAP interface moved into the container's
|
||||||
network namespace. For images whose network agent exposes, per adapter
|
network namespace. For images whose network agent exposes, per adapter
|
||||||
``N``, a receive socket ``s%02d.sock`` and a send-to path ``c%02d.sock``
|
``N``, a receive socket ``s%02d.sock`` and a send-to path ``c%02d.sock``
|
||||||
(raw Ethernet frames, one datagram per frame) inside a persisted volume
|
(raw Ethernet frames, one datagram per frame) inside the container —
|
||||||
directory — e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge
|
e.g. Cisco CML's iol-runner (see IOLDockerVM). uBridge reaches the
|
||||||
binds ``c{N:02d}.sock`` (its receive side) and sends to
|
sockets through the container's root in ``/proc`` (see
|
||||||
``s{N:02d}.sock``. No TAP is created, the container's network namespace
|
``_unix_socket_wiring_dir``), so no volume mount is required; it binds
|
||||||
is untouched and the ``mac_address`` template field is ignored (the
|
``c{N:02d}.sock`` (its receive side) and sends to ``s{N:02d}.sock``.
|
||||||
image's agent owns the MAC scheme).
|
No TAP is created, the container's network namespace is untouched and
|
||||||
|
the ``mac_address`` template field is ignored (the image's agent owns
|
||||||
|
the MAC scheme).
|
||||||
* ``GNS3_UNIX_SOCKET_DIR=<dir>`` — in-container directory holding the
|
* ``GNS3_UNIX_SOCKET_DIR=<dir>`` — in-container directory holding the
|
||||||
socket files (default ``/tmp``). Must be a persisted volume
|
socket files (default ``/tmp``).
|
||||||
(extra_volumes) so uBridge can reach the sockets on the host; node
|
|
||||||
creation fails with an actionable error otherwise.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -104,7 +103,6 @@ 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(",")
|
||||||
@ -167,17 +165,6 @@ class VendorDockerVM(DockerVM):
|
|||||||
are never shadowed by an empty mount.
|
are never shadowed by an empty mount.
|
||||||
"""
|
"""
|
||||||
binds = super()._mount_binds(image_info)
|
binds = super()._mount_binds(image_info)
|
||||||
if self._unix_socket_nio:
|
|
||||||
socket_dir = self._unix_socket_dir.rstrip("/")
|
|
||||||
if not any(
|
|
||||||
v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/")
|
|
||||||
for v in self._volumes
|
|
||||||
):
|
|
||||||
raise DockerError(
|
|
||||||
f"GNS3_UNIX_SOCKET_NIO requires socket directory '{self._unix_socket_dir}' of "
|
|
||||||
f"container '{self._name}' to be a persisted volume (add it to extra_volumes) "
|
|
||||||
f"so uBridge can reach the sockets on the host"
|
|
||||||
)
|
|
||||||
if self._gns3_init:
|
if self._gns3_init:
|
||||||
return binds
|
return binds
|
||||||
binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"]
|
binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"]
|
||||||
@ -320,57 +307,20 @@ class VendorDockerVM(DockerVM):
|
|||||||
return self._interface_names[adapter_number]
|
return self._interface_names[adapter_number]
|
||||||
return f"eth{adapter_number}"
|
return f"eth{adapter_number}"
|
||||||
|
|
||||||
@property
|
async def _unix_socket_wiring_dir(self):
|
||||||
def _unix_socket_host_dir(self):
|
|
||||||
"""
|
"""
|
||||||
Host-side path of the in-container unix-socket directory: the bind
|
Host-side directory to reference in the uBridge unix-NIO commands:
|
||||||
source of the persisted volume it lives in.
|
the container's socket directory reached through its root in /proc.
|
||||||
"""
|
|
||||||
return os.path.join(self.working_dir, os.path.relpath(self._unix_socket_dir, "/"))
|
|
||||||
|
|
||||||
def _unix_socket_wiring_dir(self):
|
The sockets live in the container's own filesystem (no volume mount
|
||||||
"""
|
needed); /proc/<pid>/root/<dir> resolves to the same files uBridge
|
||||||
Directory to reference in the uBridge unix-NIO commands.
|
sees, from outside any namespace. This also keeps the path well under
|
||||||
|
the 107-byte sun_path cap — a node directory (projects/<uuid>/…)
|
||||||
AF_UNIX paths are capped at 107 bytes (sun_path minus the NUL), and a
|
alone exceeds it.
|
||||||
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
|
pid = await self._get_namespace()
|
||||||
if len(host_dir) + len("/c00.sock") <= 107:
|
return f"/proc/{pid}/root{self._unix_socket_dir}"
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
@ -379,15 +329,16 @@ class VendorDockerVM(DockerVM):
|
|||||||
a TAP interface moved into the container's network namespace.
|
a TAP interface moved into the container's network namespace.
|
||||||
|
|
||||||
Per adapter N the image's network agent is expected to create, inside
|
Per adapter N the image's network agent is expected to create, inside
|
||||||
GNS3_UNIX_SOCKET_DIR (a persisted volume, enforced by _mount_binds):
|
GNS3_UNIX_SOCKET_DIR:
|
||||||
|
|
||||||
* ``s{N:02d}.sock`` — its receive socket; frames sent there are
|
* ``s{N:02d}.sock`` — its receive socket; frames sent there are
|
||||||
injected into guest interface N;
|
injected into guest interface N;
|
||||||
* ``c{N:02d}.sock`` — the path it sends guest-egress frames to.
|
* ``c{N:02d}.sock`` — the path it sends guest-egress frames to.
|
||||||
|
|
||||||
uBridge binds the c-socket as its receive side and sends to the
|
uBridge binds the c-socket as its receive side and sends to the
|
||||||
s-socket. No TAP is allocated, the namespace is untouched and guest
|
s-socket (both reached through /proc — see _unix_socket_wiring_dir).
|
||||||
MAC addresses are whatever the image's agent uses.
|
No TAP is allocated, the namespace is untouched and guest MAC
|
||||||
|
addresses are whatever the image's agent uses.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._unix_socket_nio:
|
if not self._unix_socket_nio:
|
||||||
@ -406,8 +357,7 @@ class VendorDockerVM(DockerVM):
|
|||||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||||
self._bridges.add(bridge_name)
|
self._bridges.add(bridge_name)
|
||||||
|
|
||||||
host_dir = self._unix_socket_host_dir
|
wiring_dir = await self._unix_socket_wiring_dir()
|
||||||
wiring_dir = self._unix_socket_wiring_dir()
|
|
||||||
local_sock = os.path.join(wiring_dir, f"c{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")
|
remote_sock = os.path.join(wiring_dir, f"s{adapter_number:02d}.sock")
|
||||||
|
|
||||||
@ -424,7 +374,8 @@ class VendorDockerVM(DockerVM):
|
|||||||
f"Socket '{remote_sock}' for adapter {adapter_number} of container "
|
f"Socket '{remote_sock}' for adapter {adapter_number} of container "
|
||||||
f"'{self._name}' did not appear within 30 seconds. Check that the "
|
f"'{self._name}' did not appear within 30 seconds. Check that the "
|
||||||
f"container's port count covers adapter {adapter_number} and that "
|
f"container's port count covers adapter {adapter_number} and that "
|
||||||
f"'{self._unix_socket_dir}' is bind-mounted from the node directory."
|
f"its network agent creates the per-adapter socket pair in "
|
||||||
|
f"'{self._unix_socket_dir}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._ubridge_send(f'bridge add_nio_unix {bridge_name} "{local_sock}" "{remote_sock}"')
|
await self._ubridge_send(f'bridge add_nio_unix {bridge_name} "{local_sock}" "{remote_sock}"')
|
||||||
|
|||||||
@ -23,7 +23,6 @@ the uBridge command stream.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import glob
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
@ -98,6 +97,23 @@ def _seed_proc(stdout=b"seedcid\n", returncode=0):
|
|||||||
return proc
|
return proc
|
||||||
|
|
||||||
|
|
||||||
|
# A container PID that can never exist on a real host (Linux caps PIDs at
|
||||||
|
# 2^22): lets the wiring tests run against /proc paths without any risk of
|
||||||
|
# touching a live process's files.
|
||||||
|
_FAKE_PID = 4194304
|
||||||
|
|
||||||
|
|
||||||
|
async def _no_wait(path, timeout=None):
|
||||||
|
"""Stand-in for wait_for_file_creation: pretend the socket is there."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_wiring(vm):
|
||||||
|
"""Mock everything _add_ubridge_connection's unix-NIO path needs."""
|
||||||
|
vm._ubridge_hypervisor = MagicMock()
|
||||||
|
vm._get_namespace = AsyncioMagicMock(return_value=_FAKE_PID)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Factory selection
|
# Factory selection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -178,7 +194,7 @@ async def test_create_keeps_image_entrypoint(compute_project, manager):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_auto_adds_config_and_tmp_volumes(compute_project, manager):
|
async def test_create_auto_adds_config_and_tmp_run_volumes(compute_project, manager):
|
||||||
|
|
||||||
with asyncio_patch("gns3server.compute.docker.Docker.list_images",
|
with asyncio_patch("gns3server.compute.docker.Docker.list_images",
|
||||||
return_value=[{"image": "iol-xe"}]):
|
return_value=[{"image": "iol-xe"}]):
|
||||||
@ -190,13 +206,16 @@ async def test_create_auto_adds_config_and_tmp_volumes(compute_project, manager)
|
|||||||
await vm.create()
|
await vm.create()
|
||||||
sent = mock.call_args.kwargs["data"]
|
sent = mock.call_args.kwargs["data"]
|
||||||
targets = [m["Target"] for m in sent["HostConfig"]["Mounts"]]
|
targets = [m["Target"] for m in sent["HostConfig"]["Mounts"]]
|
||||||
# both volumes are forced and bound at their real in-container
|
# /config (runner config) and /tmp/run (startup-config + NVRAM)
|
||||||
# paths (skip-init retargeting), reachable by uBridge on the host
|
# are forced and bound at their real in-container paths
|
||||||
|
# (skip-init retargeting); the sockets stay in the container's
|
||||||
|
# own /tmp, reached via /proc, so /tmp itself is not a volume
|
||||||
assert "/config" in targets
|
assert "/config" in targets
|
||||||
assert "/tmp" in targets
|
assert "/tmp/run" in targets
|
||||||
|
assert "/tmp" not in targets
|
||||||
assert not any(t.startswith("/gns3volumes/") for t in targets)
|
assert not any(t.startswith("/gns3volumes/") for t in targets)
|
||||||
vol_env = [v for v in sent["Env"] if v.startswith("GNS3_VOLUMES=")][0]
|
vol_env = [v for v in sent["Env"] if v.startswith("GNS3_VOLUMES=")][0]
|
||||||
assert "/config" in vol_env and "/tmp" in vol_env
|
assert "/config" in vol_env and "/tmp/run" in vol_env
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -274,14 +293,16 @@ async def test_start_creates_run_dir(compute_project, manager):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_start_cleans_stale_sockets_but_keeps_run(compute_project, manager):
|
async def test_start_does_no_host_side_socket_cleanup(compute_project, manager):
|
||||||
|
"""
|
||||||
|
Sockets live in the container's own /tmp (fresh in every container GNS3
|
||||||
|
creates), so start() must not touch anything under the node's tmp/
|
||||||
|
beyond creating tmp/run.
|
||||||
|
"""
|
||||||
|
|
||||||
vm = _make_vm(compute_project, manager)
|
vm = _make_vm(compute_project, manager)
|
||||||
tmp_dir = os.path.join(vm.working_dir, "tmp")
|
tmp_dir = os.path.join(vm.working_dir, "tmp")
|
||||||
os.makedirs(os.path.join(tmp_dir, "run"), exist_ok=True)
|
os.makedirs(os.path.join(tmp_dir, "run"), exist_ok=True)
|
||||||
for name in ("s00.sock", "c00.sock", "s01.sock", "c01.sock"):
|
|
||||||
open(os.path.join(tmp_dir, name), "w").close()
|
|
||||||
os.makedirs(os.path.join(tmp_dir, "netio1000"))
|
|
||||||
open(os.path.join(tmp_dir, "run", "nvram_00001"), "w").close()
|
open(os.path.join(tmp_dir, "run", "nvram_00001"), "w").close()
|
||||||
open(os.path.join(tmp_dir, "run", "config"), "w").close()
|
open(os.path.join(tmp_dir, "run", "config"), "w").close()
|
||||||
|
|
||||||
@ -290,31 +311,11 @@ async def test_start_cleans_stale_sockets_but_keeps_run(compute_project, manager
|
|||||||
with asyncio_patch("gns3server.compute.docker.Docker.query"):
|
with asyncio_patch("gns3server.compute.docker.Docker.query"):
|
||||||
await vm.start()
|
await vm.start()
|
||||||
|
|
||||||
assert glob.glob(os.path.join(tmp_dir, "s??.sock")) == []
|
# the persistent runtime is untouched
|
||||||
assert glob.glob(os.path.join(tmp_dir, "c??.sock")) == []
|
|
||||||
assert not os.path.exists(os.path.join(tmp_dir, "netio1000"))
|
|
||||||
# the persistent runtime survives the cleanup
|
|
||||||
assert os.path.exists(os.path.join(tmp_dir, "run", "nvram_00001"))
|
assert os.path.exists(os.path.join(tmp_dir, "run", "nvram_00001"))
|
||||||
assert os.path.exists(os.path.join(tmp_dir, "run", "config"))
|
assert os.path.exists(os.path.join(tmp_dir, "run", "config"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_skips_cleanup_when_already_running(compute_project, manager):
|
|
||||||
|
|
||||||
vm = _make_vm(compute_project, manager)
|
|
||||||
tmp_dir = os.path.join(vm.working_dir, "tmp")
|
|
||||||
os.makedirs(tmp_dir, exist_ok=True)
|
|
||||||
open(os.path.join(tmp_dir, "s00.sock"), "w").close()
|
|
||||||
|
|
||||||
_mock_start(vm, state="running")
|
|
||||||
with patch("gns3server.compute.docker.Docker.install_busybox"):
|
|
||||||
with asyncio_patch("gns3server.compute.docker.Docker.query"):
|
|
||||||
await vm.start()
|
|
||||||
|
|
||||||
# live runner sockets must not be deleted behind its back
|
|
||||||
assert os.path.exists(os.path.join(tmp_dir, "s00.sock"))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_fix_permissions_is_noop(compute_project, manager):
|
async def test_fix_permissions_is_noop(compute_project, manager):
|
||||||
|
|
||||||
@ -344,49 +345,36 @@ async def test_restart_is_graceful_stop_then_start(compute_project, manager):
|
|||||||
async def test_add_ubridge_connection_unix_wiring(compute_project, manager):
|
async def test_add_ubridge_connection_unix_wiring(compute_project, manager):
|
||||||
|
|
||||||
vm = _make_vm(compute_project, manager)
|
vm = _make_vm(compute_project, manager)
|
||||||
vm._ubridge_hypervisor = MagicMock()
|
_mock_wiring(vm)
|
||||||
host_dir = os.path.join(vm.working_dir, "tmp")
|
|
||||||
os.makedirs(host_dir, exist_ok=True)
|
|
||||||
# the runner's receive socket must exist (created by the container)
|
|
||||||
open(os.path.join(host_dir, "s00.sock"), "w").close()
|
|
||||||
|
|
||||||
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
|
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
|
||||||
await vm._add_ubridge_connection(nio, 0)
|
with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation",
|
||||||
|
side_effect=_no_wait):
|
||||||
|
await vm._add_ubridge_connection(nio, 0)
|
||||||
|
|
||||||
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
|
local_sock = f"/proc/{_FAKE_PID}/root/tmp/c00.sock"
|
||||||
|
remote_sock = f"/proc/{_FAKE_PID}/root/tmp/s00.sock"
|
||||||
|
# the wiring path rides the container's root in /proc — well under
|
||||||
|
# sun_path's 107 bytes no matter how deep the project directory is
|
||||||
|
assert len(local_sock) <= 107
|
||||||
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(wiring_dir, "c00.sock")}" '
|
assert call.send(f'bridge add_nio_unix bridge0 "{local_sock}" "{remote_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(wiring_dir, "c00.sock")
|
assert vm._ethernet_adapters[0].host_ifc == local_sock
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_add_ubridge_connection_stale_local_socket_unlinked(compute_project, manager):
|
|
||||||
|
|
||||||
vm = _make_vm(compute_project, manager)
|
|
||||||
vm._ubridge_hypervisor = MagicMock()
|
|
||||||
host_dir = os.path.join(vm.working_dir, "tmp")
|
|
||||||
os.makedirs(host_dir, exist_ok=True)
|
|
||||||
open(os.path.join(host_dir, "c00.sock"), "w").close()
|
|
||||||
open(os.path.join(host_dir, "s00.sock"), "w").close()
|
|
||||||
|
|
||||||
await vm._add_ubridge_connection(None, 0)
|
|
||||||
assert not os.path.exists(os.path.join(host_dir, "c00.sock"))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_ubridge_connection_adapter_out_of_range(compute_project, manager):
|
async def test_add_ubridge_connection_adapter_out_of_range(compute_project, manager):
|
||||||
|
|
||||||
vm = _make_vm(compute_project, manager)
|
vm = _make_vm(compute_project, manager)
|
||||||
vm._ubridge_hypervisor = MagicMock()
|
_mock_wiring(vm)
|
||||||
with pytest.raises(DockerError):
|
with pytest.raises(DockerError):
|
||||||
await vm._add_ubridge_connection(None, 42)
|
await vm._add_ubridge_connection(None, 42)
|
||||||
|
|
||||||
@ -395,7 +383,7 @@ async def test_add_ubridge_connection_adapter_out_of_range(compute_project, mana
|
|||||||
async def test_add_ubridge_connection_timeout_is_actionable(compute_project, manager):
|
async def test_add_ubridge_connection_timeout_is_actionable(compute_project, manager):
|
||||||
|
|
||||||
vm = _make_vm(compute_project, manager)
|
vm = _make_vm(compute_project, manager)
|
||||||
vm._ubridge_hypervisor = MagicMock()
|
_mock_wiring(vm)
|
||||||
|
|
||||||
async def raise_timeout(path, timeout=60):
|
async def raise_timeout(path, timeout=60):
|
||||||
raise asyncio.TimeoutError()
|
raise asyncio.TimeoutError()
|
||||||
@ -404,21 +392,19 @@ async def test_add_ubridge_connection_timeout_is_actionable(compute_project, man
|
|||||||
side_effect=raise_timeout):
|
side_effect=raise_timeout):
|
||||||
with pytest.raises(DockerError) as excinfo:
|
with pytest.raises(DockerError) as excinfo:
|
||||||
await vm._add_ubridge_connection(None, 0)
|
await vm._add_ubridge_connection(None, 0)
|
||||||
# the message names the adapter and the socket directory
|
# the message names the adapter and the exact wiring path
|
||||||
assert "adapter 0" in str(excinfo.value)
|
assert "adapter 0" in str(excinfo.value)
|
||||||
assert "/tmp" in str(excinfo.value)
|
assert f"/proc/{_FAKE_PID}/root/tmp/s00.sock" in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_ubridge_connection_without_nio_still_wires(compute_project, manager):
|
async def test_add_ubridge_connection_without_nio_still_wires(compute_project, manager):
|
||||||
|
|
||||||
vm = _make_vm(compute_project, manager)
|
vm = _make_vm(compute_project, manager)
|
||||||
vm._ubridge_hypervisor = MagicMock()
|
_mock_wiring(vm)
|
||||||
host_dir = os.path.join(vm.working_dir, "tmp")
|
with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation",
|
||||||
os.makedirs(host_dir, exist_ok=True)
|
side_effect=_no_wait):
|
||||||
open(os.path.join(host_dir, "s00.sock"), "w").close()
|
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 "bridge create bridge0" in flat
|
assert "bridge create bridge0" in flat
|
||||||
assert "add_nio_unix" in flat
|
assert "add_nio_unix" in flat
|
||||||
@ -452,23 +438,16 @@ def test_env_unix_socket_nio_parsing(compute_project, manager):
|
|||||||
assert vm._unix_socket_nio is False
|
assert vm._unix_socket_nio is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
def test_unix_socket_dir_needs_no_volume(compute_project, manager):
|
||||||
async def test_unix_socket_dir_must_be_a_volume(compute_project, manager):
|
|
||||||
|
|
||||||
vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest",
|
vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest",
|
||||||
console_type="docker_exec",
|
console_type="docker_exec",
|
||||||
environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1",
|
environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1",
|
||||||
extra_volumes=[])
|
extra_volumes=[])
|
||||||
with pytest.raises(DockerError) as excinfo:
|
# sockets are reached through /proc, not through a volume bind — creating
|
||||||
vm._mount_binds({"Config": {"Volumes": {}}})
|
# the container without the socket dir in extra_volumes is fine
|
||||||
assert "extra_volumes" in str(excinfo.value)
|
|
||||||
|
|
||||||
vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest",
|
|
||||||
console_type="docker_exec",
|
|
||||||
environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1",
|
|
||||||
extra_volumes=["/tmp"])
|
|
||||||
binds = vm._mount_binds({"Config": {"Volumes": {}}})
|
binds = vm._mount_binds({"Config": {"Volumes": {}}})
|
||||||
assert any(b["Target"] == "/tmp" for b in binds)
|
assert not any(b["Target"] == "/tmp" for b in binds)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -477,44 +456,10 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage
|
|||||||
vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest",
|
vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest",
|
||||||
console_type="docker_exec",
|
console_type="docker_exec",
|
||||||
environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks")
|
environment="GNS3_SKIP_INIT=1\nGNS3_UNIX_SOCKET_NIO=1\nGNS3_UNIX_SOCKET_DIR=/var/run/socks")
|
||||||
vm._ubridge_hypervisor = MagicMock()
|
_mock_wiring(vm)
|
||||||
host_dir = os.path.join(vm.working_dir, "var", "run", "socks")
|
with patch("gns3server.compute.docker.vendor_docker_vm.wait_for_file_creation",
|
||||||
os.makedirs(host_dir, exist_ok=True)
|
side_effect=_no_wait):
|
||||||
open(os.path.join(host_dir, "s00.sock"), "w").close()
|
|
||||||
|
|
||||||
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(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)
|
await vm._add_ubridge_connection(None, 0)
|
||||||
alias = os.path.join(str(runtime_dir), "gns3", f"unixio-{vm.id}")
|
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
|
||||||
assert os.path.islink(alias) and os.readlink(alias) == long_dir
|
assert f'"/proc/{_FAKE_PID}/root/var/run/socks/s00.sock"' in flat
|
||||||
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
|
assert "add_nio_tap" not in flat
|
||||||
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