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

@@ -23,16 +23,11 @@ This module handles Docker API operations via aiohttp Unix socket.
import asyncio
import logging
import sys
from asyncio import timeout as async_timeout
from typing import Any
import aiohttp
if sys.version_info >= (3, 11):
from asyncio import timeout as async_timeout
else:
from async_timeout import timeout as async_timeout
from gns3server.utils import parse_version
logger = logging.getLogger(__name__)
+47
View File
@@ -0,0 +1,47 @@
#
# Copyright (C) 2026 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 typing import Any, Dict
BINARY_SCHEMA: Dict[str, Any] = {"type": "string", "format": "binary"}
NOTIFICATION_LINE_SCHEMA: Dict[str, Any] = {
"type": "object",
"description": "One notification event per line",
"properties": {
"action": {"type": "string"},
"event": {"description": "Event payload, its shape depends on the action"},
},
"required": ["action", "event"],
"additionalProperties": True,
}
PCAP_MEDIA_TYPE = "application/vnd.tcpdump.pcap"
NDJSON_MEDIA_TYPE = "application/x-ndjson"
OCTET_STREAM_MEDIA_TYPE = "application/octet-stream"
ZIP_MEDIA_TYPE = "application/zip"
def binary_response(media_type: str = OCTET_STREAM_MEDIA_TYPE, description: str = "Binary content") -> Dict[str, Any]:
return {"description": description, "content": {media_type: {"schema": BINARY_SCHEMA}}}
def ndjson_response(description: str = "Stream of newline-delimited JSON notifications") -> Dict[str, Any]:
return {"description": description, "content": {NDJSON_MEDIA_TYPE: {"schema": NOTIFICATION_LINE_SCHEMA}}}
def binary_request_body(media_type: str = OCTET_STREAM_MEDIA_TYPE) -> Dict[str, Any]:
return {"requestBody": {"required": True, "content": {media_type: {"schema": BINARY_SCHEMA}}}}
+3 -1
View File
@@ -32,6 +32,7 @@ from gns3server.compute.compute_error import (
from gns3server.compute.error import ImageMissingError, NodeError
from gns3server.compute.ubridge.ubridge_error import UbridgeError
from gns3server.controller.gns3vm.gns3_vm_error import GNS3VMError
from gns3server.version import __api_version__
from . import (
atm_switch_nodes,
@@ -61,8 +62,9 @@ log = logging.getLogger(__name__)
compute_api = FastAPI(
title="GNS3 compute API",
description="This page describes the private compute API for GNS3. PLEASE DO NOT USE DIRECTLY!",
version="3.0.0",
generate_unique_id_function=generate_operation_id,
version=__api_version__,
servers=[{"url": "/v3/compute"}],
)
compute_api.state.controller_host = None
+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:
+12 -10
View File
@@ -40,6 +40,7 @@ from gns3server.api.operation_ids import add_stub_routes, generate_operation_id
from gns3server.api.errors import customize_openapi_errors, error_response
from gns3server.api.routes import controller, index
from gns3server.api.routes.compute import compute_api
from gns3server.config import Config
from gns3server.controller.controller_error import (
ComputeConflictError,
ControllerBadRequestError,
@@ -50,6 +51,7 @@ from gns3server.controller.controller_error import (
ControllerUnauthorizedError,
)
from gns3server.core import tasks
from gns3server.version import __api_version__
if MCP_AVAILABLE:
from gns3server.agent import mcp
@@ -60,6 +62,7 @@ else:
_mcp_router = APIRouter(prefix="/mcp", tags=["MCP"])
@_mcp_router.api_route("/{path:path}", methods=["GET", "POST", "DELETE", "PATCH", "PUT"], include_in_schema=False)
async def mcp_not_available(path: str = ""):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
@@ -86,15 +89,9 @@ def get_application() -> FastAPI:
application = ControllerAPI(
lifespan=tasks.lifespan,
title="GNS3 controller API",
description="This page describes the public controller API for GNS3.\n\n"
"## Notification streams\n\n"
"Notifications are available as HTTP streams of newline delimited JSON objects (`GET /v3/notifications` and "
"`GET /v3/projects/{project_id}/notifications`) and as WebSockets sending one JSON object per text frame "
"(`/v3/notifications/ws`, `/v3/projects/{project_id}/notifications/ws` and "
"`/v3/projects/{project_id}/notifications/markers/ws`). "
"OpenAPI cannot describe WebSocket routes, they carry the same `Notification` messages as the HTTP streams. "
"Marker events (`marker.match`) are only sent on the markers WebSocket.",
version="3.0.0",
description="This page describes the public controller API for GNS3",
version=__api_version__,
servers=[{"url": "/"}],
docs_url=None,
redoc_url=None,
generate_unique_id_function=generate_operation_id,
@@ -114,7 +111,12 @@ def get_application() -> FastAPI:
application.mount("/v3/compute", compute_api, name="compute")
# Register MCP routes (stub returns 501 if MCP dependencies are not installed)
application.include_router(_mcp_router, prefix="/v3", tags=["MCP"])
application.include_router(
_mcp_router,
prefix="/v3",
tags=["MCP"],
include_in_schema=Config.instance().settings.Server.openapi_include_ai,
)
return application
+2 -10
View File
@@ -671,11 +671,7 @@ class BaseNode:
)
# keep forwarding websocket data in both direction
if sys.version_info >= (3, 11, 0):
# Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden.
aws = [asyncio.create_task(ws_forward(telnet_writer)), asyncio.create_task(telnet_forward(telnet_reader))]
else:
aws = [ws_forward(telnet_writer), telnet_forward(telnet_reader)]
aws = [asyncio.create_task(ws_forward(telnet_writer)), asyncio.create_task(telnet_forward(telnet_reader))]
done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)
for task in done:
@@ -762,11 +758,7 @@ class BaseNode:
log.warning(f"Exception while forwarding VNC data to WebSocket: {e!r}")
# Keep forwarding WebSocket data in both directions
if sys.version_info >= (3, 11, 0):
# Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden.
aws = [asyncio.create_task(ws_forward(vnc_writer)), asyncio.create_task(vnc_forward(vnc_reader))]
else:
aws = [ws_forward(vnc_writer), vnc_forward(vnc_reader)]
aws = [asyncio.create_task(ws_forward(vnc_writer)), asyncio.create_task(vnc_forward(vnc_reader))]
done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)
for task in done:
+2 -7
View File
@@ -19,19 +19,14 @@ import asyncio
import io
import ipaddress
import json
import logging
import socket
import sys
from asyncio import timeout as asynctimeout
import aiohttp
from fastapi import HTTPException
if sys.version_info >= (3, 11):
from asyncio import timeout as asynctimeout
else:
from async_timeout import timeout as asynctimeout
import logging
from ..controller.controller_error import (
ComputeConflictError,
ComputeError,
+4 -4
View File
@@ -16,11 +16,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from typing import List, Optional, Union, cast
from typing import Any, List, Optional, Union, cast
from urllib.parse import urlparse
from uuid import UUID
from sqlalchemy import delete, select, update
from sqlalchemy import Select, delete, select, update
from sqlalchemy.engine import CursorResult
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -331,7 +331,7 @@ class RbacRepository(BaseRepository):
Retrieve all user ACEs matching the user_id and privilege name.
"""
query = (
query: Select[Any, Any, Any, Any] = (
select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name)
.join(models.Privilege.roles)
.join(models.Role.acl_entries)
@@ -349,7 +349,7 @@ class RbacRepository(BaseRepository):
Retrieve all group ACEs matching the user_id and privilege name.
"""
query = (
query: Select[Any, Any, Any, Any] = (
select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name)
.join(models.Privilege.roles)
.join(models.Role.acl_entries)
+4 -1
View File
@@ -17,7 +17,7 @@
# General schemas
from .common import ErrorMessage
from .config import ServerConfig
from .controller.appliances import Appliance, ApplianceVersion, ApplianceVersionV8
from .controller.appliances import Appliance, ApplianceVersion, ApplianceVersionCreate, ApplianceVersionV8
from .controller.computes import (
AutoIdlePC,
Compute,
@@ -41,6 +41,7 @@ from .controller.links import (
LinkFilterParameter,
LinkFilters,
LinkFilterType,
LinkIfaceInfo,
LinkUpdate,
MarkerCreate,
MarkerDefinitionCreate,
@@ -167,6 +168,7 @@ __all__ = [
"ApiKeyCreate",
"Appliance",
"ApplianceVersion",
"ApplianceVersionCreate",
"ApplianceVersionV8",
"AutoIdlePC",
"BatchNIOCreate",
@@ -253,6 +255,7 @@ __all__ = [
"LinkFilterParameter",
"LinkFilterType",
"LinkFilters",
"LinkIfaceInfo",
"LinkUpdate",
"LoggedInUserUpdate",
"MarkerCreate",
+3
View File
@@ -155,6 +155,9 @@ class ServerSettings(BaseModel):
False, description="Local server mode, set by the --local command line argument (not meant to be set by hand)"
)
enable_http_auth: bool = Field(True, description="Enable compute HTTP authentication")
openapi_include_ai: bool = Field(
True, description="Include the AI Copilot, LLM and MCP routes in the generated OpenAPI specification"
)
name: str = Field(
f"{socket.gethostname()} (controller)",
description="Server name, default is what is returned by socket.gethostname()",
+34 -1
View File
@@ -20,7 +20,7 @@ from enum import Enum
from typing import Annotated, List, Literal, Optional, Union
from uuid import UUID
from pydantic import AnyUrl, BaseModel, Discriminator, EmailStr, Field, Tag
from pydantic import AnyUrl, BaseModel, BeforeValidator, Discriminator, EmailStr, Field, Tag
from ..common import ExtraConfig
@@ -648,6 +648,39 @@ class ApplianceVersionV8(BaseModel):
images: Optional[ApplianceVersionImages] = Field(None, title="Images used for this version")
_V8_VERSION_ONLY_FIELDS = frozenset(set(ApplianceVersionV8.model_fields) - set(ApplianceVersion.model_fields))
class ApplianceVersionCreateV1_6(ApplianceVersion):
"""Request body to add a version to an appliance with registry version 1-6"""
registry_version: Literal[1, 2, 3, 4, 5, 6] = Field(
1, title="Version of the registry compatible with the appliance receiving the version"
)
class ApplianceVersionCreateV8(ApplianceVersionV8):
"""Request body to add a version to an appliance with registry version 8"""
registry_version: Literal[8] = Field(
8, title="Version of the registry compatible with the appliance receiving the version"
)
def _infer_version_registry_version(data):
if isinstance(data, dict) and "registry_version" not in data:
data = dict(data)
data["registry_version"] = 8 if _V8_VERSION_ONLY_FIELDS.intersection(data) else 1
return data
ApplianceVersionCreate = Annotated[
Union[ApplianceVersionCreateV1_6, ApplianceVersionCreateV8],
BeforeValidator(_infer_version_registry_version),
Discriminator("registry_version"),
]
# ============================================================================
# Child Models with Discriminated Union
# ============================================================================
+7 -2
View File
@@ -15,10 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum
from typing import Annotated, List, Literal, Optional, Tuple
from typing import Annotated, List, Literal, Optional, Tuple, Union
from uuid import UUID, uuid4
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic import BaseModel, ConfigDict, Discriminator, Field, field_validator, model_validator
from ..update import PartialUpdateModel
from .labels import Label
@@ -205,6 +205,7 @@ class UDPPortInfo(BaseModel):
rhost: str
rport: int
type: str
kind: Literal["udp"]
class EthernetPortInfo(BaseModel):
@@ -215,6 +216,10 @@ class EthernetPortInfo(BaseModel):
node_id: UUID
interface: str
type: str
kind: Literal["ethernet"]
LinkIfaceInfo = Annotated[Union[UDPPortInfo, EthernetPortInfo], Discriminator("kind")]
class LinkCapture(BaseModel):
@@ -103,6 +103,7 @@ class ServerSettingsUpdate(BaseModel):
local: Optional[bool] = None
enable_http_auth: Optional[bool] = None
openapi_include_ai: Optional[bool] = None
name: Optional[str] = None
protocol: Optional[ServerProtocol] = None
host: Optional[str] = None
+3 -3
View File
@@ -255,9 +255,9 @@ class Server:
else:
log.info(f"Compute authentication is enabled with username '{config.Server.compute_username}'")
# we only support Python 3 version >= 3.9
if sys.version_info < (3, 9, 0):
raise SystemExit("Python 3.9 or higher is required")
# we only support Python 3 version >= 3.11
if sys.version_info < (3, 11, 0):
raise SystemExit("Python 3.11 or higher is required")
log.info(
f"Running with Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} and has PID {os.getpid()}"
+2
View File
@@ -35,3 +35,5 @@ if "dev" in __version__:
__version__ += "+" + r
except Exception as e:
print(e)
__api_version__ = __version__.split("+", 1)[0]