mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-18 17:02:26 +03:00
Updating any Docker node property destroys and recreates the container (the route calls DockerVM.update() as soon as the property diff is non-empty), so it discards everything written inside it. On a running node that happened silently, as a side effect of an unrelated edit: the user only saw the node restart. Refuse the update while the container is up and say what stopping it is for, like QEMU, IOU, Dynamips, VPCS, VirtualBox and VMware already do for their own restart-requiring settings. The check runs before anything is applied — a refused update leaves the node untouched — and only when the PUT really changes a property, since Web clients send the whole node back on every edit. is_running() is not reusable for the question: it reports a paused container as not running and stops crashed containers as a side effect, so DockerVM gains is_up() for "running or paused".
512 lines
23 KiB
Python
512 lines
23 KiB
Python
#
|
|
# Copyright (C) 2020 GNS3 Technologies Inc.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# 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 unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from fastapi import FastAPI, status
|
|
from httpx import AsyncClient
|
|
|
|
from gns3server.compute.docker import Docker
|
|
from gns3server.compute.docker.iol_docker_vm import IOLDockerVM
|
|
from gns3server.compute.project import Project
|
|
from tests.utils import AsyncioMagicMock, asyncio_patch
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
class TestDockerNodesRoutes:
|
|
@pytest.fixture
|
|
def base_params(self) -> dict:
|
|
"""Return standard parameters"""
|
|
|
|
params = {
|
|
"name": "DOCKER-TEST-1",
|
|
"image": "nginx",
|
|
"start_command": "nginx-daemon",
|
|
"adapters": 2,
|
|
"environment": "YES=1\nNO=0",
|
|
"console_type": "telnet",
|
|
"console_resolution": "1280x1024",
|
|
"extra_hosts": "test:127.0.0.1",
|
|
}
|
|
return params
|
|
|
|
# @pytest.yield_fixture(autouse=True)
|
|
# def mock_connection():
|
|
#
|
|
# docker = Docker.instance()
|
|
# docker._connected = True
|
|
# docker._connector = MagicMock()
|
|
# yield
|
|
# Docker._instance = None
|
|
|
|
@pytest_asyncio.fixture
|
|
async def vm(self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict) -> dict:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]):
|
|
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}):
|
|
with asyncio_patch("gns3server.compute.docker.DockerVM._get_container_state", return_value="exited"):
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:create_docker_node", project_id=compute_project.id), json=base_params
|
|
)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
return response.json()
|
|
|
|
async def test_docker_create(
|
|
self, app: FastAPI, compute_client: AsyncClient, compute_project: Project, base_params: dict
|
|
) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]):
|
|
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}):
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:create_docker_node", project_id=compute_project.id), json=base_params
|
|
)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.json()["name"] == "DOCKER-TEST-1"
|
|
assert response.json()["project_id"] == compute_project.id
|
|
assert response.json()["container_id"] == "8bd8153ea8f5"
|
|
assert response.json()["image"] == "nginx:latest"
|
|
assert response.json()["adapters"] == 2
|
|
assert response.json()["environment"] == "YES=1\nNO=0"
|
|
assert response.json()["console_resolution"] == "1280x1024"
|
|
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",
|
|
(
|
|
("valid-name.com", status.HTTP_201_CREATED),
|
|
("42name", status.HTTP_201_CREATED),
|
|
("424242", status.HTTP_409_CONFLICT),
|
|
("name42", status.HTTP_201_CREATED),
|
|
("name.424242", status.HTTP_409_CONFLICT),
|
|
("-name", status.HTTP_409_CONFLICT),
|
|
("name%-test", status.HTTP_409_CONFLICT),
|
|
("x" * 63, status.HTTP_201_CREATED),
|
|
("x" * 64, status.HTTP_409_CONFLICT),
|
|
(("x" * 62 + ".") * 4, status.HTTP_201_CREATED),
|
|
("xx" + ("x" * 62 + ".") * 4, status.HTTP_409_CONFLICT),
|
|
),
|
|
)
|
|
async def test_docker_create_with_invalid_name(
|
|
self,
|
|
app: FastAPI,
|
|
compute_client: AsyncClient,
|
|
compute_project: Project,
|
|
base_params: dict,
|
|
name: str,
|
|
status_code: int,
|
|
) -> None:
|
|
|
|
base_params["name"] = name
|
|
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]):
|
|
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}):
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:create_docker_node", project_id=compute_project.id), json=base_params
|
|
)
|
|
assert response.status_code == status_code
|
|
|
|
async def test_docker_start(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.start", return_value=True) as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:start_docker_node", project_id=vm["project_id"], node_id=vm["node_id"])
|
|
)
|
|
assert mock.called
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_docker_stop(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.stop", return_value=True) as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:stop_docker_node", project_id=vm["project_id"], node_id=vm["node_id"])
|
|
)
|
|
assert mock.called
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_docker_reload(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.restart", return_value=True) as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:reload_docker_node", project_id=vm["project_id"], node_id=vm["node_id"])
|
|
)
|
|
assert mock.called
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_docker_delete(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.delete", return_value=True) as mock:
|
|
response = await compute_client.delete(
|
|
app.url_path_for("compute:delete_docker_node", project_id=vm["project_id"], node_id=vm["node_id"])
|
|
)
|
|
assert mock.called
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_docker_pause(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.pause", return_value=True) as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:pause_docker_node", project_id=vm["project_id"], node_id=vm["node_id"])
|
|
)
|
|
assert mock.called
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_docker_unpause(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.unpause", return_value=True) as mock:
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:unpause_docker_node", project_id=vm["project_id"], node_id=vm["node_id"])
|
|
)
|
|
assert mock.called
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_docker_nio_create_udp(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
params = {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"}
|
|
|
|
url = app.url_path_for(
|
|
"compute:create_docker_node_nio",
|
|
project_id=vm["project_id"],
|
|
node_id=vm["node_id"],
|
|
adapter_number="0",
|
|
port_number="0",
|
|
)
|
|
response = await compute_client.post(url, json=params)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.json()["type"] == "nio_udp"
|
|
|
|
async def test_docker_nio_create_bridge_requires_the_field_but_takes_null(
|
|
self, app: FastAPI, compute_client: AsyncClient, vm: dict
|
|
) -> None:
|
|
"""
|
|
BridgeNIO.bridge is required-but-nullable: an omitted key is a 422
|
|
(the NIO factories read it, and the routes drop unset fields — an
|
|
omitted key used to pass validation and die as a KeyError during
|
|
binding), while an explicit null is the absorbed-link shape.
|
|
"""
|
|
|
|
url = app.url_path_for(
|
|
"compute:create_docker_node_nio",
|
|
project_id=vm["project_id"],
|
|
node_id=vm["node_id"],
|
|
adapter_number="0",
|
|
port_number="0",
|
|
)
|
|
|
|
response = await compute_client.post(url, json={"type": "nio_bridge"})
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
|
|
|
response = await compute_client.post(url, json={"type": "nio_bridge", "bridge": None})
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.json()["bridge"] is None
|
|
|
|
async def test_docker_nio_batch_create(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
"""
|
|
Exercise the project-wide batch NIO endpoint: bind two NIOs on the same
|
|
docker node in a single request (the path used during project open).
|
|
"""
|
|
|
|
params = {
|
|
"nios": [
|
|
{
|
|
"node_id": vm["node_id"],
|
|
"adapter_number": 0,
|
|
"port_number": 0,
|
|
"nio": {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"},
|
|
},
|
|
{
|
|
"node_id": vm["node_id"],
|
|
"adapter_number": 1,
|
|
"port_number": 0,
|
|
"nio": {"type": "nio_udp", "lport": 4243, "rport": 4344, "rhost": "127.0.0.1"},
|
|
},
|
|
]
|
|
}
|
|
|
|
url = app.url_path_for("compute:create_batch_nios", project_id=vm["project_id"])
|
|
response = await compute_client.post(url, json=params)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.json()["added"] == 2
|
|
|
|
async def test_docker_update_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
params = {
|
|
"type": "nio_udp",
|
|
"lport": 4242,
|
|
"rport": 4343,
|
|
"rhost": "127.0.0.1",
|
|
"filters": {"packet_loss": 10},
|
|
"suspend": False,
|
|
}
|
|
|
|
url = app.url_path_for(
|
|
"compute:create_docker_node_nio",
|
|
project_id=vm["project_id"],
|
|
node_id=vm["node_id"],
|
|
adapter_number="0",
|
|
port_number="0",
|
|
)
|
|
response = await compute_client.post(url, json=params)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.json()["filters"] == {"packet_loss": 10}
|
|
params["filters"] = {}
|
|
params["suspend"] = True
|
|
|
|
url = app.url_path_for(
|
|
"compute:update_docker_node_nio",
|
|
project_id=vm["project_id"],
|
|
node_id=vm["node_id"],
|
|
adapter_number="0",
|
|
port_number="0",
|
|
)
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.adapter_update_nio_binding"):
|
|
response = await compute_client.put(url, json=params)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.json()["type"] == "nio_udp"
|
|
assert response.json()["filters"] == {}
|
|
assert response.json()["suspend"] is True
|
|
|
|
async def test_docker_delete_nio(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
url = app.url_path_for(
|
|
"compute:delete_docker_node_nio",
|
|
project_id=vm["project_id"],
|
|
node_id=vm["node_id"],
|
|
adapter_number="0",
|
|
port_number="0",
|
|
)
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.adapter_remove_nio_binding"):
|
|
response = await compute_client.delete(url)
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
async def test_docker_update(
|
|
self, app: FastAPI, compute_client: AsyncClient, vm: dict, free_console_port: int
|
|
) -> None:
|
|
|
|
params = {
|
|
"name": "test",
|
|
"console": free_console_port,
|
|
"start_command": "yes",
|
|
"environment": "GNS3=1\nGNS4=0",
|
|
"extra_hosts": "test:127.0.0.1",
|
|
}
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.update") as mock:
|
|
response = await compute_client.put(
|
|
app.url_path_for("compute:update_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]),
|
|
json=params,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert mock.called
|
|
assert response.json()["name"] == "test"
|
|
assert response.json()["console"] == free_console_port
|
|
assert response.json()["start_command"] == "yes"
|
|
assert response.json()["environment"] == "GNS3=1\nGNS4=0"
|
|
assert response.json()["extra_hosts"] == "test:127.0.0.1"
|
|
|
|
async def test_docker_update_empty_strings_do_not_recreate_container(
|
|
self, app: FastAPI, compute_client: AsyncClient, compute_project: Project
|
|
) -> None:
|
|
"""
|
|
Web clients serialize empty form fields as "" while unset values are
|
|
stored as None on the node: a full PUT must not see a phantom change
|
|
and recreate the container for nothing.
|
|
"""
|
|
|
|
params = {"name": "DOCKER-EMPTY", "image": "nginx", "environment": ""}
|
|
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]):
|
|
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}):
|
|
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()["environment"] is None # "" normalized at creation
|
|
assert response.json()["console_http_path"] == "/"
|
|
node_id = response.json()["node_id"]
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.update") as mock:
|
|
response = await compute_client.put(
|
|
app.url_path_for("compute:update_docker_node", project_id=compute_project.id, node_id=node_id),
|
|
json={
|
|
"name": "DOCKER-EMPTY",
|
|
"start_command": "",
|
|
"environment": "",
|
|
"extra_hosts": "",
|
|
"console_http_path": "",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
assert not mock.called # no real change: the container must not be recreated
|
|
assert response.json()["start_command"] is None
|
|
assert response.json()["console_http_path"] == "/"
|
|
|
|
async def test_docker_update_refused_while_container_is_up(
|
|
self, app: FastAPI, compute_client: AsyncClient, vm: dict
|
|
) -> None:
|
|
"""
|
|
A property change is applied by recreating the container, which throws
|
|
away what was written inside it. While the container is up (running or
|
|
paused) the update is refused instead of resetting the node behind the
|
|
user's back, and the error says what stopping it would cost.
|
|
"""
|
|
|
|
for state in ("running", "paused"):
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.update") as mock:
|
|
with asyncio_patch(
|
|
"gns3server.compute.docker.docker_vm.DockerVM._get_container_state", return_value=state
|
|
):
|
|
response = await compute_client.put(
|
|
app.url_path_for(
|
|
"compute:update_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]
|
|
),
|
|
json={"console_type": "vnc"},
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
assert "must be stopped" in response.json()["message"]
|
|
assert "console_type" in response.json()["message"]
|
|
assert not mock.called
|
|
|
|
# the refused update left the node exactly as it was
|
|
response = await compute_client.get(
|
|
app.url_path_for("compute:get_docker_node", project_id=vm["project_id"], node_id=vm["node_id"])
|
|
)
|
|
assert response.json()["console_type"] == "telnet"
|
|
|
|
async def test_docker_update_without_change_is_allowed_while_container_is_up(
|
|
self, app: FastAPI, compute_client: AsyncClient, vm: dict
|
|
) -> None:
|
|
"""
|
|
Clients send the whole node back on every edit: a PUT that changes
|
|
nothing must not be refused just because the container is running.
|
|
"""
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM._get_container_state", return_value="running"):
|
|
response = await compute_client.put(
|
|
app.url_path_for("compute:update_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]),
|
|
json={"name": "DOCKER-TEST-1", "console_type": "telnet"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
async def test_docker_update_stopped_container_is_recreated(
|
|
self, app: FastAPI, compute_client: AsyncClient, vm: dict
|
|
) -> None:
|
|
"""A stopped container has no live state to lose: the change applies."""
|
|
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.update") as mock:
|
|
with asyncio_patch(
|
|
"gns3server.compute.docker.docker_vm.DockerVM._get_container_state", return_value="exited"
|
|
):
|
|
response = await compute_client.put(
|
|
app.url_path_for("compute:update_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]),
|
|
json={"console_type": "vnc"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert mock.called
|
|
assert response.json()["console_type"] == "vnc"
|
|
|
|
async def test_docker_start_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
url = app.url_path_for(
|
|
"compute:start_docker_node_capture",
|
|
project_id=vm["project_id"],
|
|
node_id=vm["node_id"],
|
|
adapter_number="0",
|
|
port_number="0",
|
|
)
|
|
|
|
with patch("gns3server.compute.docker.docker_vm.DockerVM.is_running", return_value=True):
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.start_capture") as mock:
|
|
params = {"capture_file_name": "test.pcap", "data_link_type": "DLT_EN10MB"}
|
|
response = await compute_client.post(url, json=params)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert mock.called
|
|
assert "test.pcap" in response.json()["pcap_file_path"]
|
|
|
|
async def test_docker_stop_capture(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None:
|
|
|
|
url = app.url_path_for(
|
|
"compute:stop_docker_node_capture",
|
|
project_id=vm["project_id"],
|
|
node_id=vm["node_id"],
|
|
adapter_number="0",
|
|
port_number="0",
|
|
)
|
|
|
|
with patch("gns3server.compute.docker.docker_vm.DockerVM.is_running", return_value=True):
|
|
with asyncio_patch("gns3server.compute.docker.docker_vm.DockerVM.stop_capture") as mock:
|
|
response = await compute_client.post(url)
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
assert mock.called
|
|
|
|
async def test_docker_duplicate(
|
|
self, app: FastAPI, compute_client: AsyncClient, vm: dict, base_params: dict
|
|
) -> None:
|
|
|
|
# create destination node first
|
|
with asyncio_patch("gns3server.compute.docker.Docker.list_images", return_value=[{"image": "nginx"}]):
|
|
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value={"Id": "8bd8153ea8f5"}):
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:create_docker_node", project_id=vm["project_id"]), json=base_params
|
|
)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
|
|
params = {"destination_node_id": response.json()["node_id"]}
|
|
response = await compute_client.post(
|
|
app.url_path_for("compute:duplicate_docker_node", project_id=vm["project_id"], node_id=vm["node_id"]),
|
|
json=params,
|
|
)
|
|
assert response.status_code == status.HTTP_201_CREATED
|