docker: inject config files into containers via extra_configs

Add an `extra_configs` field (list of {target, content}) to the docker
node/template/appliance schemas. For each entry GNS3 writes `content` to a
file in the node working directory and bind-mounts it read-only at `target`
inside the container.

This lets a NOS appliance seed its startup config without rebuilding the
image: XRd points XR_FIRST_BOOT_CONFIG at an injected /firstboot.cfg, FRR at
/etc/frr/frr.conf, etc. The bind is a single-file mount applied at create
time, so it works for both the generic init.sh path and vendor nodes that
skip init.sh (console_type=docker_exec). Entries are only injected when
present, so ordinary nodes are unaffected.

The content can't go through `environment` (it is line-delimited, one var per
line), hence a dedicated field -- the same plumbing shape as extra_volumes.
This commit is contained in:
YueGuobin committed 2026-08-13 23:33:34 +08:00
1 parent 2bee34031c
commit 08e37a4509
7 files changed
+98 -3

No files matched your search

+39
View File
@@ -69,6 +69,7 @@ def test_json(vm, compute_project):
'console_http_path': '/',
'extra_hosts': None,
'extra_volumes': [],
'extra_configs': [],
'memory': 0,
'cpus': 0,
'aux': vm.aux,
@@ -307,6 +308,44 @@ async def test_create_applies_env_host_config(compute_project, manager):
), "GNS3_ user vars must not leak into the container environment"
@pytest.mark.asyncio
async def test_create_with_extra_configs(compute_project, manager):
"""
extra_configs entries are written to the node working directory and
bind-mounted read-only at their target path inside the container.
"""
extra_configs = [{"target": "/firstboot.cfg", "content": "username clab\n!\nend"}]
response = {"Id": "e90e34656806", "Warnings": []}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", extra_configs=extra_configs)
await vm.create()
mounts = mock.call_args[1]["data"]["HostConfig"]["Mounts"]
injected = [m for m in mounts if m.get("Target") == "/firstboot.cfg"]
assert len(injected) == 1
assert injected[0]["ReadOnly"] is True
with open(injected[0]["Source"]) as f:
assert f.read() == "username clab\n!\nend"
@pytest.mark.asyncio
async def test_create_with_extra_configs_invalid_target(compute_project, manager):
"""
An extra_configs target that is not absolute (or contains '..') is rejected.
"""
extra_configs = [{"target": "relative/path", "content": "x"}]
response = {"Id": "e90e34656806", "Warnings": []}
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "ubuntu"}]):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response):
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu", extra_configs=extra_configs)
with pytest.raises(DockerError):
await vm.create()
@pytest.mark.asyncio
async def test_create_with_colon_in_project_name(compute_project, manager):