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:
YueGuobin committed 2026-09-05 21:54:52 +08:00
1 parent 8b3aafbbdc
commit 5685f40dd6
3 files changed
+75 -38

No files matched your search

+38 -14
View File
@@ -81,6 +81,8 @@ def _make_vm(compute_project, manager, environment="GNS3_IOL_RUNNER=1",
extra_volumes=extra_volumes or [], adapters=adapters,
)
vm._cid = "e90e34656842"
# mirrors the controller flow, which always delivers an allocated id
vm.application_id = 700
return vm
@@ -263,7 +265,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"] == 512 + int(vm.id.replace("-", ""), 16) % 511
assert config["local-app"] == 700
assert config["remote-app"] == 1023
assert config["memory"] == 2048
assert config["user-id"] == os.getuid()
@@ -271,30 +273,33 @@ async def test_start_writes_iol_config(compute_project, manager):
assert vm.status == "started"
def test_local_app_is_distinct_per_node(compute_project, manager):
# IOL derives interface MACs from the app ID: two nodes sharing one would
# 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)
assert vm1.application_id != vm2.application_id
for vm in (vm1, vm2):
assert 512 <= vm.application_id <= 1022
@pytest.mark.asyncio
async def test_missing_application_id_is_an_actionable_error(compute_project, manager):
# There is no fallback id on purpose: an uncoordinated one could collide
# with a pool allocation and blackhole traffic as a MAC loop. Nodes come
# through the controller, which always allocates.
vm = _make_vm(compute_project, manager)
vm._application_id = None
vm._get_container_state = AsyncioMagicMock(return_value="stopped")
with pytest.raises(DockerError, match="no application ID"):
await vm._prepare_iol_runtime()
@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
# 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.application_id = 700
assert vm.application_id == 700
vm.application_id = 701
assert vm.application_id == 701
_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
assert config["local-app"] == 701
@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
vm.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