Merge branch '3.1' into api-issue-07-x7z6we

This commit is contained in:
Jeremy Grossmann authored and GitHub committed 2026-10-08 19:10:42 +02:00
commit 2dd1652e8a
30 files changed
+549 -72

No files matched your search

+19 -2
View File
@@ -19,6 +19,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
# Check AI Copilot availability
from gns3server.agent import AI_COPILOT_AVAILABLE
from gns3server.api.operation_ids import add_stub_routes
from gns3server.config import Config
# Conditionally import AI-dependent routes
if AI_COPILOT_AVAILABLE:
@@ -33,6 +34,11 @@ else:
_copilot_router = APIRouter()
_llm_router = APIRouter()
@_chat_router.api_route("/{path:path}", methods=["GET", "POST", "DELETE", "PATCH", "PUT"], include_in_schema=False)
@_copilot_router.api_route(
"/{path:path}", methods=["GET", "POST", "DELETE", "PATCH", "PUT"], include_in_schema=False
)
@_llm_router.api_route("/{path:path}", methods=["GET", "POST", "DELETE", "PATCH", "PUT"], include_in_schema=False)
async def ai_not_available(path: str = ""):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
@@ -69,6 +75,8 @@ from . import (
)
from .dependencies.authentication import get_current_active_user
_include_ai_in_schema = Config.instance().settings.Server.openapi_include_ai
router = APIRouter()
router.include_router(controller.router, tags=["Controller"])
@@ -118,11 +126,19 @@ router.include_router(
)
router.include_router(
_llm_router, prefix="/access", dependencies=[Depends(get_current_active_user)], tags=["LLM Model Configurations"]
_llm_router,
prefix="/access",
dependencies=[Depends(get_current_active_user)],
tags=["LLM Model Configurations"],
include_in_schema=_include_ai_in_schema,
)
router.include_router(
_copilot_router, prefix="/copilot", dependencies=[Depends(get_current_active_user)], tags=["GNS3 Copilot"]
_copilot_router,
prefix="/copilot",
dependencies=[Depends(get_current_active_user)],
tags=["GNS3 Copilot"],
include_in_schema=_include_ai_in_schema,
)
router.include_router(
@@ -130,6 +146,7 @@ router.include_router(
prefix="/copilot/projects/{project_id}/chat",
dependencies=[Depends(get_current_active_user)],
tags=["GNS3 Copilot"],
include_in_schema=_include_ai_in_schema,
)
router.include_router(api_keys.router, dependencies=[Depends(get_current_active_user)], tags=["API Keys"])
@@ -19,7 +19,7 @@ API routes for appliances.
"""
import logging
from typing import List, Optional, Union
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, status
@@ -85,9 +85,7 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance:
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Appliance.Allocate"))],
)
def add_appliance_version(
appliance_id: UUID, appliance_version: Union[schemas.ApplianceVersion, schemas.ApplianceVersionV8]
) -> dict:
def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersionCreate) -> dict:
"""
Add a version to an appliance.
@@ -109,7 +107,7 @@ def add_appliance_version(
if version.get("name") == appliance_version.name:
raise ControllerError(message=f"Appliance '{appliance_id}' already has version '{appliance_version.name}'")
appliance.versions.append(appliance_version.model_dump(exclude_unset=True))
appliance.versions.append(appliance_version.model_dump(exclude_unset=True, exclude={"registry_version"}))
return appliance.asdict()
@@ -32,6 +32,7 @@ from starlette.requests import ClientDisconnect
import gns3server.db.models as models
from gns3server import schemas
from gns3server.api.openapi import binary_request_body
from gns3server.compute.qemu import Qemu
from gns3server.config import Config
from gns3server.controller import Controller
@@ -214,6 +215,7 @@ async def get_images(
response_model=schemas.ImageUpload,
response_model_exclude_unset=True,
status_code=status.HTTP_201_CREATED,
openapi_extra=binary_request_body(),
dependencies=[Depends(has_privilege("Image.Allocate"))],
)
async def upload_image(
+11 -3
View File
@@ -20,7 +20,7 @@ API routes for links.
import logging
import os
from typing import Any, List, Optional, Union
from typing import Any, List, Optional
from uuid import UUID, uuid4
import aiohttp
@@ -31,6 +31,7 @@ from fastapi.responses import FileResponse, StreamingResponse
from gns3server import schemas
from gns3server.agent.web_wireshark.manager import WebWiresharkManager
from gns3server.api.openapi import PCAP_MEDIA_TYPE, binary_response
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
from gns3server.controller.link import _UNSET, Link
@@ -330,7 +331,12 @@ async def restart_wireshark(http_request: Request, link: Link = Depends(dep_link
return {"status": "restarted"}
@router.get("/{link_id}/capture/stream", dependencies=[Depends(has_privilege("Link.Capture"))])
@router.get(
"/{link_id}/capture/stream",
response_class=StreamingResponse,
responses={200: binary_response(PCAP_MEDIA_TYPE, "Packet capture stream")},
dependencies=[Depends(has_privilege("Link.Capture"))],
)
async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> StreamingResponse:
"""
Stream the PCAP capture file from compute.
@@ -539,7 +545,7 @@ async def update_marker(marker_name: str, marker_data: schemas.MarkerUpdate, lin
@router.get(
"/{link_id}/iface",
response_model=Union[schemas.UDPPortInfo, schemas.EthernetPortInfo],
response_model=schemas.LinkIfaceInfo,
dependencies=[Depends(has_privilege("Link.Audit"))],
)
async def get_iface(link: Link = Depends(dep_link)) -> dict:
@@ -570,6 +576,7 @@ async def get_iface(link: Link = Depends(dep_link)) -> dict:
port_type = port.get("type", "")
if "udp" in port_type.lower():
ifaces_info = {
"kind": "udp",
"node_id": node.id,
"type": f"{port_type}",
"lport": port["lport"],
@@ -578,6 +585,7 @@ async def get_iface(link: Link = Depends(dep_link)) -> dict:
}
else:
ifaces_info = {
"kind": "ethernet",
"node_id": node.id,
"type": f"{port_type}",
"interface": port["interface"],
+14 -1
View File
@@ -32,6 +32,7 @@ from fastapi.responses import StreamingResponse
from fastapi.routing import APIRoute
from gns3server import schemas
from gns3server.api.openapi import binary_request_body, binary_response
from gns3server.config import Config
from gns3server.controller import Controller
from gns3server.controller.controller_error import (
@@ -632,7 +633,12 @@ async def list_node_files(
return res.json
@router.get("/{node_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Node.Audit"))])
@router.get(
"/{node_id}/files/{file_path:path}",
response_class=StreamingResponse,
responses={200: binary_response(description="File content")},
dependencies=[Depends(has_privilege("Node.Audit"))],
)
async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
"""
Return a file from the node directory.
@@ -673,6 +679,13 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
@router.post(
"/{node_id}/files/{file_path:path}",
status_code=status.HTTP_201_CREATED,
responses={
201: {
"description": "File written",
"content": {"application/json": {"schema": {"type": "null"}}},
}
},
openapi_extra=binary_request_body(),
dependencies=[Depends(has_privilege("Node.Modify"))],
)
async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)):
+26 -3
View File
@@ -51,6 +51,7 @@ from fastapi.responses import FileResponse, StreamingResponse
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server import schemas
from gns3server.api.openapi import ZIP_MEDIA_TYPE, binary_request_body, binary_response
from gns3server.api.responses import NDJSON_MEDIA_TYPE, NOTIFICATION_STREAM_RESPONSES, NDJSONResponse
from gns3server.controller import Controller, marker_replay
from gns3server.controller.controller_error import ControllerBadRequestError, ControllerError
@@ -688,7 +689,12 @@ async def project_marker_ws_notifications(
log.warning(f"Error while sending marker event to WebSocket client: {e}")
@router.get("/{project_id}/export", dependencies=[Depends(has_privilege("Project.Audit"))])
@router.get(
"/{project_id}/export",
response_class=StreamingResponse,
responses={200: binary_response(ZIP_MEDIA_TYPE, "Portable project archive")},
dependencies=[Depends(has_privilege("Project.Audit"))],
)
async def export_project(
project: Project = Depends(dep_project),
include_snapshots: bool = False,
@@ -774,6 +780,7 @@ async def export_project(
"/{project_id}/import",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
openapi_extra=binary_request_body(ZIP_MEDIA_TYPE),
dependencies=[Depends(has_privilege("Project.Allocate"))],
)
async def import_project(project_id: UUID, request: Request, name: Optional[str] = None) -> schemas.Project:
@@ -898,7 +905,12 @@ async def unlock_project(project: Project = Depends(dep_project)) -> None:
project.unlock()
@router.get("/{project_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Project.Audit"))])
@router.get(
"/{project_id}/files/{file_path:path}",
response_class=FileResponse,
responses={200: binary_response(description="File content")},
dependencies=[Depends(has_privilege("Project.Audit"))],
)
async def get_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse:
"""
Return a file from a project.
@@ -920,7 +932,17 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)) -> F
return FileResponse(path, media_type="application/octet-stream")
@router.get("/{project_id}/gns3file", dependencies=[Depends(has_privilege("Project.Audit"))])
@router.get(
"/{project_id}/gns3file",
response_class=FileResponse,
responses={
200: {
"description": "Project topology file",
"content": {"application/json": {"schema": {"type": "object"}}},
}
},
dependencies=[Depends(has_privilege("Project.Audit"))],
)
async def get_project_gns3_file(project: Project = Depends(dep_project)) -> FileResponse:
"""
Return the .gns3 topology file of a project.
@@ -938,6 +960,7 @@ async def get_project_gns3_file(project: Project = Depends(dep_project)) -> File
@router.post(
"/{project_id}/files/{file_path:path}",
status_code=status.HTTP_204_NO_CONTENT,
openapi_extra=binary_request_body(),
dependencies=[Depends(has_privilege("Project.Modify"))],
)
async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None:
+7 -1
View File
@@ -27,6 +27,7 @@ from fastapi import APIRouter, Depends, Request, Response, status
from fastapi.responses import FileResponse
from gns3server import schemas
from gns3server.api.openapi import binary_request_body, binary_response
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError
from gns3server.utils.get_resource import get_resource
@@ -53,7 +54,11 @@ def get_symbols() -> List[dict]:
@router.get(
"/{symbol_id:path}/raw",
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}},
response_class=FileResponse,
responses={
200: binary_response(description="Symbol file"),
404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"},
},
dependencies=[Depends(has_privilege("Symbol.Audit"))],
)
async def get_symbol(symbol_id: str, request: Request) -> Response:
@@ -112,6 +117,7 @@ def get_default_symbols() -> dict:
@router.post(
"/{symbol_id:path}/raw",
status_code=status.HTTP_204_NO_CONTENT,
openapi_extra=binary_request_body(),
dependencies=[Depends(has_privilege("Symbol.Allocate"))],
)
async def upload_symbol(symbol_id: str, request: Request) -> None: