From 4fa7f158fd569b5c869fc6ea1f7faf0478007b70 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Thu, 1 Oct 2026 01:36:52 +0800 Subject: [PATCH] fix(schemas): one definition of the capabilities payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /computes/{id} answered with only part of the capabilities the compute reports: the response schema kept a second, private Capabilities class of its own (it knew ubridge_tc and nothing else), and FastAPI's response model silently drops fields it does not define — so ubridge_tap and ubridge_iol_tap never reached clients even though the compute had sent them and links were already being built on those datapaths. Import the one definition instead of duplicating it, so the next capability cannot drift the same way. --- gns3server/schemas/controller/computes.py | 23 +++---------- tests/api/routes/controller/test_computes.py | 34 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/gns3server/schemas/controller/computes.py b/gns3server/schemas/controller/computes.py index 836ac9607..4924827e5 100644 --- a/gns3server/schemas/controller/computes.py +++ b/gns3server/schemas/controller/computes.py @@ -21,8 +21,11 @@ from typing import Any, List, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from .base import DateTimeModelMixin -from .capabilities import UbridgeTcCapabilities -from .nodes import NodeType +# The capabilities payload has one definition (controller.capabilities): a +# second, private copy here silently stripped every field it did not know +# about from GET /computes/{id} — which is how ubridge_tap and +# ubridge_iol_tap went missing while the compute returned them. +from .capabilities import Capabilities class Protocol(str, Enum): @@ -93,22 +96,6 @@ class ComputeUpdate(ComputeBase): ) -class Capabilities(BaseModel): - """ - Capabilities supported by a compute. - """ - - version: str = Field(..., description="Compute version number") - node_types: List[NodeType] = Field(..., description="Node types supported by the compute") - platform: str = Field(..., description="Platform where the compute is running (Linux, Windows or macOS)") - cpus: int = Field(..., description="Number of CPUs on this compute") - memory: int = Field(..., description="Amount of memory on this compute") - disk_size: int = Field(..., description="Disk size on this compute") - ubridge_tc: Optional[UbridgeTcCapabilities] = Field( - None, description="uBridge tc-module capabilities; absent when the probe failed or uBridge has no tc module" - ) - - class Compute(DateTimeModelMixin, ComputeBase): """ Data returned for a compute. diff --git a/tests/api/routes/controller/test_computes.py b/tests/api/routes/controller/test_computes.py index 3c213c07c..83ced12b7 100644 --- a/tests/api/routes/controller/test_computes.py +++ b/tests/api/routes/controller/test_computes.py @@ -74,6 +74,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(