mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
Merge branch '3.1' into fork/markparonyan/api-issue-10-0eztih
# Conflicts: # gns3server/schemas/__init__.py
This commit is contained in:
110 files changed
+21567
-701
No files matched your search
@@ -99,6 +99,40 @@ class TestComputeRoutes:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["compute_id"] == str(test_compute.compute_id)
|
||||
|
||||
async def test_compute_get_reports_every_capability(self, app: FastAPI, client: AsyncClient, controller) -> None:
|
||||
"""
|
||||
The capabilities payload the compute reports must survive the
|
||||
response model intact. The local compute is the interesting one: it
|
||||
is the controller object serialized straight through, and a second,
|
||||
private Capabilities definition in the response schema silently
|
||||
stripped the kernel-datapath fields (ubridge_tap, ubridge_iol_tap)
|
||||
there — links were then built on a datapath clients could not see.
|
||||
"""
|
||||
|
||||
await controller.add_compute(
|
||||
compute_id="local", name="local", host="127.0.0.1", port=3080, force=True, connect=False
|
||||
)
|
||||
controller.get_compute("local")._capabilities.update(
|
||||
{
|
||||
"version": "3.1.0",
|
||||
"node_types": ["docker"],
|
||||
"platform": "linux",
|
||||
"cpus": 4,
|
||||
"memory": 1024,
|
||||
"disk_size": 2048,
|
||||
"ubridge_tc": {"netem": ["delay"], "ebpf": True, "ebpf_modes": ["nth"], "cbpf": True},
|
||||
"ubridge_tap": True,
|
||||
"ubridge_iol_tap": False,
|
||||
}
|
||||
)
|
||||
|
||||
response = await client.get(app.url_path_for("get_compute", compute_id="local"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
capabilities = response.json()["capabilities"]
|
||||
assert capabilities["ubridge_tap"] is True
|
||||
assert capabilities["ubridge_iol_tap"] is False
|
||||
assert capabilities["ubridge_tc"]["netem"] == ["delay"]
|
||||
|
||||
async def test_compute_get_local(self, app: FastAPI, client: AsyncClient, controller) -> None:
|
||||
|
||||
await controller.add_compute(
|
||||
|
||||
@@ -81,6 +81,45 @@ class TestLinkRoutes:
|
||||
assert len(project.links) == 1
|
||||
assert list(project.links.values())[0].filters == filters
|
||||
|
||||
async def test_create_links_batch(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
|
||||
) -> None:
|
||||
|
||||
node1, node2 = nodes
|
||||
valid = {
|
||||
"nodes": [
|
||||
{"node_id": node1.id, "adapter_number": 0, "port_number": 3},
|
||||
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
|
||||
]
|
||||
}
|
||||
same_node = {
|
||||
"nodes": [
|
||||
{"node_id": node1.id, "adapter_number": 0, "port_number": 3},
|
||||
{"node_id": node1.id, "adapter_number": 0, "port_number": 3},
|
||||
]
|
||||
}
|
||||
unknown_node = {
|
||||
"nodes": [
|
||||
{"node_id": str(uuid.uuid4()), "adapter_number": 0, "port_number": 3},
|
||||
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
|
||||
]
|
||||
}
|
||||
|
||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create"):
|
||||
response = await client.post(
|
||||
app.url_path_for("create_links", project_id=project.id), json=[same_node, unknown_node, valid]
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.json()
|
||||
assert [r["status_code"] for r in results] == [409, 404, 201]
|
||||
assert results[0]["link"] is None
|
||||
assert results[0]["error"]["message"]
|
||||
assert results[1]["error"]["message"]
|
||||
assert results[2]["error"] is None
|
||||
assert results[2]["link"]["link_id"] in project.links
|
||||
assert len(project.links) == 1
|
||||
|
||||
async def test_create_link_failure(
|
||||
self, app: FastAPI, client: AsyncClient, compute: Compute, project: Project
|
||||
) -> None:
|
||||
@@ -243,9 +282,36 @@ class TestLinkRoutes:
|
||||
assert mock.called
|
||||
link_id = response.json()["link_id"]
|
||||
assert response.json()["nodes"][0]["label"]["x"] == 42
|
||||
# the runtime datapath fact survives the response model on both POST and GET
|
||||
assert response.json()["kernel_datapath"] is False
|
||||
response = await client.get(app.url_path_for("get_link", project_id=project.id, link_id=link_id))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["nodes"][0]["label"]["x"] == 42
|
||||
assert response.json()["kernel_datapath"] is False
|
||||
|
||||
async def test_get_link_kernel_datapath(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
|
||||
) -> None:
|
||||
"""A kernel-wired link reports kernel_datapath true (was dropped by the schema)."""
|
||||
|
||||
node1, node2 = nodes
|
||||
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create"):
|
||||
response = await client.post(
|
||||
app.url_path_for("create_link", project_id=project.id),
|
||||
json={
|
||||
"nodes": [
|
||||
{"node_id": node1.id, "adapter_number": 0, "port_number": 3},
|
||||
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
link_id = response.json()["link_id"]
|
||||
link = project.get_link(link_id)
|
||||
link._link_data = [{"type": "nio_bridge"}]
|
||||
response = await client.get(app.url_path_for("get_link", project_id=project.id, link_id=link_id))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["kernel_datapath"] is True
|
||||
|
||||
async def test_link_etag_and_if_match(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
|
||||
@@ -515,7 +581,10 @@ class TestLinkRoutes:
|
||||
{"frequency_drop": [32768]},
|
||||
{"delay": [32768, 0]},
|
||||
{"delay": [10, -1]},
|
||||
{"packet_loss": [1, 2]},
|
||||
# a second packet_loss value (correlation) is valid — it is a
|
||||
# kernel-datapath parameter the controller accepts; gemodel out
|
||||
# of range stands in for the invalid slot instead
|
||||
{"gemodel": [101, 50, 50]},
|
||||
{"bpf": [1]},
|
||||
{"packet_loss": ["abc"]},
|
||||
],
|
||||
@@ -529,7 +598,9 @@ class TestLinkRoutes:
|
||||
response = await client.put(
|
||||
app.url_path_for("update_link", project_id=project.id, link_id=link.id), json={"filters": filters}
|
||||
)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||
# The filter vocabulary is capability-gated per link, so values are
|
||||
# validated by the controller (409), not by a strict request schema
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert "message" in response.json()
|
||||
assert link.filters == {}
|
||||
|
||||
@@ -541,10 +612,24 @@ class TestLinkRoutes:
|
||||
app.url_path_for("update_link", project_id=project.id, link_id=link.id),
|
||||
json={"filters": {"unknown": [1]}},
|
||||
)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||
assert response.status_code == status.HTTP_409_CONFLICT
|
||||
assert "message" in response.json()
|
||||
assert link.filters == {}
|
||||
|
||||
async def test_update_link_packet_loss_correlation(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project
|
||||
) -> None:
|
||||
# The optional packet_loss correlation (second value) is a
|
||||
# kernel-datapath parameter accepted by the controller
|
||||
link = Link(project)
|
||||
project._links = {link.id: link}
|
||||
response = await client.put(
|
||||
app.url_path_for("update_link", project_id=project.id, link_id=link.id),
|
||||
json={"filters": {"packet_loss": [1, 2]}},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["filters"] == {"packet_loss": [1, 2]}
|
||||
|
||||
async def test_update_link_invalid_delay_latency(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
|
||||
|
||||
link = Link(project)
|
||||
@@ -557,9 +642,12 @@ class TestLinkRoutes:
|
||||
async def test_openapi_link_filters(self, app: FastAPI) -> None:
|
||||
|
||||
schemas = app.openapi()["components"]["schemas"]
|
||||
link_filters = schemas["LinkFilters"]
|
||||
assert link_filters["additionalProperties"] is False
|
||||
assert set(link_filters["properties"]) == {"frequency_drop", "packet_loss", "delay", "corrupt", "bpf"}
|
||||
# The filters vocabulary is capability-gated per link and validated by
|
||||
# the controller, so the Link schema carries it as a free-form object
|
||||
# (no strict LinkFilters model on the request path)
|
||||
assert "LinkFilters" not in schemas
|
||||
filters_property = schemas["Link"]["properties"]["filters"]
|
||||
assert filters_property["anyOf"][0] == {"additionalProperties": True, "type": "object"}
|
||||
assert schemas["LinkFilterDefinition"]["properties"]["parameters"]["items"] == {
|
||||
"$ref": "#/components/schemas/LinkFilterParameter"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ from pydantic import SecretStr
|
||||
from gns3server.api.routes.controller.nodes import vnc_console, ws_console
|
||||
from gns3server.config import Config
|
||||
from gns3server.controller.compute import Compute
|
||||
from gns3server.controller.controller_error import ComputeError
|
||||
from gns3server.controller.controller_error import ComputeError, ControllerError
|
||||
from gns3server.controller.node import Node
|
||||
from gns3server.controller.project import Project
|
||||
from gns3server.services import auth_service
|
||||
@@ -73,6 +73,103 @@ class TestNodeRoutes:
|
||||
assert response.json()["name"] == "test"
|
||||
assert "name" not in response.json()["properties"]
|
||||
|
||||
async def test_create_node_taken_name_is_renamed(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
|
||||
) -> None:
|
||||
|
||||
response = MagicMock()
|
||||
response.json = {"console": 2048}
|
||||
compute.post = AsyncioMagicMock(return_value=response)
|
||||
payload = {"name": "R1", "node_type": "vpcs", "compute_id": "example.com"}
|
||||
|
||||
first = await client.post(app.url_path_for("create_node", project_id=project.id), json=payload)
|
||||
second = await client.post(app.url_path_for("create_node", project_id=project.id), json=payload)
|
||||
|
||||
assert first.json()["name"] == "R1"
|
||||
assert second.status_code == status.HTTP_201_CREATED
|
||||
assert second.json()["name"] == "R2"
|
||||
|
||||
async def test_create_node_strict_names(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
|
||||
) -> None:
|
||||
|
||||
response = MagicMock()
|
||||
response.json = {"console": 2048}
|
||||
compute.post = AsyncioMagicMock(return_value=response)
|
||||
payload = {"name": "R1", "node_type": "vpcs", "compute_id": "example.com", "strict_names": True}
|
||||
|
||||
first = await client.post(app.url_path_for("create_node", project_id=project.id), json=payload)
|
||||
second = await client.post(app.url_path_for("create_node", project_id=project.id), json=payload)
|
||||
|
||||
assert first.status_code == status.HTTP_201_CREATED
|
||||
assert second.status_code == status.HTTP_409_CONFLICT
|
||||
assert "R1" in second.json()["message"]
|
||||
assert len(project.nodes) == 1
|
||||
|
||||
async def test_create_nodes_batch(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
|
||||
) -> None:
|
||||
|
||||
response = MagicMock()
|
||||
response.json = {"console": 2048}
|
||||
compute.post = AsyncioMagicMock(return_value=response)
|
||||
nodes = [{"name": "R{0}", "node_type": "vpcs", "compute_id": "example.com"} for _ in range(3)]
|
||||
|
||||
response = await client.post(app.url_path_for("create_nodes", project_id=project.id), json=nodes)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.json()
|
||||
assert [r["status_code"] for r in results] == [201, 201, 201]
|
||||
assert [r["node"]["name"] for r in results] == ["R1", "R2", "R3"]
|
||||
assert all(r["error"] is None for r in results)
|
||||
assert len(project.nodes) == 3
|
||||
|
||||
async def test_create_nodes_batch_item_failures_keep_order(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
|
||||
) -> None:
|
||||
|
||||
response = MagicMock()
|
||||
response.json = {"console": 2048}
|
||||
compute.post = AsyncioMagicMock(return_value=response)
|
||||
nodes = [
|
||||
{"name": "R1", "node_type": "vpcs", "compute_id": "example.com", "strict_names": True},
|
||||
{"name": "R1", "node_type": "vpcs", "compute_id": "example.com", "strict_names": True},
|
||||
{"name": "R1", "node_type": "vpcs", "compute_id": "unknown"},
|
||||
{"name": "R1", "node_type": "vpcs", "compute_id": "example.com"},
|
||||
]
|
||||
|
||||
response = await client.post(app.url_path_for("create_nodes", project_id=project.id), json=nodes)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
results = response.json()
|
||||
assert [r["status_code"] for r in results] == [201, 409, 404, 201]
|
||||
assert results[0]["node"]["name"] == "R1"
|
||||
assert results[1]["node"] is None
|
||||
assert "R1" in results[1]["error"]["message"]
|
||||
assert results[2]["error"]["message"]
|
||||
assert results[3]["node"]["name"] == "R2"
|
||||
assert len(project.nodes) == 2
|
||||
|
||||
async def test_create_nodes_batch_failed_node_releases_name(
|
||||
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
|
||||
) -> None:
|
||||
|
||||
compute.post = AsyncioMagicMock(side_effect=ControllerError("compute failure"))
|
||||
nodes = [{"name": "R1", "node_type": "vpcs", "compute_id": "example.com", "strict_names": True}]
|
||||
|
||||
response = await client.post(app.url_path_for("create_nodes", project_id=project.id), json=nodes)
|
||||
|
||||
assert response.json()[0]["status_code"] == status.HTTP_409_CONFLICT
|
||||
assert len(project.nodes) == 0
|
||||
|
||||
response_ok = MagicMock()
|
||||
response_ok.json = {"console": 2048}
|
||||
compute.post = AsyncioMagicMock(return_value=response_ok)
|
||||
response = await client.post(app.url_path_for("create_nodes", project_id=project.id), json=nodes)
|
||||
|
||||
assert response.json()[0]["status_code"] == status.HTTP_201_CREATED
|
||||
assert response.json()[0]["node"]["name"] == "R1"
|
||||
|
||||
async def test_list_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None:
|
||||
|
||||
response = MagicMock()
|
||||
|
||||
Reference in new issue
Block a user