mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-09-07 02:25:21 +03:00
feat: allocate IOL Docker application IDs from a pool disjoint from IOU
IOL interface MACs derive from the application ID (aabb.cc{app}{iface}),
so ids must be unique across opened projects sharing computes — the same
reason IOU has its allocator. IOL Docker nodes draw from the upper half
(512-1022, netiomux's fixed peer is 1023) so the two node types can
neither collide nor starve each other; IOU behavior is unchanged.
The controller sniffs the same GNS3_IOL_RUNNER environment marker the
compute uses to select IOLDockerVM (both the nested-properties and
template/top-level-kwarg shapes), stores the id in node properties like
IOU does, and passes it through the Docker create payload. Without an
allocation the compute falls back to a stable node-derived id in the
same upper range.
This commit is contained in:
parent
b17d021bf6
commit
0119373690
@ -138,9 +138,13 @@ diagnosing wiring issues).
|
||||
+ ~512 MB headroom or the OOM-killer will shoot the router.
|
||||
* **MAC addresses**: the `mac_address` template field and per-adapter custom
|
||||
MACs are ignored — IOL derives its own scheme from the node's application
|
||||
ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The ID is derived from
|
||||
the node UUID so linked routers always get distinct MACs (nodes sharing an
|
||||
ID would silently drop each other's frames as MAC loops).
|
||||
ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The controller
|
||||
allocates the ID at node creation from the upper half of the id space
|
||||
(512–1022, disjoint from IOU's 1–511, limit 511 IOL Docker nodes across
|
||||
opened projects sharing computes); without an allocation (raw compute API,
|
||||
pre-allocation topologies) a stable node-derived fallback in the same
|
||||
range is used. Nodes sharing an ID would silently drop each other's
|
||||
frames as MAC loops.
|
||||
* **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.
|
||||
|
||||
@ -29,8 +29,10 @@ netiomux exposes per-interface AF_UNIX datagram sockets in the container's
|
||||
``/tmp`` (``s%02d.sock`` receive, ``c%02d.sock`` send — raw Ethernet frames),
|
||||
wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM
|
||||
(uBridge reaches them through a per-node runtime directory bound at /tmp —
|
||||
see ``VendorDockerVM._unix_socket_host_dir``). The local application ID is
|
||||
derived from the node ID so that linked nodes get distinct MACs.
|
||||
see ``VendorDockerVM._unix_socket_host_dir``). The controller allocates the
|
||||
IOL application ID (upper half of the id space, disjoint from IOU's) so that
|
||||
linked nodes get distinct MACs; without an allocation a stable node-derived
|
||||
fallback is used.
|
||||
|
||||
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
|
||||
"""
|
||||
@ -91,6 +93,27 @@ class IOLDockerVM(VendorDockerVM):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Application ID allocated by the controller (upper half of the id
|
||||
# space, disjoint from IOU's); None until the create payload carries it.
|
||||
self._application_id = None
|
||||
|
||||
@property
|
||||
def application_id(self) -> int:
|
||||
"""
|
||||
IOL application ID: drives interface MACs (aabb.cc{app}{iface}) and
|
||||
the NVRAM file name. Falls back to a stable per-node value in the
|
||||
IOL Docker half of the id space when no allocation is available
|
||||
(raw compute API use, topologies created before allocation existed).
|
||||
"""
|
||||
|
||||
if self._application_id is None:
|
||||
return 512 + int(self.id.replace("-", ""), 16) % 511
|
||||
return self._application_id
|
||||
|
||||
@application_id.setter
|
||||
def application_id(self, value) -> None:
|
||||
self._application_id = int(value)
|
||||
|
||||
@DockerVM.adapters.setter
|
||||
def adapters(self, adapters):
|
||||
"""
|
||||
@ -194,11 +217,12 @@ class IOLDockerVM(VendorDockerVM):
|
||||
"num-eth": self.adapters * 4, # every adapter is a 4-port unit
|
||||
"num-serial": 0, # GNS3 docker adapters are ethernet-only
|
||||
# IOL derives interface MACs from the local application ID
|
||||
# (aabb.cc00.0<app><iface>0); every node needs a distinct one or
|
||||
# linked routers share MACs and drop each other's frames as loops.
|
||||
# Derived from the node ID: stable across restarts, unique enough
|
||||
# (CML allocates per-lab sequential IDs for the same reason).
|
||||
"local-app": int(self.id.replace("-", ""), 16) % 1022 + 1,
|
||||
# (aabb.cc{app}{iface}); every node needs a distinct one or
|
||||
# linked routers share MACs and drop each other's frames as
|
||||
# loops. Allocated by the controller per node (upper half of
|
||||
# the id space, disjoint from IOU's — CML does the same with
|
||||
# its per-deployment iol_app_id).
|
||||
"local-app": self.application_id,
|
||||
"remote-app": 1023, # netiomux's fake peer application ID
|
||||
"user-id": os.getuid(),
|
||||
"group-id": os.getgid(),
|
||||
|
||||
@ -30,6 +30,7 @@ from .controller_error import (
|
||||
from .node_types import BUILTIN_NODE_TYPES
|
||||
from .ports.port_factory import PortFactory, StandardPortFactory, DynamipsPortFactory
|
||||
from ..utils.images import images_directories
|
||||
from ..utils.application_id import is_iol_runner_environment
|
||||
from ..utils import macaddress_to_int, int_to_macaddress
|
||||
from ..config import Config
|
||||
|
||||
@ -804,7 +805,7 @@ class Node:
|
||||
self._ports = DynamipsPortFactory(self._properties)
|
||||
return
|
||||
elif self._node_type == "docker":
|
||||
if "GNS3_IOL_RUNNER=" in (self._properties.get("environment") or ""):
|
||||
if is_iol_runner_environment(self._properties.get("environment")):
|
||||
# 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).
|
||||
|
||||
@ -40,7 +40,7 @@ from .udp_link import UDPLink
|
||||
from .link import _UNSET
|
||||
from ..config import Config
|
||||
from ..utils.path import check_path_allowed, get_default_project_directory
|
||||
from ..utils.application_id import get_next_application_id
|
||||
from ..utils.application_id import get_next_application_id, is_iol_runner_environment
|
||||
from ..utils.asyncio.pool import Pool
|
||||
from ..utils.packet_filter_validation import validate_bpf_syntax
|
||||
from ..utils.asyncio import locking
|
||||
@ -69,6 +69,21 @@ def open_required(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
def _is_iol_docker_kwargs(kwargs) -> bool:
|
||||
"""
|
||||
Whether add_node() kwargs describe an iol-runner Docker node. The
|
||||
environment can arrive nested in a ``properties`` dict or as a top-level
|
||||
kwarg (the template path spreads it), matching the two shapes IOU
|
||||
application-id injection handles.
|
||||
"""
|
||||
|
||||
if "properties" in kwargs.keys():
|
||||
environment = (kwargs.get("properties") or {}).get("environment")
|
||||
else:
|
||||
environment = kwargs.get("environment")
|
||||
return is_iol_runner_environment(environment)
|
||||
|
||||
|
||||
class Project:
|
||||
"""
|
||||
A project inside a controller
|
||||
@ -159,7 +174,7 @@ class Project:
|
||||
assert self._status != "closed"
|
||||
self.dump()
|
||||
|
||||
self._iou_id_lock = asyncio.Lock()
|
||||
self._application_id_lock = asyncio.Lock()
|
||||
# Serialise the "ensure project exists on this compute" check in
|
||||
# _create_node: without it, concurrent node creations all pass the
|
||||
# `compute not in _project_created_on_compute` check before any has
|
||||
@ -646,7 +661,7 @@ class Project:
|
||||
self._computes.append(compute.id)
|
||||
|
||||
if node_type == "iou":
|
||||
async with self._iou_id_lock:
|
||||
async with self._application_id_lock:
|
||||
# IOU application IDs must be allocated serially to avoid duplicates.
|
||||
# The lock must also cover _create_node() because get_next_application_id()
|
||||
# checks in-memory nodes (self._nodes), which are only registered
|
||||
@ -658,6 +673,23 @@ class Project:
|
||||
elif "application_id" not in kwargs.keys() and not kwargs.get("properties"):
|
||||
kwargs["application_id"] = get_next_application_id(self._controller.projects, self._computes)
|
||||
node = await self._create_node(compute, name, node_id, node_type, **kwargs)
|
||||
elif node_type == "docker" and _is_iol_docker_kwargs(kwargs):
|
||||
# IOL Docker nodes derive interface MACs from the application ID
|
||||
# exactly like IOU; they draw from the disjoint upper half of the
|
||||
# id space so the two node types can neither collide nor starve
|
||||
# each other.
|
||||
async with self._application_id_lock:
|
||||
if "properties" in kwargs.keys():
|
||||
properties = kwargs.get("properties") or {}
|
||||
if "application_id" not in properties:
|
||||
properties["application_id"] = get_next_application_id(
|
||||
self._controller.projects, self._computes, iol_docker=True
|
||||
)
|
||||
elif "application_id" not in kwargs.keys():
|
||||
kwargs["application_id"] = get_next_application_id(
|
||||
self._controller.projects, self._computes, iol_docker=True
|
||||
)
|
||||
node = await self._create_node(compute, name, node_id, node_type, **kwargs)
|
||||
else:
|
||||
node = await self._create_node(compute, name, node_id, node_type, **kwargs)
|
||||
self.emit_notification("node.created", node.asdict())
|
||||
|
||||
@ -69,7 +69,9 @@ class DockerCreate(DockerBase):
|
||||
Properties to create a Docker node.
|
||||
"""
|
||||
|
||||
pass
|
||||
application_id: Optional[int] = Field(
|
||||
None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)"
|
||||
)
|
||||
|
||||
|
||||
class DockerUpdate(DockerBase):
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from gns3server.controller.controller_error import ControllerError
|
||||
|
||||
@ -20,13 +21,32 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# IOU draws from the lower half: uBridge's iol_bridge uses application_id + 512
|
||||
# for its netio peer endpoint. IOL Docker nodes (iol-runner images) draw from
|
||||
# the upper half: their netiomux peer is the fixed id 1023. Both node types
|
||||
# derive interface MACs from the id (aabb.cc{app}{iface}), so the two pools
|
||||
# must stay disjoint — a shared id silently blackholes traffic between the
|
||||
# nodes as a MAC loop.
|
||||
IOU_APPLICATION_ID_POOL = range(1, 512)
|
||||
IOL_DOCKER_APPLICATION_ID_POOL = range(512, 1023)
|
||||
|
||||
def get_next_application_id(projects, computes):
|
||||
|
||||
def is_iol_runner_environment(environment) -> bool:
|
||||
"""
|
||||
Whether a Docker node environment carries the GNS3_IOL_RUNNER marker —
|
||||
the same signal the compute uses to select the IOLDockerVM class.
|
||||
"""
|
||||
|
||||
return "GNS3_IOL_RUNNER=" in (environment or "")
|
||||
|
||||
|
||||
def get_next_application_id(projects, computes, iol_docker=False):
|
||||
"""
|
||||
Calculates free application_id from given nodes
|
||||
|
||||
:param projects: all projects managed by controller
|
||||
:param computes: all computes used by the project
|
||||
:param iol_docker: allocate for an IOL Docker (iol-runner) node instead of IOU
|
||||
:raises HTTPConflict when exceeds number
|
||||
:return: integer first free id
|
||||
"""
|
||||
@ -38,12 +58,26 @@ def get_next_application_id(projects, computes):
|
||||
if project.status == "opened":
|
||||
nodes.extend(list(project.nodes.values()))
|
||||
|
||||
used = {n.properties["application_id"] for n in nodes if n.node_type == "iou" and n.compute.id in computes}
|
||||
pool = set(range(1, 512))
|
||||
if iol_docker:
|
||||
used = {
|
||||
n.properties["application_id"]
|
||||
for n in nodes
|
||||
if n.node_type == "docker"
|
||||
and n.compute.id in computes
|
||||
and "application_id" in n.properties
|
||||
and is_iol_runner_environment(n.properties.get("environment"))
|
||||
}
|
||||
pool = set(IOL_DOCKER_APPLICATION_ID_POOL)
|
||||
limit = "511 IOL Docker nodes"
|
||||
else:
|
||||
used = {n.properties["application_id"] for n in nodes if n.node_type == "iou" and n.compute.id in computes}
|
||||
pool = set(IOU_APPLICATION_ID_POOL)
|
||||
limit = "512 nodes"
|
||||
try:
|
||||
application_id = (pool - used).pop()
|
||||
return application_id
|
||||
except KeyError:
|
||||
raise ControllerError(
|
||||
"Cannot create a new IOU node (limit of 512 nodes across all opened projects using the same computes)"
|
||||
f"Cannot create a new {'IOL Docker' if iol_docker else 'IOU'} node "
|
||||
f"(limit of {limit} across all opened projects using the same computes)"
|
||||
)
|
||||
|
||||
@ -261,7 +261,7 @@ async def test_start_writes_iol_config(compute_project, manager):
|
||||
assert config["binary"] == "/binary.iol"
|
||||
assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit
|
||||
assert config["num-serial"] == 0
|
||||
assert config["local-app"] == int(vm.id.replace("-", ""), 16) % 1022 + 1
|
||||
assert config["local-app"] == 512 + int(vm.id.replace("-", ""), 16) % 511
|
||||
assert config["remote-app"] == 1023
|
||||
assert config["memory"] == 2048
|
||||
assert config["user-id"] == os.getuid()
|
||||
@ -274,10 +274,25 @@ def test_local_app_is_distinct_per_node(compute_project, manager):
|
||||
# drop each other's frames as MAC loops, so IDs must differ per node.
|
||||
vm1 = _make_vm(compute_project, manager)
|
||||
vm2 = _make_vm(compute_project, manager)
|
||||
id1 = int(vm1.id.replace("-", ""), 16) % 1022 + 1
|
||||
id2 = int(vm2.id.replace("-", ""), 16) % 1022 + 1
|
||||
assert id1 != id2
|
||||
assert 1 <= id1 <= 1022 and 1 <= id2 <= 1022
|
||||
assert vm1.application_id != vm2.application_id
|
||||
for vm in (vm1, vm2):
|
||||
assert 512 <= vm.application_id <= 1022
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_allocated_application_id_overrides_fallback(compute_project, manager):
|
||||
# The controller allocates the application ID (upper half of the id
|
||||
# space); the node-derived hash is only a fallback without one.
|
||||
vm = _make_vm(compute_project, manager)
|
||||
vm.application_id = 700
|
||||
assert vm.application_id == 700
|
||||
_mock_start(vm)
|
||||
with patch("gns3server.compute.docker.Docker.install_busybox"):
|
||||
with asyncio_patch("gns3server.compute.docker.Docker.query"):
|
||||
await vm.start()
|
||||
with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f:
|
||||
config = json.load(f)
|
||||
assert config["local-app"] == 700
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@ -296,6 +296,47 @@ async def test_add_node_iou(controller):
|
||||
assert node3.properties["application_id"] == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_node_iol_docker(controller):
|
||||
"""
|
||||
IOL Docker nodes (GNS3_IOL_RUNNER marker) get an application ID from the
|
||||
upper half of the id space, disjoint from IOU's lower half
|
||||
"""
|
||||
|
||||
compute = MagicMock()
|
||||
compute.id = "local"
|
||||
project = await controller.add_project(project_id=str(uuid.uuid4()), name="test1")
|
||||
project.emit_notification = MagicMock()
|
||||
|
||||
response = MagicMock()
|
||||
compute.post = AsyncioMagicMock(return_value=response)
|
||||
|
||||
# template shape: environment as a top-level kwarg
|
||||
node1 = await project.add_node(
|
||||
compute, "iol1", None, node_type="docker", image="iol-xe/iol-xe:17-18-02", environment="GNS3_IOL_RUNNER=1"
|
||||
)
|
||||
# raw API shape: environment nested in properties
|
||||
node2 = await project.add_node(
|
||||
compute,
|
||||
"iol2",
|
||||
None,
|
||||
node_type="docker",
|
||||
properties={"image": "iol-xe/iol-xe:17-18-02", "environment": "GNS3_IOL_RUNNER=1"},
|
||||
)
|
||||
# plain docker nodes are left alone
|
||||
node3 = await project.add_node(
|
||||
compute, "web", None, node_type="docker", image="nginx", environment="FOO=1", adapters=1
|
||||
)
|
||||
|
||||
assert node1.properties["application_id"] == 512
|
||||
assert node2.properties["application_id"] == 513
|
||||
assert "application_id" not in node3.properties
|
||||
|
||||
# IOU keeps its own pool: a subsequent IOU node still gets the lower half
|
||||
node4 = await project.add_node(compute, "iou1", None, node_type="iou")
|
||||
assert node4.properties["application_id"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_node_iou_with_multiple_projects(controller):
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user