mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-23 03:12:22 +03:00
fix: keep payload-delivered state across container (re)creation
create() re-runs _parse_vendor_environment on every (re)create — and a stop removes the container, so every start recreates it. Resetting the controller-allocated application id there silently flipped nodes to the hash fallback after their first stop/start (MAC change, plus collision risk with the allocation pool), and dropped any pending startup-config delivered by a PUT. The application id and startup-config state now default via class attributes instead of being re-initialized by the parser, and the hash fallback is removed outright: starting an IOL node without a coordinated allocation raises an actionable error — an uncoordinated id could collide with the pool and blackhole traffic as a MAC loop. Found in the E2E run: node booted app id 512, came back as 566 (hash fallback) after one stop/start; the PUT-ed config edit vanished the same way.
This commit is contained in:
parent
8b3aafbbdc
commit
5685f40dd6
@ -140,7 +140,10 @@ from it boots with that configuration as its personal starting point.
|
|||||||
share the nvram container format, so the server-side `nvram_import`
|
share the nvram container format, so the server-side `nvram_import`
|
||||||
utility produces a file IOL boots from directly. Consequences:
|
utility produces a file IOL boots from directly. Consequences:
|
||||||
* `write memory` survives stop/start and container recreation (a plain
|
* `write memory` survives stop/start and container recreation (a plain
|
||||||
restart never re-applies the startup-config over it).
|
restart never re-applies the startup-config over it). The first
|
||||||
|
`write memory` after a server-built NVRAM asks for a one-time
|
||||||
|
`[confirm]` (the builder stamps an IOS 15.4 version marker into the
|
||||||
|
nvram header; press return — IOS then writes its own).
|
||||||
* Editing a node's `startup_config_content` property (PUT) re-applies it
|
* Editing a node's `startup_config_content` property (PUT) re-applies it
|
||||||
on the next start, overwriting what `write memory` had saved — the
|
on the next start, overwriting what `write memory` had saved — the
|
||||||
explicit edit wins, as with IOU.
|
explicit edit wins, as with IOU.
|
||||||
@ -175,10 +178,10 @@ diagnosing wiring issues).
|
|||||||
ID (`aabb.cc{app}{iface}`), e.g. `aabb.cc03.0400`. The controller
|
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
|
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
|
(512–1022, disjoint from IOU's 1–511, limit 511 IOL Docker nodes across
|
||||||
opened projects sharing computes); without an allocation (raw compute API,
|
opened projects sharing computes). Starting a node without an allocation
|
||||||
pre-allocation topologies) a stable node-derived fallback in the same
|
(raw compute API, pre-allocation topologies) is an error, not a fallback —
|
||||||
range is used. Nodes sharing an ID would silently drop each other's
|
an uncoordinated ID could collide with the pool and make nodes silently
|
||||||
frames as MAC loops.
|
drop each other's frames as MAC loops.
|
||||||
* **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0`
|
* **Interface names are IOL-style `Ethernet0/0`**, not `GigabitEthernet0/0`
|
||||||
(4 ports per unit, matching the adapter-count granularity) — startup
|
(4 ports per unit, matching the adapter-count granularity) — startup
|
||||||
configs addressing `GigabitEthernet…` are rejected by the parser.
|
configs addressing `GigabitEthernet…` are rejected by the parser.
|
||||||
|
|||||||
@ -31,8 +31,9 @@ wired by the generic ``GNS3_UNIX_SOCKET_NIO`` capability of VendorDockerVM
|
|||||||
(uBridge reaches them through a per-node runtime directory bound at /tmp —
|
(uBridge reaches them through a per-node runtime directory bound at /tmp —
|
||||||
see ``VendorDockerVM._unix_socket_host_dir``). The controller allocates the
|
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
|
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
|
linked nodes get distinct MACs; starting a node without an allocation is an
|
||||||
fallback is used.
|
error, not a fallback — an uncoordinated id could collide with the pool and
|
||||||
|
blackhole traffic as a MAC loop.
|
||||||
|
|
||||||
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
|
This class is selected by the ``GNS3_IOL_RUNNER=1`` environment marker.
|
||||||
"""
|
"""
|
||||||
@ -84,6 +85,17 @@ class IOLDockerVM(VendorDockerVM):
|
|||||||
# The runner launches IOL with a fixed 256KB nvram (-n 256)
|
# The runner launches IOL with a fixed 256KB nvram (-n 256)
|
||||||
_IOL_NVRAM_SIZE_KB = 256
|
_IOL_NVRAM_SIZE_KB = 256
|
||||||
|
|
||||||
|
# Payload-delivered state. Deliberately NOT initialized in
|
||||||
|
# _parse_vendor_environment(): create() re-runs that parser on every
|
||||||
|
# (re)create (a stop removes the container, so every start recreates it)
|
||||||
|
# to pick up environment changes — resetting these there would lose the
|
||||||
|
# controller-allocated application id (MACs would flip to the fallback
|
||||||
|
# hash, colliding with the allocation pool) and any pending
|
||||||
|
# startup-config delivered by a PUT.
|
||||||
|
_application_id = None
|
||||||
|
_startup_config_content = None
|
||||||
|
_startup_config_dirty = False
|
||||||
|
|
||||||
def _parse_vendor_environment(self):
|
def _parse_vendor_environment(self):
|
||||||
|
|
||||||
super()._parse_vendor_environment()
|
super()._parse_vendor_environment()
|
||||||
@ -106,28 +118,17 @@ class IOLDockerVM(VendorDockerVM):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
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
|
|
||||||
|
|
||||||
# Startup-config content materialized by the controller from the file
|
|
||||||
# referenced by GNS3_IOL_STARTUP_CONFIG. Applied to the NVRAM at start
|
|
||||||
# time (not in the setter: the application id may not be final yet
|
|
||||||
# when the create payload is applied field by field).
|
|
||||||
self._startup_config_content = None
|
|
||||||
self._startup_config_dirty = False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def application_id(self) -> int:
|
def application_id(self) -> int:
|
||||||
"""
|
"""
|
||||||
IOL application ID: drives interface MACs (aabb.cc{app}{iface}) and
|
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
|
the NVRAM file name. Allocated by the controller from the IOL Docker
|
||||||
IOL Docker half of the id space when no allocation is available
|
half of the id space (disjoint from IOU's) — there is deliberately
|
||||||
(raw compute API use, topologies created before allocation existed).
|
no fallback: an id derived any other way could silently collide with
|
||||||
|
an allocation and blackhole traffic as a MAC loop. Starting a node
|
||||||
|
without one raises (see _prepare_iol_runtime).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._application_id is None:
|
|
||||||
return 512 + int(self.id.replace("-", ""), 16) % 511
|
|
||||||
return self._application_id
|
return self._application_id
|
||||||
|
|
||||||
@application_id.setter
|
@application_id.setter
|
||||||
@ -197,7 +198,7 @@ class IOLDockerVM(VendorDockerVM):
|
|||||||
start pushes the new content with the already-updated name.
|
start pushes the new content with the already-updated name.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._startup_config_dirty:
|
if not self._startup_config_dirty and self._application_id is not None:
|
||||||
nvram_file = self._iol_nvram_file()
|
nvram_file = self._iol_nvram_file()
|
||||||
if os.path.exists(nvram_file):
|
if os.path.exists(nvram_file):
|
||||||
try:
|
try:
|
||||||
@ -302,6 +303,15 @@ class IOLDockerVM(VendorDockerVM):
|
|||||||
except DockerHttp404Error:
|
except DockerHttp404Error:
|
||||||
state = "stopped"
|
state = "stopped"
|
||||||
|
|
||||||
|
if self._application_id is None:
|
||||||
|
raise DockerError(
|
||||||
|
f"IOL container '{self._name}' has no application ID: nodes must be "
|
||||||
|
"created through the controller (which allocates one from the pool "
|
||||||
|
"shared with IOU), or created with an explicit application_id "
|
||||||
|
"(512-1022) on the compute API. Without a coordinated ID two nodes "
|
||||||
|
"would share MACs and drop each other's frames as loops."
|
||||||
|
)
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,8 @@ def _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1",
|
|||||||
extra_volumes=extra_volumes or [], adapters=adapters,
|
extra_volumes=extra_volumes or [], adapters=adapters,
|
||||||
)
|
)
|
||||||
vm._cid = "e90e34656842"
|
vm._cid = "e90e34656842"
|
||||||
|
# mirrors the controller flow, which always delivers an allocated id
|
||||||
|
vm.application_id = 700
|
||||||
return vm
|
return vm
|
||||||
|
|
||||||
|
|
||||||
@ -263,7 +265,7 @@ async def test_start_writes_iol_config(compute_project, manager):
|
|||||||
assert config["binary"] == "/binary.iol"
|
assert config["binary"] == "/binary.iol"
|
||||||
assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit
|
assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit
|
||||||
assert config["num-serial"] == 0
|
assert config["num-serial"] == 0
|
||||||
assert config["local-app"] == 512 + int(vm.id.replace("-", ""), 16) % 511
|
assert config["local-app"] == 700
|
||||||
assert config["remote-app"] == 1023
|
assert config["remote-app"] == 1023
|
||||||
assert config["memory"] == 2048
|
assert config["memory"] == 2048
|
||||||
assert config["user-id"] == os.getuid()
|
assert config["user-id"] == os.getuid()
|
||||||
@ -271,30 +273,33 @@ async def test_start_writes_iol_config(compute_project, manager):
|
|||||||
assert vm.status == "started"
|
assert vm.status == "started"
|
||||||
|
|
||||||
|
|
||||||
def test_local_app_is_distinct_per_node(compute_project, manager):
|
@pytest.mark.asyncio
|
||||||
# IOL derives interface MACs from the app ID: two nodes sharing one would
|
async def test_missing_application_id_is_an_actionable_error(compute_project, manager):
|
||||||
# drop each other's frames as MAC loops, so IDs must differ per node.
|
# There is no fallback id on purpose: an uncoordinated one could collide
|
||||||
vm1 = _make_vm(compute_project, manager)
|
# with a pool allocation and blackhole traffic as a MAC loop. Nodes come
|
||||||
vm2 = _make_vm(compute_project, manager)
|
# through the controller, which always allocates.
|
||||||
assert vm1.application_id != vm2.application_id
|
vm = _make_vm(compute_project, manager)
|
||||||
for vm in (vm1, vm2):
|
vm._application_id = None
|
||||||
assert 512 <= vm.application_id <= 1022
|
vm._get_container_state = AsyncioMagicMock(return_value="stopped")
|
||||||
|
|
||||||
|
with pytest.raises(DockerError, match="no application ID"):
|
||||||
|
await vm._prepare_iol_runtime()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_allocated_application_id_overrides_fallback(compute_project, manager):
|
async def test_allocated_application_id_used(compute_project, manager):
|
||||||
# The controller allocates the application ID (upper half of the id
|
# The controller allocates the application ID (upper half of the id
|
||||||
# space); the node-derived hash is only a fallback without one.
|
# space, disjoint from IOU's); the node uses it verbatim.
|
||||||
vm = _make_vm(compute_project, manager)
|
vm = _make_vm(compute_project, manager)
|
||||||
vm.application_id = 700
|
vm.application_id = 701
|
||||||
assert vm.application_id == 700
|
assert vm.application_id == 701
|
||||||
_mock_start(vm)
|
_mock_start(vm)
|
||||||
with patch("gns3server.compute.docker.Docker.install_busybox"):
|
with patch("gns3server.compute.docker.Docker.install_busybox"):
|
||||||
with asyncio_patch("gns3server.compute.docker.Docker.query"):
|
with asyncio_patch("gns3server.compute.docker.Docker.query"):
|
||||||
await vm.start()
|
await vm.start()
|
||||||
with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f:
|
with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
assert config["local-app"] == 700
|
assert config["local-app"] == 701
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -734,3 +739,22 @@ def test_asdict_exposes_startup_config_content(compute_project, manager):
|
|||||||
assert vm.asdict()["startup_config_content"] is None
|
assert vm.asdict()["startup_config_content"] is None
|
||||||
vm.startup_config_content = "hostname RouterOne"
|
vm.startup_config_content = "hostname RouterOne"
|
||||||
assert vm.asdict()["startup_config_content"] == "hostname RouterOne"
|
assert vm.asdict()["startup_config_content"] == "hostname RouterOne"
|
||||||
|
|
||||||
|
|
||||||
|
def test_reparse_on_recreate_keeps_payload_state(compute_project, manager):
|
||||||
|
# create() re-runs _parse_vendor_environment on every (re)create (a stop
|
||||||
|
# removes the container, so every start recreates it): payload-delivered
|
||||||
|
# state must survive the re-parse. Losing the allocated application id
|
||||||
|
# would flip MACs to the fallback hash (colliding with the allocation
|
||||||
|
# pool); losing the startup-config would drop pending PUT edits.
|
||||||
|
vm = _make_vm(compute_project, manager)
|
||||||
|
vm.application_id = 700
|
||||||
|
vm.startup_config_content = "hostname RouterOne"
|
||||||
|
|
||||||
|
vm._parse_vendor_environment()
|
||||||
|
|
||||||
|
assert vm.application_id == 700
|
||||||
|
assert vm.startup_config_content == "hostname RouterOne"
|
||||||
|
assert vm._startup_config_dirty is True
|
||||||
|
# environment-derived state still re-derives
|
||||||
|
assert vm._gns3_init is False and vm._unix_socket_nio is True
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user