mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-08 19:15:26 +03:00
feat: model IOL adapters as 4-port units like the IOU node type
IOL interfaces come in 4-port units (Ethernet0/0-3, Ethernet1/0-3, ...),
addressed like IOU as (adapter_number, port_number 0-3):
- IOLDockerVM builds EthernetAdapter(interfaces=4) per adapter and asks
the runner for adapters x 4 interfaces (num-eth).
- DockerVM threads port_number through the NIO/capture API (the compute
routes parsed it from the URL but dropped it); single-port adapters
keep the historical bridge{N} names and command sequence, multi-port
adapters get one bridge per port (bridge{a}_{p}).
- The unix-socket NIO wiring addresses sockets flat across adapters:
adapter x ports-per-adapter + port, so single-port images keep their
exact socket layout.
- The controller port list for GNS3_IOL_RUNNER docker nodes is generated
by StandardPortFactory with the IOU naming (Ethernet{segment0}/{port0},
segment size 4); plain docker nodes keep eth{N}.
This commit is contained in:
parent
9b55164064
commit
0ca9ccc637
@ -89,16 +89,10 @@ API docs for the auth flow), or in the Web UI under
|
||||
"image": "iol-xe/iol-xe:17-18-02",
|
||||
"category": "router",
|
||||
"symbol": ":/symbols/router.svg",
|
||||
"adapters": 4,
|
||||
"adapters": 2,
|
||||
"console_type": "telnet",
|
||||
"environment": "GNS3_IOL_RUNNER=1",
|
||||
"extra_volumes": ["/config"],
|
||||
"custom_adapters": [
|
||||
{"adapter_number": 0, "port_name": "Ethernet0/0"},
|
||||
{"adapter_number": 1, "port_name": "Ethernet0/1"},
|
||||
{"adapter_number": 2, "port_name": "Ethernet0/2"},
|
||||
{"adapter_number": 3, "port_name": "Ethernet0/3"}
|
||||
]
|
||||
"extra_volumes": ["/config"]
|
||||
}
|
||||
```
|
||||
|
||||
@ -106,19 +100,19 @@ API docs for the auth flow), or in the Web UI under
|
||||
|---|---|---|
|
||||
| `environment` | `GNS3_IOL_RUNNER=1` | The switch that selects `IOLDockerVM` (skip-init, unix-socket NIO, auto volumes). Optional: `GNS3_IOL_MEMORY=<MB>` (default 2048). |
|
||||
| `extra_volumes` | `["/config"]` | `/tmp/run` is auto-added. **Never add `/tmp`** — it would persist the socket directory into the projects tree and uBridge would reject the too-long AF_UNIX path. |
|
||||
| `custom_adapters` | one entry per adapter | Names each port after the IOL interface it maps to: adapter `N` → `Ethernet{N/4}/{N%4}` (same mechanism SR Linux uses for `mgmt0`/`e1-1`). Raise the list up to `Ethernet7/3` (32 ports, the IOL maximum) when raising `adapters`; adapters beyond the list fall back to `eth{N}`. |
|
||||
| `adapters` | number of 4-port units | The IOU convention: one adapter = `Ethernet0/0`–`Ethernet0/3`, two adapters add `Ethernet1/0`–`1/3`, … (8 units / 32 ports max). Ports are addressed as (adapter, port 0–3) and shown grouped in the UI. |
|
||||
| `memory` | optional; `0` (default) = no cap | Unset works — Docker applies no limit. When you do set a cap, keep it at IOL memory + ~512 MB, or the cgroup OOM-killer shoots the router. |
|
||||
| `console_type` | `telnet` | The runner muxes the IOS console onto PID 1 stdio; `docker_exec` is not needed. |
|
||||
| `adapters` | multiple of 4 | IOL port granularity (one 4-port unit per range). |
|
||||
|
||||
### Verify
|
||||
|
||||
1. The node's port list shows `Ethernet0/0`… (from the template's
|
||||
`custom_adapters`).
|
||||
1. The node's port list shows the grouped IOL interfaces
|
||||
(`Ethernet0/0`–`Ethernet1/3` for two adapters), addressed
|
||||
(adapter, port).
|
||||
2. Drop a node into a project and start it — the console shows the
|
||||
`Linux Unix (i686)` banner within seconds.
|
||||
3. `$XDG_RUNTIME_DIR/gns3/unixio/<node-id>/` contains `s00.sock`… (one
|
||||
pair per adapter).
|
||||
pair per port).
|
||||
4. The startup-config lives at
|
||||
`project-files/docker/<node>/tmp/run/config` (interface names
|
||||
`Ethernet0/0`, not `GigabitEthernet0/0`).
|
||||
@ -147,9 +141,10 @@ diagnosing wiring issues).
|
||||
* **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0`
|
||||
(4 ports per unit, matching the adapter-count granularity) — startup
|
||||
configs addressing `GigabitEthernet…` are rejected by the parser.
|
||||
* **Adapters**: change the adapter count while the node is stopped; the
|
||||
config is regenerated on the next start and the runner creates the
|
||||
matching socket set (IOL granularity is 4 ports per unit).
|
||||
* **Adapters**: one adapter is a 4-port unit (the IOU model): change the
|
||||
count while the node is stopped; the config is regenerated on the next
|
||||
start (`num-eth` = adapters × 4) and the runner creates the matching
|
||||
socket set.
|
||||
* **Stop before editing**: NVRAM is only flushed on a graceful stop (SIGTERM,
|
||||
"cleanup done" in `process.log`); a kill loses the running-config changes
|
||||
since the last `write memory`.
|
||||
|
||||
@ -284,7 +284,7 @@ async def create_docker_node_nio(
|
||||
"""
|
||||
|
||||
nio = Docker.instance().create_nio(jsonable_encoder(nio_data, exclude_unset=True))
|
||||
await node.adapter_add_nio_binding(adapter_number, nio)
|
||||
await node.adapter_add_nio_binding(adapter_number, nio, port_number)
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@ -302,13 +302,13 @@ async def update_docker_node_nio(
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
nio = node.get_nio(adapter_number)
|
||||
nio = node.get_nio(adapter_number, port_number)
|
||||
nio.filters.clear()
|
||||
if nio_data.filters:
|
||||
nio.filters = nio_data.filters
|
||||
nio.markers = nio_data.markers or {}
|
||||
nio.suspend = nio_data.suspend
|
||||
await node.adapter_update_nio_binding(adapter_number, nio)
|
||||
await node.adapter_update_nio_binding(adapter_number, nio, port_number)
|
||||
return nio.asdict()
|
||||
|
||||
|
||||
@ -327,7 +327,7 @@ async def delete_docker_node_nio(
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
await node.adapter_remove_nio_binding(adapter_number)
|
||||
await node.adapter_remove_nio_binding(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.post(
|
||||
@ -346,7 +346,7 @@ async def start_docker_node_capture(
|
||||
"""
|
||||
|
||||
pcap_file_path = os.path.join(node.project.capture_working_directory(), node_capture_data.capture_file_name)
|
||||
await node.start_capture(adapter_number, pcap_file_path)
|
||||
await node.start_capture(adapter_number, pcap_file_path, port_number)
|
||||
return {"pcap_file_path": str(pcap_file_path)}
|
||||
|
||||
|
||||
@ -365,7 +365,7 @@ async def stop_docker_node_capture(
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
await node.stop_capture(adapter_number)
|
||||
await node.stop_capture(adapter_number, port_number)
|
||||
|
||||
|
||||
@router.get(
|
||||
@ -382,7 +382,7 @@ async def stream_pcap_file(
|
||||
The port number on the Docker node is always 0.
|
||||
"""
|
||||
|
||||
nio = node.get_nio(adapter_number)
|
||||
nio = node.get_nio(adapter_number, port_number)
|
||||
stream = Docker.instance().stream_pcap_file(nio, node.project.id)
|
||||
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
|
||||
|
||||
|
||||
@ -899,19 +899,24 @@ class DockerVM(BaseNode):
|
||||
await self._start_ubridge(require_privileged_access=True)
|
||||
|
||||
for adapter_number in range(0, self.adapters):
|
||||
nio = self._ethernet_adapters[adapter_number].get_nio(0)
|
||||
async with self.manager.ubridge_lock:
|
||||
try:
|
||||
await self._add_ubridge_connection(nio, adapter_number)
|
||||
except UbridgeNamespaceError:
|
||||
log.error("Container %s failed to start", self.name)
|
||||
await self.stop()
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
# Single-port adapters (the standard case) loop once, keeping
|
||||
# the historical command sequence; multi-port adapters
|
||||
# (e.g. IOL's 4-port units) get one bridge per port.
|
||||
for port_number in range(0, adapter.interfaces):
|
||||
nio = adapter.get_nio(port_number)
|
||||
async with self.manager.ubridge_lock:
|
||||
try:
|
||||
await self._add_ubridge_connection(nio, adapter_number, port_number)
|
||||
except UbridgeNamespaceError:
|
||||
log.error("Container %s failed to start", self.name)
|
||||
await self.stop()
|
||||
|
||||
# The container can crash soon after the start, this means we can not move the interface to the container namespace
|
||||
logdata = await self._get_log()
|
||||
for line in logdata.split("\n"):
|
||||
log.error(line)
|
||||
raise DockerError(logdata)
|
||||
# The container can crash soon after the start, this means we can not move the interface to the container namespace
|
||||
logdata = await self._get_log()
|
||||
for line in logdata.split("\n"):
|
||||
log.error(line)
|
||||
raise DockerError(logdata)
|
||||
|
||||
await self._start_console_server()
|
||||
|
||||
@ -1406,6 +1411,21 @@ class DockerVM(BaseNode):
|
||||
"""
|
||||
return f"eth{adapter_number}"
|
||||
|
||||
def _bridge_name(self, adapter_number, port_number=0):
|
||||
"""
|
||||
uBridge bridge name for an adapter port. Adapters with a single
|
||||
port (every standard Docker node) keep the historical
|
||||
"bridge{adapter}" name; multi-port adapters (e.g. IOL's 4-port
|
||||
units) get one bridge per port.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param port_number: port number on the adapter
|
||||
"""
|
||||
|
||||
if port_number:
|
||||
return f"bridge{adapter_number}_{port_number}"
|
||||
return f"bridge{adapter_number}"
|
||||
|
||||
async def _start_interface_monitor(self):
|
||||
"""Monitor administrative state changes for this container's adapters.
|
||||
|
||||
@ -1558,12 +1578,14 @@ class DockerVM(BaseNode):
|
||||
if writer:
|
||||
await self._close_interface_monitor_writer(writer)
|
||||
|
||||
async def _add_ubridge_connection(self, nio, adapter_number):
|
||||
async def _add_ubridge_connection(self, nio, adapter_number, port_number=0):
|
||||
"""
|
||||
Creates a connection in uBridge.
|
||||
|
||||
:param nio: NIO instance or None if it's a dummy interface (if an interface is missing in ubridge you can't see it via ifconfig in the container)
|
||||
:param adapter_number: adapter number
|
||||
:param port_number: port number on the adapter (standard Docker
|
||||
adapters have a single port, so this is always 0 on the TAP path)
|
||||
"""
|
||||
|
||||
try:
|
||||
@ -1575,6 +1597,13 @@ class DockerVM(BaseNode):
|
||||
)
|
||||
)
|
||||
|
||||
if port_number and adapter.interfaces == 1:
|
||||
raise DockerError(
|
||||
"Port {port_number} doesn't exist on adapter {adapter_number} of Docker container '{name}'".format(
|
||||
name=self.name, port_number=port_number, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
for index in range(4096):
|
||||
if f"tap-gns3-e{index}" not in psutil.net_if_addrs():
|
||||
adapter.host_ifc = f"tap-gns3-e{str(index)}"
|
||||
@ -1585,12 +1614,12 @@ class DockerVM(BaseNode):
|
||||
name=self.name, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
self._bridges.add(bridge_name)
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_tap bridge{adapter_number} {hostif} off".format(
|
||||
adapter_number=adapter_number, hostif=adapter.host_ifc
|
||||
"bridge add_nio_tap {bridge_name} {hostif} off".format(
|
||||
bridge_name=bridge_name, hostif=adapter.host_ifc
|
||||
)
|
||||
)
|
||||
|
||||
@ -1618,23 +1647,24 @@ class DockerVM(BaseNode):
|
||||
log.debug(f"Created adapter {adapter_number} with MAC address {mac_address} in namespace {self._namespace}")
|
||||
|
||||
if nio:
|
||||
await self._connect_nio(adapter_number, nio)
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend)
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number)
|
||||
|
||||
async def _get_namespace(self):
|
||||
|
||||
result = await self.manager.query("GET", f"containers/{self._cid}/json")
|
||||
return int(result["State"]["Pid"])
|
||||
|
||||
async def _set_adapter_carrier(self, adapter_number, connected):
|
||||
async def _set_adapter_carrier(self, adapter_number, connected, port_number=0):
|
||||
"""Replicate a Docker adapter's connection state on its TAP device."""
|
||||
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
state = "on" if connected else "off"
|
||||
await self._ubridge_send(f"bridge set_nio_tap_carrier bridge{adapter_number} {state}")
|
||||
await self._ubridge_send(f"bridge set_nio_tap_carrier {bridge_name} {state}")
|
||||
|
||||
async def _connect_nio(self, adapter_number, nio):
|
||||
async def _connect_nio(self, adapter_number, nio, port_number=0):
|
||||
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
await self._ubridge_send(
|
||||
"bridge add_nio_udp {bridge_name} {lport} {rhost} {rport}".format(
|
||||
bridge_name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
@ -1650,12 +1680,13 @@ class DockerVM(BaseNode):
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
await self._ubridge_apply_markers(bridge_name, nio)
|
||||
|
||||
async def adapter_add_nio_binding(self, adapter_number, nio):
|
||||
async def adapter_add_nio_binding(self, adapter_number, nio, port_number=0):
|
||||
"""
|
||||
Adds an adapter NIO binding.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param nio: NIO instance to add to the slot/port
|
||||
:param nio: NIO instance to add to the adapter/port
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
try:
|
||||
@ -1667,38 +1698,47 @@ class DockerVM(BaseNode):
|
||||
)
|
||||
)
|
||||
|
||||
if self.status == "started" and self.ubridge:
|
||||
await self._connect_nio(adapter_number, nio)
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend)
|
||||
if not adapter.port_exists(port_number):
|
||||
raise DockerError(
|
||||
"Port {port_number} doesn't exist on adapter {adapter_number} of Docker container '{name}'".format(
|
||||
name=self.name, port_number=port_number, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
adapter.add_nio(0, nio)
|
||||
if self.status == "started" and self.ubridge:
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number)
|
||||
|
||||
adapter.add_nio(port_number, nio)
|
||||
log.debug(
|
||||
"Docker container '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(
|
||||
name=self.name, id=self._id, nio=nio, adapter_number=adapter_number
|
||||
)
|
||||
)
|
||||
|
||||
async def adapter_update_nio_binding(self, adapter_number, nio):
|
||||
async def adapter_update_nio_binding(self, adapter_number, nio, port_number=0):
|
||||
"""
|
||||
Update an adapter NIO binding.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param nio: NIO instance to update the adapter
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
if self.ubridge:
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if bridge_name in self._bridges:
|
||||
await self._ubridge_apply_filters(bridge_name, nio.filters)
|
||||
await self._ubridge_apply_markers(bridge_name, nio)
|
||||
if self.status == "started":
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend)
|
||||
await self._set_adapter_carrier(adapter_number, not nio.suspend, port_number)
|
||||
|
||||
async def adapter_remove_nio_binding(self, adapter_number):
|
||||
async def adapter_remove_nio_binding(self, adapter_number, port_number=0):
|
||||
"""
|
||||
Removes an adapter NIO binding.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
@ -1712,20 +1752,20 @@ class DockerVM(BaseNode):
|
||||
)
|
||||
)
|
||||
|
||||
await self.stop_capture(adapter_number)
|
||||
await self.stop_capture(adapter_number, port_number)
|
||||
if self.ubridge:
|
||||
nio = adapter.get_nio(0)
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
nio = adapter.get_nio(port_number)
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if self.status == "started":
|
||||
await self._set_adapter_carrier(adapter_number, False)
|
||||
await self._set_adapter_carrier(adapter_number, False, port_number)
|
||||
await self._ubridge_send(f"bridge stop {bridge_name}")
|
||||
await self._ubridge_send(
|
||||
"bridge remove_nio_udp bridge{adapter} {lport} {rhost} {rport}".format(
|
||||
adapter=adapter_number, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
"bridge remove_nio_udp {bridge_name} {lport} {rhost} {rport}".format(
|
||||
bridge_name=bridge_name, lport=nio.lport, rhost=nio.rhost, rport=nio.rport
|
||||
)
|
||||
)
|
||||
|
||||
adapter.remove_nio(0)
|
||||
adapter.remove_nio(port_number)
|
||||
|
||||
log.debug(
|
||||
"Docker VM '{name}' [{id}]: {nio} removed from adapter {adapter_number}".format(
|
||||
@ -1733,11 +1773,12 @@ class DockerVM(BaseNode):
|
||||
)
|
||||
)
|
||||
|
||||
def get_nio(self, adapter_number):
|
||||
def get_nio(self, adapter_number, port_number=0):
|
||||
"""
|
||||
Gets an adapter NIO binding.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
@ -1751,10 +1792,10 @@ class DockerVM(BaseNode):
|
||||
)
|
||||
)
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
nio = adapter.get_nio(port_number)
|
||||
|
||||
if not nio:
|
||||
raise DockerError(f"Adapter {adapter_number} is not connected")
|
||||
raise DockerError(f"Adapter {adapter_number} port {port_number} is not connected")
|
||||
|
||||
return nio
|
||||
|
||||
@ -1800,46 +1841,49 @@ class DockerVM(BaseNode):
|
||||
|
||||
await self.manager.pull_image(image, progress_callback=callback)
|
||||
|
||||
async def _start_ubridge_capture(self, adapter_number, output_file):
|
||||
async def _start_ubridge_capture(self, adapter_number, output_file, port_number=0):
|
||||
"""
|
||||
Starts a packet capture in uBridge.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param output_file: PCAP destination file for the capture
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
adapter = f"bridge{adapter_number}"
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if not self.ubridge:
|
||||
raise DockerError("Cannot start the packet capture: uBridge is not running")
|
||||
await self._ubridge_send(f'bridge start_capture {adapter} "{output_file}"')
|
||||
await self._ubridge_send(f'bridge start_capture {bridge_name} "{output_file}"')
|
||||
|
||||
async def _stop_ubridge_capture(self, adapter_number):
|
||||
async def _stop_ubridge_capture(self, adapter_number, port_number=0):
|
||||
"""
|
||||
Stops a packet capture in uBridge.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
adapter = f"bridge{adapter_number}"
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
if not self.ubridge:
|
||||
raise DockerError("Cannot stop the packet capture: uBridge is not running")
|
||||
await self._ubridge_send(f"bridge stop_capture {adapter}")
|
||||
await self._ubridge_send(f"bridge stop_capture {bridge_name}")
|
||||
|
||||
async def start_capture(self, adapter_number, output_file):
|
||||
async def start_capture(self, adapter_number, output_file, port_number=0):
|
||||
"""
|
||||
Starts a packet capture.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param output_file: PCAP destination file for the capture
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
nio = self.get_nio(adapter_number)
|
||||
nio = self.get_nio(adapter_number, port_number)
|
||||
if nio.capturing:
|
||||
raise DockerError(f"Packet capture is already activated on adapter {adapter_number}")
|
||||
|
||||
nio.start_packet_capture(output_file)
|
||||
if self.status == "started" and self.ubridge:
|
||||
await self._start_ubridge_capture(adapter_number, output_file)
|
||||
await self._start_ubridge_capture(adapter_number, output_file, port_number)
|
||||
|
||||
log.debug(
|
||||
"Docker VM '{name}' [{id}]: starting packet capture on adapter {adapter_number}".format(
|
||||
@ -1847,19 +1891,20 @@ class DockerVM(BaseNode):
|
||||
)
|
||||
)
|
||||
|
||||
async def stop_capture(self, adapter_number):
|
||||
async def stop_capture(self, adapter_number, port_number=0):
|
||||
"""
|
||||
Stops a packet capture.
|
||||
|
||||
:param adapter_number: adapter number
|
||||
:param port_number: port number on the adapter (0 for single-port adapters)
|
||||
"""
|
||||
|
||||
nio = self.get_nio(adapter_number)
|
||||
nio = self.get_nio(adapter_number, port_number)
|
||||
if not nio.capturing:
|
||||
return
|
||||
nio.stop_packet_capture()
|
||||
if self.status == "started" and self.ubridge:
|
||||
await self._stop_ubridge_capture(adapter_number)
|
||||
await self._stop_ubridge_capture(adapter_number, port_number)
|
||||
|
||||
log.debug(
|
||||
"Docker VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(
|
||||
|
||||
@ -43,7 +43,9 @@ import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from gns3server.compute.adapters.ethernet_adapter import EthernetAdapter
|
||||
from gns3server.compute.docker.docker_error import DockerHttp404Error
|
||||
from gns3server.compute.docker.docker_vm import DockerVM
|
||||
from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -90,6 +92,28 @@ class IOLDockerVM(VendorDockerVM):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@DockerVM.adapters.setter
|
||||
def adapters(self, adapters):
|
||||
"""
|
||||
Override: one IOL adapter is a 4-port unit — the IOU model. The
|
||||
template's adapter count is the number of units (2 adapters =
|
||||
Ethernet0/0-3 + Ethernet1/0-3); the generated config asks the runner
|
||||
for adapters × 4 interfaces and links address ports as
|
||||
(adapter_number, port_number 0-3).
|
||||
"""
|
||||
|
||||
if len(self._ethernet_adapters) == adapters:
|
||||
return
|
||||
|
||||
self._ethernet_adapters.clear()
|
||||
for _ in range(0, adapters):
|
||||
self._ethernet_adapters.append(EthernetAdapter(interfaces=4))
|
||||
|
||||
log.debug(
|
||||
"IOL container '%s': number of 4-port Ethernet adapters set to %d",
|
||||
self._name, adapters,
|
||||
)
|
||||
|
||||
def _persistent_volume_list(self, image_info, include_network_config=True):
|
||||
"""
|
||||
Override: the runner requires ``/config`` (its config file, generated
|
||||
@ -168,7 +192,7 @@ class IOLDockerVM(VendorDockerVM):
|
||||
config = {
|
||||
"binary": "/binary.iol",
|
||||
"memory": self._iol_memory,
|
||||
"num-eth": self.adapters,
|
||||
"num-eth": self.adapters * 4, # every adapter is a 4-port unit
|
||||
"num-serial": 0, # GNS3 docker adapters are ethernet-only
|
||||
"local-app": 1,
|
||||
"remote-app": 2,
|
||||
|
||||
@ -377,14 +377,17 @@ class VendorDockerVM(DockerVM):
|
||||
await super().delete()
|
||||
self._remove_unix_socket_host_dir()
|
||||
|
||||
async def _add_ubridge_connection(self, nio, adapter_number):
|
||||
async def _add_ubridge_connection(self, nio, adapter_number, port_number=0):
|
||||
"""
|
||||
Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter through the
|
||||
image's AF_UNIX datagram socket pair (raw Ethernet frames) instead of
|
||||
a TAP interface moved into the container's network namespace.
|
||||
Override: with GNS3_UNIX_SOCKET_NIO, bridge the adapter port through
|
||||
the image's AF_UNIX datagram socket pair (raw Ethernet frames)
|
||||
instead of a TAP interface moved into the container's network
|
||||
namespace.
|
||||
|
||||
Per adapter N the image's network agent is expected to create, inside
|
||||
GNS3_UNIX_SOCKET_DIR:
|
||||
Ports are addressed flat across adapters: interface index = adapter
|
||||
number × ports-per-adapter + port number (single-port adapters
|
||||
reduce to the adapter number). Per interface N the image's network
|
||||
agent is expected to create, inside GNS3_UNIX_SOCKET_DIR:
|
||||
|
||||
* ``s{N:02d}.sock`` — its receive socket; frames sent there are
|
||||
injected into guest interface N;
|
||||
@ -398,7 +401,7 @@ class VendorDockerVM(DockerVM):
|
||||
"""
|
||||
|
||||
if not self._unix_socket_nio:
|
||||
return await super()._add_ubridge_connection(nio, adapter_number)
|
||||
return await super()._add_ubridge_connection(nio, adapter_number, port_number)
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
@ -409,14 +412,15 @@ class VendorDockerVM(DockerVM):
|
||||
)
|
||||
)
|
||||
|
||||
bridge_name = f"bridge{adapter_number}"
|
||||
interface_number = adapter_number * adapter.interfaces + port_number
|
||||
bridge_name = self._bridge_name(adapter_number, port_number)
|
||||
try:
|
||||
await self._ubridge_send(f"bridge create {bridge_name}")
|
||||
self._bridges.add(bridge_name)
|
||||
|
||||
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")
|
||||
local_sock = os.path.join(wiring_dir, f"c{interface_number:02d}.sock")
|
||||
remote_sock = os.path.join(wiring_dir, f"s{interface_number:02d}.sock")
|
||||
|
||||
# A c-socket left over from a previous ubridge run would fail its bind.
|
||||
with contextlib.suppress(OSError):
|
||||
@ -443,12 +447,12 @@ class VendorDockerVM(DockerVM):
|
||||
raise
|
||||
adapter.host_ifc = local_sock # bookkeeping / removal logging only
|
||||
log.debug(
|
||||
"Adapter %d of container '%s' wired via unix sockets %s <-> %s",
|
||||
adapter_number, self._name, local_sock, remote_sock,
|
||||
"Adapter %d port %d of container '%s' wired via unix sockets %s <-> %s",
|
||||
adapter_number, port_number, self._name, local_sock, remote_sock,
|
||||
)
|
||||
|
||||
if nio:
|
||||
await self._connect_nio(adapter_number, nio)
|
||||
await self._connect_nio(adapter_number, nio, port_number)
|
||||
|
||||
def _cleanup_console_resources(self):
|
||||
"""
|
||||
|
||||
@ -804,22 +804,35 @@ class Node:
|
||||
self._ports = DynamipsPortFactory(self._properties)
|
||||
return
|
||||
elif self._node_type == "docker":
|
||||
for adapter_number in range(0, self._properties["adapters"]):
|
||||
custom_adapter_settings = {}
|
||||
if self.custom_adapters:
|
||||
for custom_adapter in self.custom_adapters:
|
||||
if custom_adapter["adapter_number"] == adapter_number:
|
||||
custom_adapter_settings = custom_adapter
|
||||
break
|
||||
port_name = f"eth{adapter_number}"
|
||||
port_name = custom_adapter_settings.get("port_name", port_name)
|
||||
mac_address = custom_adapter_settings.get("mac_address")
|
||||
if not mac_address and "mac_address" in self._properties:
|
||||
mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number)
|
||||
if "GNS3_IOL_RUNNER=" in (self._properties.get("environment") or ""):
|
||||
# IOL adapters are 4-port units (the IOU model): ports are
|
||||
# Ethernet0/0-3, Ethernet1/0-3, … addressed as
|
||||
# (adapter_number, port_number 0-3).
|
||||
self._ports = StandardPortFactory(
|
||||
self._properties,
|
||||
4,
|
||||
self._first_port_name,
|
||||
"Ethernet{segment0}/{port0}",
|
||||
4,
|
||||
self.custom_adapters,
|
||||
)
|
||||
else:
|
||||
for adapter_number in range(0, self._properties["adapters"]):
|
||||
custom_adapter_settings = {}
|
||||
if self.custom_adapters:
|
||||
for custom_adapter in self.custom_adapters:
|
||||
if custom_adapter["adapter_number"] == adapter_number:
|
||||
custom_adapter_settings = custom_adapter
|
||||
break
|
||||
port_name = f"eth{adapter_number}"
|
||||
port_name = custom_adapter_settings.get("port_name", port_name)
|
||||
mac_address = custom_adapter_settings.get("mac_address")
|
||||
if not mac_address and "mac_address" in self._properties:
|
||||
mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number)
|
||||
|
||||
port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name)
|
||||
port.mac_address = mac_address
|
||||
self._ports.append(port)
|
||||
port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name)
|
||||
port.mac_address = mac_address
|
||||
self._ports.append(port)
|
||||
elif self._node_type in ("ethernet_switch", "ethernet_hub"):
|
||||
# Basic node we don't want to have adapter number
|
||||
port_number = 0
|
||||
|
||||
@ -1198,7 +1198,7 @@ async def test_start(vm, manager, free_console_port, tmpdir):
|
||||
await vm.start()
|
||||
|
||||
mock_query.assert_called_with("POST", "containers/e90e34656842/start")
|
||||
vm._add_ubridge_connection.assert_called_once_with(nio, 0)
|
||||
vm._add_ubridge_connection.assert_called_once_with(nio, 0, 0)
|
||||
assert vm._start_ubridge.called
|
||||
assert vm._start_console.called
|
||||
assert vm._start_aux.called
|
||||
@ -1253,7 +1253,7 @@ async def test_start_namespace_failed(vm, manager, free_console_port):
|
||||
await vm.start()
|
||||
|
||||
mock_query.assert_any_call("POST", "containers/e90e34656842/start")
|
||||
mock_add_ubridge_connection.assert_called_once_with(nio, 0)
|
||||
mock_add_ubridge_connection.assert_called_once_with(nio, 0, 0)
|
||||
assert mock_start_ubridge.called
|
||||
assert vm.status == "stopped"
|
||||
|
||||
|
||||
@ -259,7 +259,7 @@ async def test_start_writes_iol_config(compute_project, manager):
|
||||
with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f:
|
||||
config = json.load(f)
|
||||
assert config["binary"] == "/binary.iol"
|
||||
assert config["num-eth"] == 4
|
||||
assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit
|
||||
assert config["num-serial"] == 0
|
||||
assert config["local-app"] == 1
|
||||
assert config["remote-app"] == 2
|
||||
@ -285,7 +285,7 @@ async def test_start_rewrites_config_on_adapter_change(compute_project, manager)
|
||||
await vm.start()
|
||||
|
||||
with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f:
|
||||
assert json.load(f)["num-eth"] == 8
|
||||
assert json.load(f)["num-eth"] == 32 # 8 adapters × 4 ports
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -460,6 +460,56 @@ async def test_add_ubridge_connection_without_nio_still_wires(compute_project, m
|
||||
assert "bridge start" not in flat
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# IOU-style port model — 1 adapter = 4 ethernet ports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_adapters_are_four_port_units(compute_project, manager):
|
||||
|
||||
vm = _make_vm(compute_project, manager, adapters=2)
|
||||
assert vm.adapters == 2
|
||||
assert len(vm._ethernet_adapters) == 2
|
||||
for adapter in vm._ethernet_adapters:
|
||||
assert adapter.interfaces == 4
|
||||
for port_number in range(4):
|
||||
assert adapter.port_exists(port_number)
|
||||
assert not adapter.port_exists(4)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wiring_addresses_ports_within_adapters(compute_project, manager):
|
||||
|
||||
vm = _make_vm(compute_project, manager, adapters=2)
|
||||
_mock_wiring(vm)
|
||||
wiring_dir = _wiring_dir(vm)
|
||||
os.makedirs(wiring_dir, exist_ok=True)
|
||||
# adapter 1, port 2 -> flat interface index 6 (1 * 4 + 2)
|
||||
open(os.path.join(wiring_dir, "s06.sock"), "w").close()
|
||||
|
||||
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
|
||||
await vm._add_ubridge_connection(nio, 1, port_number=2)
|
||||
|
||||
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
|
||||
assert 'bridge add_nio_unix bridge1_2 ' in flat
|
||||
assert f'"{os.path.join(wiring_dir, "c06.sock")}"' in flat
|
||||
assert f'"{os.path.join(wiring_dir, "s06.sock")}"' in flat
|
||||
assert "add_nio_udp bridge1_2 4242 127.0.0.1 4343" in flat
|
||||
assert vm._ethernet_adapters[1].host_ifc == os.path.join(wiring_dir, "c06.sock")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nio_binding_rejects_port_out_of_range(compute_project, manager):
|
||||
|
||||
vm = _make_vm(compute_project, manager, adapters=2)
|
||||
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
|
||||
with pytest.raises(DockerError) as excinfo:
|
||||
await vm.adapter_add_nio_binding(1, nio, port_number=4)
|
||||
assert "Port 4" in str(excinfo.value)
|
||||
|
||||
with pytest.raises(DockerError):
|
||||
await vm.adapter_add_nio_binding(9, nio, port_number=0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generic GNS3_UNIX_SOCKET_NIO knob on plain VendorDockerVM
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -46,6 +46,32 @@ def node(compute, project):
|
||||
return node
|
||||
|
||||
|
||||
def test_docker_iol_ports_grouped_in_four_port_units(compute, project):
|
||||
"""
|
||||
IOL docker nodes (GNS3_IOL_RUNNER) model adapters as 4-port units: ports
|
||||
are Ethernet0/0-3, Ethernet1/0-3, … addressed (adapter, port 0-3), like
|
||||
the native IOU node type. Plain docker nodes keep the flat eth naming.
|
||||
"""
|
||||
|
||||
node = Node(project, compute, "iol",
|
||||
node_id=str(uuid.uuid4()),
|
||||
node_type="docker",
|
||||
properties={"adapters": 2, "environment": "GNS3_IOL_RUNNER=1"})
|
||||
ports = node.ports
|
||||
assert [p.asdict()["name"] for p in ports] == [
|
||||
"Ethernet0/0", "Ethernet0/1", "Ethernet0/2", "Ethernet0/3",
|
||||
"Ethernet1/0", "Ethernet1/1", "Ethernet1/2", "Ethernet1/3",
|
||||
]
|
||||
assert (ports[5].adapter_number, ports[5].port_number) == (1, 1)
|
||||
assert ports[5].short_name == "e1/1"
|
||||
|
||||
node = Node(project, compute, "web",
|
||||
node_id=str(uuid.uuid4()),
|
||||
node_type="docker",
|
||||
properties={"adapters": 2})
|
||||
assert [p.asdict()["name"] for p in node.ports] == ["eth0", "eth1"]
|
||||
|
||||
|
||||
def test_name(compute, project):
|
||||
"""
|
||||
If node use a name template generate names
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user