feat: IOU-style startup-config for IOL Docker nodes

Templates reference a config file with the GNS3_IOL_STARTUP_CONFIG
environment knob; the controller materializes the file content into
startup_config_content on node creation (sent once, knob consumed —
the same pattern as the IOU startup_config mapping). The compute builds
the content into the node's nvram_<app id> at the next start using the
IOU nvram_import utility (IOL and IOU share the nvram container format,
verified against iol-xe 17.18.02): valid config at boot, no setup
dialog, %h hostname substitution, hostname rewrite on rename.

Semantics verified against the runner: IOL boots from NVRAM whenever it
holds a config, so a plain stop/start never re-applies the startup
config and 'write memory' survives restarts; an explicit content edit
(PUT) is re-applied on the next start and wins over the saved config,
like IOU.
This commit is contained in:
YueGuobin committed 2026-09-05 21:54:52 +08:00
1 parent 9147deccaa
commit 8b3aafbbdc
8 files changed
+440 -9

No files matched your search

+44 -2
View File
@@ -20,9 +20,11 @@ import pytest_asyncio
from fastapi import FastAPI, status
from httpx import AsyncClient
from tests.utils import asyncio_patch
from unittest.mock import patch
from tests.utils import asyncio_patch, AsyncioMagicMock
from unittest.mock import patch, MagicMock
from gns3server.compute.docker import Docker
from gns3server.compute.docker.iol_docker_vm import IOLDockerVM
from gns3server.compute.project import Project
pytestmark = pytest.mark.asyncio
@@ -98,6 +100,46 @@ class TestDockerNodesRoutes:
assert response.json()["extra_hosts"] == "test:127.0.0.1"
async def test_docker_create_iol_startup_config(
self, app: FastAPI,
compute_client: AsyncClient,
compute_project: Project,
base_params: dict
) -> None:
"""
The controller materializes the GNS3_IOL_STARTUP_CONFIG knob into
startup_config_content; the create route must carry it onto the
IOLDockerVM (plain Docker nodes have no such property and skip it).
"""
params = dict(base_params)
params["image"] = "iol-xe/iol-xe:17-18-02"
params["environment"] = "GNS3_IOL_RUNNER=1"
params["startup_config_content"] = "hostname %h\n"
create_response = {
"Id": "8bd8153ea8f5",
"Warnings": [],
"Config": {
"Entrypoint": ["/iol-runner", "-config", "/config/iol-config.json", "-stdio"],
"Cmd": [],
"Volumes": {},
},
}
seed_proc = MagicMock()
seed_proc.communicate = AsyncioMagicMock(return_value=(b"seedcid\n", b""))
seed_proc.returncode = 0
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "iol-xe/iol-xe"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=create_response):
with patch("asyncio.subprocess.create_subprocess_exec", return_value=seed_proc):
response = await compute_client.post(
app.url_path_for("compute:create_docker_node", project_id=compute_project.id), json=params
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["startup_config_content"] == "hostname %h\n"
vm = Docker.instance().get_node(response.json()["node_id"], project_id=str(compute_project.id))
assert isinstance(vm, IOLDockerVM)
assert vm.startup_config_content == "hostname %h\n"
@pytest.mark.parametrize(
"name, status_code",
(