Merge branch '3.1' into image-manager-out-of-sync

This commit is contained in:
Jeremy Grossmann authored and GitHub committed 2026-10-01 14:44:24 +02:00
commit a2cdc14a25
129 files changed
+944 -863

No files matched your search

@@ -18,6 +18,7 @@
API routes for ATM switch nodes.
"""
from typing import Any
import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
@@ -29,7 +30,9 @@ from gns3server import schemas
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.atm_switch import ATMSwitch
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}
}
router = APIRouter(responses=responses)
@@ -57,13 +60,13 @@ async def create_atm_switch(project_id: UUID, node_data: schemas.ATMSwitchCreate
# Use the Dynamips ATM switch to simulate this node
dynamips_manager = Dynamips.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
node = await dynamips_manager.create_node(
node_data.get("name"),
data.get("name"),
str(project_id),
node_data.get("node_id"),
data.get("node_id"),
node_type="atm_switch",
mappings=node_data.get("mappings"),
mappings=data.get("mappings"),
)
return node.asdict()
@@ -97,11 +100,11 @@ async def update_atm_switch(
Update an ATM switch node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]:
await node.set_name(node_data["name"])
if "mappings" in node_data:
node.mappings = node_data["mappings"]
data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in data and node.name != data["name"]:
await node.set_name(data["name"])
if "mappings" in data:
node.mappings = data["mappings"]
node.updated()
return node.asdict()
@@ -39,7 +39,8 @@ def get_capabilities(request: Request) -> dict:
node_types.extend(module.node_types())
# record the controller hostname or IP address
request.app.state.controller_host = request.client.host
if request.client:
request.app.state.controller_host = request.client.host
return {
"version": __version__,
+16 -14
View File
@@ -23,14 +23,16 @@ import os
from fastapi import APIRouter, Depends, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from typing import Any, Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.cloud import Cloud
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}
}
router = APIRouter(responses=responses)
@@ -57,21 +59,21 @@ async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate) -> sche
"""
builtin_manager = Builtin.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
node = await builtin_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
data.get("node_id"),
node_type="cloud",
ports=node_data.get("ports_mapping"),
ports=data.get("ports_mapping"),
)
# add the remote console settings
node.remote_console_host = node_data.get("remote_console_host", node.remote_console_host)
node.remote_console_port = node_data.get("remote_console_port", node.remote_console_port)
node.remote_console_type = node_data.get("remote_console_type", node.remote_console_type)
node.remote_console_http_path = node_data.get("remote_console_http_path", node.remote_console_http_path)
node.usage = node_data.get("usage", "")
node.remote_console_host = data.get("remote_console_host", node.remote_console_host)
node.remote_console_port = data.get("remote_console_port", node.remote_console_port)
node.remote_console_type = data.get("remote_console_type", node.remote_console_type)
node.remote_console_http_path = data.get("remote_console_http_path", node.remote_console_http_path)
node.usage = data.get("usage", "")
return node.asdict()
@@ -90,8 +92,8 @@ async def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep
Update a cloud node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in node_data.items():
data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value)
node.updated()
@@ -178,7 +180,7 @@ async def update_cloud_nio(
nio = node.get_nio(port_number)
nio.filters.clear()
if nio_data.filters:
if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
nio.filters = nio_data.filters
# NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers.
nio.markers = getattr(nio_data, "markers", None) or {}
+1 -1
View File
@@ -163,7 +163,7 @@ def compute_statistics() -> dict:
@router.get("/qemu/capabilities")
async def get_qemu_capabilities() -> dict:
capabilities = {"kvm": []}
capabilities: dict[str, list] = {"kvm": []}
kvms = await Qemu.get_kvm_archs()
if kvms:
capabilities["kvm"] = kvms
@@ -91,9 +91,9 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock
if not separator:
raise invalid_user_credentials_exc
username = secrets.compare_digest(username, server_settings.compute_username)
password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
if not (username and password):
username_valid = secrets.compare_digest(username, server_settings.compute_username)
password_valid = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
if not (username_valid and password_valid):
raise invalid_user_credentials_exc
except HTTPException as e:
@@ -101,5 +101,6 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error)
log.error(err_msg)
return await websocket.close(code=1008)
await websocket.close(code=1008)
return None
return websocket
@@ -24,14 +24,16 @@ from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from typing import Any, Union
from gns3server import schemas
from gns3server.compute.docker import Docker
from gns3server.compute.docker.docker_vm import DockerVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}
}
router = APIRouter(responses=responses)
@@ -53,13 +55,13 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM:
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
dependencies=[Depends(compute_authentication)],
)
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker:
async def create_docker_node(project_id: UUID, node_create: schemas.DockerCreate) -> schemas.Docker:
"""
Create a new Docker node.
"""
docker_manager = Docker.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
node_data = jsonable_encoder(node_create, exclude_unset=True)
container = await docker_manager.create_node(
node_data.pop("name"),
str(project_id),
@@ -124,7 +126,7 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
@router.put("/{node_id}", response_model=schemas.Docker, dependencies=[Depends(compute_authentication)])
async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
async def update_docker_node(node_update: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
"""
Update a Docker node.
"""
@@ -152,7 +154,7 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
]
changed = False
node_data = jsonable_encoder(node_data, exclude_unset=True)
node_data = jsonable_encoder(node_update, exclude_unset=True)
for prop in props:
# hasattr: startup_config_content only exists on IOLDockerVM
if prop in node_data and hasattr(node, prop) and node_data[prop] != getattr(node, prop):
+14 -11
View File
@@ -23,7 +23,7 @@ import os
from fastapi import APIRouter, WebSocket, Body, Depends, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import List, Union
from typing import Any, List, Optional, Union
from uuid import UUID
from gns3server.compute.dynamips import Dynamips
@@ -32,7 +32,9 @@ from gns3server import schemas
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}
}
router = APIRouter(responses=responses)
@@ -64,25 +66,26 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
dynamips_manager = Dynamips.instance()
platform = node_data.platform
chassis: Optional[str]
if not node_data.chassis and platform in DEFAULT_CHASSIS:
chassis = DEFAULT_CHASSIS[platform]
else:
chassis = node_data.chassis
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
vm = await dynamips_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
dynamips_id=node_data.get("dynamips_id"),
data.get("node_id"),
dynamips_id=data.get("dynamips_id"),
platform=platform,
console=node_data.get("console"),
console_type=node_data.get("console_type", "telnet"),
aux=node_data.get("aux"),
aux_type=node_data.pop("aux_type", "none"),
console=data.get("console"),
console_type=data.get("console_type", "telnet"),
aux=data.get("aux"),
aux_type=data.pop("aux_type", "none"),
chassis=chassis,
node_type="dynamips",
)
await dynamips_manager.update_vm_settings(vm, node_data)
await dynamips_manager.update_vm_settings(vm, data)
return vm.asdict()
@@ -18,6 +18,7 @@
API routes for Ethernet hub nodes.
"""
from typing import Any
import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
@@ -29,7 +30,9 @@ from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.ethernet_hub import EthernetHub
from gns3server import schemas
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}
}
router = APIRouter(responses=responses)
@@ -57,13 +60,13 @@ async def create_ethernet_hub(project_id: UUID, node_data: schemas.EthernetHubCr
# Use the Dynamips Ethernet hub to simulate this node
dynamips_manager = Dynamips.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
node = await dynamips_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
data.get("node_id"),
node_type="ethernet_hub",
ports=node_data.get("ports_mapping"),
ports=data.get("ports_mapping"),
)
return node.asdict()
@@ -97,11 +100,11 @@ async def update_ethernet_hub(
Update an Ethernet hub.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]:
await node.set_name(node_data["name"])
if "ports_mapping" in node_data:
node.ports_mapping = node_data["ports_mapping"]
data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in data and node.name != data["name"]:
await node.set_name(data["name"])
if "ports_mapping" in data:
node.ports_mapping = data["ports_mapping"]
node.updated()
return node.asdict()
@@ -22,6 +22,7 @@ through uBridge's ``brctl`` module (see
``gns3server.compute.builtin.nodes.ethernet_switch``).
"""
from typing import Any
import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
@@ -33,7 +34,9 @@ from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.ethernet_switch import EthernetSwitch
from gns3server import schemas
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}
}
router = APIRouter(responses=responses)
@@ -60,17 +63,17 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
"""
builtin_manager = Builtin.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
node = await builtin_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
console=node_data.get("console"),
console_type=node_data.get("console_type"),
data.get("node_id"),
console=data.get("console"),
console_type=data.get("console_type"),
node_type="ethernet_switch",
ports=node_data.get("ports_mapping"),
ports=data.get("ports_mapping"),
)
node.usage = node_data.get("usage", "")
node.usage = data.get("usage", "")
return node.asdict()
@@ -100,16 +103,16 @@ async def update_ethernet_switch(
Update an Ethernet switch.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]:
node.name = node_data["name"]
if "usage" in node_data:
node.usage = node_data["usage"]
if "ports_mapping" in node_data:
node.ports_mapping = node_data["ports_mapping"]
data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in data and node.name != data["name"]:
node.name = data["name"]
if "usage" in data:
node.usage = data["usage"]
if "ports_mapping" in data:
node.ports_mapping = data["ports_mapping"]
await node.update_port_settings()
if "console_type" in node_data:
node.console_type = node_data["console_type"]
if "console_type" in data:
node.console_type = data["console_type"]
node.updated()
return node.asdict()
@@ -18,6 +18,7 @@
API routes for Frame Relay switch nodes.
"""
from typing import Any
import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException
@@ -29,7 +30,9 @@ from gns3server import schemas
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.frame_relay_switch import FrameRelaySwitch
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}
}
router = APIRouter(responses=responses)
@@ -59,13 +62,13 @@ async def create_frame_relay_switch(
# Use the Dynamips Frame Relay switch to simulate this node
dynamips_manager = Dynamips.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
node = await dynamips_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
data.get("node_id"),
node_type="frame_relay_switch",
mappings=node_data.get("mappings"),
mappings=data.get("mappings"),
)
return node.asdict()
@@ -99,11 +102,11 @@ async def update_frame_relay_switch(
Update an Frame Relay switch node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]:
await node.set_name(node_data["name"])
if "mappings" in node_data:
node.mappings = node_data["mappings"]
data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in data and node.name != data["name"]:
await node.set_name(data["name"])
if "mappings" in data:
node.mappings = data["mappings"]
node.updated()
return node.asdict()
+18 -16
View File
@@ -23,7 +23,7 @@ import os
from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from typing import Any, Union
from uuid import UUID
from gns3server import schemas
@@ -32,7 +32,9 @@ from gns3server.compute.iou.iou_vm import IOUVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}
}
router = APIRouter(responses=responses)
@@ -60,18 +62,18 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
"""
iou = IOU.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
vm = await iou.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
application_id=node_data.get("application_id"),
path=node_data.get("path"),
console=node_data.get("console"),
console_type=node_data.get("console_type", "telnet"),
data.get("node_id"),
application_id=data.get("application_id"),
path=data.get("path"),
console=data.get("console"),
console_type=data.get("console_type", "telnet"),
)
for name, value in node_data.items():
for name, value in data.items():
if hasattr(vm, name) and getattr(vm, name) != value:
if name == "application_id":
continue # we must ignore this to avoid overwriting the application_id allocated by the controller
@@ -79,7 +81,7 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
continue
if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0):
continue
if node_data.get("use_default_iou_values") and (name == "ram" or name == "nvram"):
if data.get("use_default_iou_values") and (name == "ram" or name == "nvram"):
continue
setattr(vm, name, value)
return vm.asdict()
@@ -100,8 +102,8 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
Update an IOU node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in node_data.items():
data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value:
if name == "application_id":
continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager
@@ -147,8 +149,8 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep
Start an IOU node.
"""
start_data = jsonable_encoder(start_data, exclude_unset=True)
for name, value in start_data.items():
start_values = jsonable_encoder(start_data, exclude_unset=True)
for name, value in start_values.items():
if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value)
@@ -223,7 +225,7 @@ async def update_iou_node_nio(
nio = node.get_nio(adapter_number, port_number)
nio.filters.clear()
if nio_data.filters:
if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
nio.filters = nio_data.filters
# NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers.
nio.markers = getattr(nio_data, "markers", None) or {}
+12 -10
View File
@@ -23,14 +23,16 @@ import os
from fastapi import APIRouter, Depends, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from typing import Any, Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.nat import Nat
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}
}
router = APIRouter(responses=responses)
@@ -57,16 +59,16 @@ async def create_nat_node(project_id: UUID, node_data: schemas.NATCreate) -> sch
"""
builtin_manager = Builtin.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
node = await builtin_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
data.get("node_id"),
node_type="nat",
ports=node_data.get("ports_mapping"),
ports=data.get("ports_mapping"),
)
node.usage = node_data.get("usage", "")
node.usage = data.get("usage", "")
return node.asdict()
@@ -85,8 +87,8 @@ async def update_nat_node(node_data: schemas.NATUpdate, node: Nat = Depends(dep_
Update a NAT node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in node_data.items():
data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value)
node.updated()
@@ -170,7 +172,7 @@ async def update_nat_node_nio(
nio = node.get_nio(port_number)
nio.filters.clear()
if nio_data.filters:
if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
nio.filters = nio_data.filters
await node.update_nio(port_number, nio)
return nio.asdict()
@@ -39,14 +39,15 @@ async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(w
"""
if websocket:
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
log.info(f"New client {client} has connected to compute WebSocket")
try:
with NotificationManager.instance().queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
log.info(f"Client {client} has disconnected from compute WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
+8 -8
View File
@@ -44,7 +44,7 @@ from gns3server import schemas
router = APIRouter()
# How many clients have subscribed to notifications
_notifications_listening = {}
_notifications_listening: dict[str, int] = {}
def dep_project(project_id: UUID) -> Project:
@@ -74,12 +74,12 @@ def create_compute_project(project_data: schemas.ProjectCreate) -> schemas.Proje
"""
pm = ProjectManager.instance()
project_data = jsonable_encoder(project_data, exclude_unset=True)
data = jsonable_encoder(project_data, exclude_unset=True)
project = pm.create_project(
name=project_data.get("name"),
path=project_data.get("path"),
project_id=project_data.get("project_id"),
variables=project_data.get("variables", None),
name=data.get("name"),
path=data.get("path"),
project_id=data.get("project_id"),
variables=data.get("variables", None),
)
return project.asdict()
@@ -261,7 +261,7 @@ async def create_batch_nios(
# This is what makes builtin L2 nodes (ethernet_switch/hub/cloud/nat)
# start their uBridge concurrently during project open instead of one
# at a time (~0.5s each for fork + socket connect).
per_node = {}
per_node: dict[str, list[schemas.BatchNIOEntry]] = {}
for entry in batch.nios:
per_node.setdefault(entry.node_id, []).append(entry)
@@ -308,7 +308,7 @@ async def update_batch_nios(
# Group entries by node so that different nodes' uBridge processes are
# updated in parallel (each node has its own AF_UNIX socket). Within a
# node entries are serial to respect the per-node uBridge command lock.
per_node = {}
per_node: dict[str, list[schemas.BatchNIOEntry]] = {}
for entry in batch.nios:
per_node.setdefault(entry.node_id, []).append(entry)
+23 -21
View File
@@ -23,7 +23,7 @@ import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from typing import Any, Union
from uuid import UUID
from gns3server import schemas
@@ -36,7 +36,9 @@ import logging
log = logging.getLogger(__name__)
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}
}
router = APIRouter(responses=responses)
@@ -64,29 +66,29 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
"""
qemu = Qemu.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
disk_images_to_reset = set(node_data.pop("disk_images_to_reset", []))
data = jsonable_encoder(node_data, exclude_unset=True)
disk_images_to_reset = set(data.pop("disk_images_to_reset", []))
vm = await qemu.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.pop("node_id", None),
linked_clone=node_data.get("linked_clone", True),
qemu_path=node_data.pop("qemu_path", None),
console=node_data.pop("console", None),
console_type=node_data.pop("console_type", "telnet"),
aux=node_data.get("aux"),
aux_type=node_data.pop("aux_type", "none"),
platform=node_data.pop("platform", None),
data.pop("node_id", None),
linked_clone=data.get("linked_clone", True),
qemu_path=data.pop("qemu_path", None),
console=data.pop("console", None),
console_type=data.pop("console_type", "telnet"),
aux=data.get("aux"),
aux_type=data.pop("aux_type", "none"),
platform=data.pop("platform", None),
)
# update the disk image with the backing file if provided
# this is needed when duplicating a node that uses backed disk images
drives = ["a", "b", "c", "d"]
for drive in drives:
disk_image_backing_file = node_data.get(f"hd{drive}_disk_image_backing_file")
disk_image_backing_file = data.get(f"hd{drive}_disk_image_backing_file")
if disk_image_backing_file:
log.debug(f"Updating disk image for drive {drive} with backing file {disk_image_backing_file}")
node_data[f"hd{drive}_disk_image"] = disk_image_backing_file
data[f"hd{drive}_disk_image"] = disk_image_backing_file
# Validate every explicitly replaced disk before removing its stale
# overlay. Other unresolved disks may still make this create request fail,
@@ -94,7 +96,7 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
for drive in drives:
disk_image_property = f"hd{drive}_disk_image"
if disk_image_property in disk_images_to_reset:
replacement_image = node_data.get(disk_image_property)
replacement_image = data.get(disk_image_property)
if replacement_image:
vm.manager.get_abs_image_path(replacement_image, vm.working_dir)
local_disk_name = f"hd{drive}_disk.qcow2"
@@ -107,11 +109,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
log.info(
"Removing stale linked-clone disk '%s' before using replacement image '%s'",
local_disk,
node_data.get(disk_image_property),
data.get(disk_image_property),
)
vm.delete_disk_image(local_disk_name)
for name, value in node_data.items():
for name, value in data.items():
if hasattr(vm, name) and getattr(vm, name) != value:
setattr(vm, name, value)
@@ -133,10 +135,10 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
Update a Qemu node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
# update the console first to avoid issue if updating console type
node.console = node_data.pop("console", node.console)
for name, value in node_data.items():
node.console = data.pop("console", node.console)
for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value:
await node.update_property(name, value)
node.updated()
@@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from typing import Any, Union
from gns3server import schemas
from gns3server.compute.virtualbox import VirtualBox
@@ -33,7 +33,9 @@ from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}
}
router = APIRouter(responses=responses, deprecated=True)
@@ -61,24 +63,24 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
"""
vbox_manager = VirtualBox.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
vm = await vbox_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
node_data.pop("vmname"),
linked_clone=node_data.pop("linked_clone", False),
console=node_data.get("console", None),
console_type=node_data.get("console_type", "telnet"),
adapters=node_data.get("adapters", 0),
data.get("node_id"),
data.pop("vmname"),
linked_clone=data.pop("linked_clone", False),
console=data.get("console", None),
console_type=data.get("console_type", "telnet"),
adapters=data.get("adapters", 0),
)
if "ram" in node_data:
ram = node_data.pop("ram")
if "ram" in data:
ram = data.pop("ram")
if ram != vm.ram:
await vm.set_ram(ram)
for name, value in node_data.items():
for name, value in data.items():
if name != "node_id":
if hasattr(vm, name) and getattr(vm, name) != value:
setattr(vm, name, value)
@@ -103,10 +105,10 @@ async def update_virtualbox_node(
Update a VirtualBox node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data:
name = node_data.pop("name")
vmname = node_data.pop("vmname", None)
data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in data:
name = data.pop("name")
vmname = data.pop("vmname", None)
if name != node.name:
oldname = node.name
node.name = name
@@ -118,20 +120,20 @@ async def update_virtualbox_node(
node.updated()
raise e
if "adapters" in node_data:
adapters = node_data.pop("adapters")
if "adapters" in data:
adapters = data.pop("adapters")
if adapters != node.adapters:
await node.set_adapters(adapters)
if "ram" in node_data:
ram = node_data.pop("ram")
if "ram" in data:
ram = data.pop("ram")
if ram != node.ram:
await node.set_ram(ram)
# update the console first to avoid issue if updating console type
node.console = node_data.pop("console", node.console)
node.console = data.pop("console", node.console)
for name, value in node_data.items():
for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value)
+15 -13
View File
@@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from typing import Any, Union
from gns3server import schemas
from gns3server.compute.vmware import VMware
@@ -32,7 +32,9 @@ from gns3server.compute.vmware.vmware_vm import VMwareVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
}
router = APIRouter(responses=responses, deprecated=True)
@@ -60,18 +62,18 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate)
"""
vmware_manager = VMware.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
vm = await vmware_manager.create_node(
node_data.pop("name"),
data.pop("name"),
str(project_id),
node_data.get("node_id"),
node_data.pop("vmx_path"),
linked_clone=node_data.pop("linked_clone"),
console=node_data.get("console", None),
console_type=node_data.get("console_type", "telnet"),
data.get("node_id"),
data.pop("vmx_path"),
linked_clone=data.pop("linked_clone"),
console=data.get("console", None),
console_type=data.get("console_type", "telnet"),
)
for name, value in node_data.items():
for name, value in data.items():
if name != "node_id":
if hasattr(vm, name) and getattr(vm, name) != value:
setattr(vm, name, value)
@@ -94,10 +96,10 @@ async def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = D
Update a VMware node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
# update the console first to avoid issue if updating console type
node.console = node_data.pop("console", node.console)
for name, value in node_data.items():
node.console = data.pop("console", node.console)
for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value)
+14 -12
View File
@@ -23,7 +23,7 @@ import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from typing import Any, Union
from uuid import UUID
from gns3server import schemas
@@ -31,7 +31,9 @@ from gns3server.compute.vpcs import VPCS
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}
}
router = APIRouter(responses=responses)
@@ -59,14 +61,14 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s
"""
vpcs = VPCS.instance()
node_data = jsonable_encoder(node_data, exclude_unset=True)
data = jsonable_encoder(node_data, exclude_unset=True)
vm = await vpcs.create_node(
node_data["name"],
data["name"],
str(project_id),
node_data.get("node_id"),
console=node_data.get("console"),
console_type=node_data.get("console_type", "telnet"),
startup_script=node_data.get("startup_script"),
data.get("node_id"),
console=data.get("console"),
console_type=data.get("console_type", "telnet"),
startup_script=data.get("startup_script"),
)
return vm.asdict()
@@ -87,14 +89,14 @@ async def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends
Update a VPCS node.
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
name = node_data.get("name", node.name)
data = jsonable_encoder(node_data, exclude_unset=True)
name = data.get("name", node.name)
if node.name != name:
node.name = name
console = node_data.get("console", node.console)
console = data.get("console", node.console)
if node.console != console:
node.console = console
console_type = node_data.get("console_type", node.console_type)
console_type = data.get("console_type", node.console_type)
if node.console_type != console_type:
node.console_type = console_type
node.updated()
+7 -6
View File
@@ -25,13 +25,14 @@ from fastapi import APIRouter, Depends, Request, status
from fastapi.routing import APIRoute, _IncludedRouter
from starlette.routing import BaseRoute, Mount
from uuid import UUID
from typing import Iterator, List, Sequence
from typing import Iterator, List, Optional, Sequence
from gns3server import schemas
from gns3server.controller.controller_error import ControllerBadRequestError, ControllerNotFoundError
from gns3server.controller import Controller
import gns3server.db.models as models
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository
@@ -96,7 +97,7 @@ async def endpoints(
endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}]
def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None:
if endpoint not in endpoints:
if not any(e["endpoint"] == endpoint for e in endpoints):
endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type})
# projects
@@ -175,7 +176,7 @@ async def endpoints(
@router.get("", response_model=List[schemas.ACE], dependencies=[Depends(has_privilege("ACE.Audit"))])
async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.ACE]:
async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[models.ACE]:
"""
Get all ACL entries.
@@ -193,7 +194,7 @@ async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacReposi
)
async def create_ace(
request: Request, ace_create: schemas.ACECreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.ACE:
) -> models.ACE:
"""
Create a new ACL entry.
@@ -224,7 +225,7 @@ async def create_ace(
async def get_ace(
ace_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.ACE:
) -> models.ACE:
"""
Get an ACL entry.
@@ -240,7 +241,7 @@ async def get_ace(
@router.put("/{ace_id}", response_model=schemas.ACE, dependencies=[Depends(has_privilege("ACE.Modify"))])
async def update_ace(
ace_id: UUID, ace_update: schemas.ACEUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.ACE:
) -> Optional[models.ACE]:
"""
Update an ACL entry.
+1 -1
View File
@@ -39,7 +39,7 @@ API_KEY_PREFIX = "gns3_"
API_KEY_BYTES = 32
def _generate_api_key(api_key_id: UUID = None) -> tuple[str, str, str, UUID]:
def _generate_api_key(api_key_id: UUID | None = None) -> tuple[str, str, str, UUID]:
if api_key_id is None:
api_key_id = uuid4()
random_bytes = secrets.token_hex(API_KEY_BYTES)
+2 -2
View File
@@ -23,7 +23,7 @@ Nested under projects: /v3/projects/{project_id}/chat/...
import json
import logging
import uuid
from typing import List, Optional
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import StreamingResponse
@@ -40,7 +40,7 @@ from .dependencies.authentication import get_current_active_user
log = logging.getLogger(__name__)
responses = {404: {"model": schemas.ErrorMessage, "description": "Resource not found"}}
responses: dict[int | str, dict[str, Any]] = {404: {"model": schemas.ErrorMessage, "description": "Resource not found"}}
router = APIRouter(responses=responses)
+7 -6
View File
@@ -23,6 +23,7 @@ from typing import Any, List, Union, Optional
from uuid import UUID
from gns3server.controller import Controller
import gns3server.db.models as models
from gns3server.db.repositories.computes import ComputesRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services.computes import ComputesService
@@ -31,7 +32,7 @@ from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}}
responses: dict[int | str, dict[str, Any]] = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}}
router = APIRouter(responses=responses)
@@ -51,14 +52,14 @@ async def create_compute(
compute_create: schemas.ComputeCreate,
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
connect: Optional[bool] = False,
) -> schemas.Compute:
) -> models.Compute:
"""
Create a new compute on the controller.
Required privilege: Compute.Allocate
"""
return await ComputesService(computes_repo).create_compute(compute_create, connect)
return await ComputesService(computes_repo).create_compute(compute_create, bool(connect))
@router.post(
@@ -86,7 +87,7 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None:
)
async def get_compute(
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
) -> schemas.Compute:
) -> Union[models.Compute, dict]:
"""
Return a compute from the controller.
@@ -104,7 +105,7 @@ async def get_compute(
)
async def get_computes(
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
) -> List[schemas.Compute]:
) -> List[models.Compute]:
"""
Return all computes known by the controller.
@@ -124,7 +125,7 @@ async def update_compute(
compute_id: Union[str, UUID],
compute_update: schemas.ComputeUpdate,
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
) -> schemas.Compute:
) -> models.Compute:
"""
Update a compute on the controller.
+11 -14
View File
@@ -20,7 +20,7 @@ import os
import time
import psutil
from fastapi import APIRouter, Request, Depends, WebSocket, WebSocketDisconnect, status
from fastapi import APIRouter, FastAPI, Request, Depends, WebSocket, WebSocketDisconnect, status
from fastapi.responses import StreamingResponse
from fastapi.encoders import jsonable_encoder
from fastapi.routing import Mount
@@ -64,7 +64,7 @@ def get_version(request: Request) -> dict:
# compute subapp
controller_host = None
for route in request.app.routes:
if isinstance(route, Mount) and route.name == "compute":
if isinstance(route, Mount) and route.name == "compute" and isinstance(route.app, FastAPI):
controller_host = route.app.state.controller_host
local_server = Config.instance().settings.Server.local
@@ -129,7 +129,7 @@ async def shutdown() -> None:
try:
future.result()
except Exception as e:
log.error(f"Could not close project: {e}", exc_info=1)
log.error(f"Could not close project: {e}", exc_info=True)
continue
# then shutdown the server itself
@@ -191,8 +191,8 @@ async def statistics() -> dict:
# Node statistics - distinguish open vs closed project nodes
open_project_nodes = []
closed_project_nodes = []
node_by_type = {}
node_by_status = {}
node_by_type: Dict[str, int] = {}
node_by_status: Dict[str, int] = {}
for project in projects:
nodes = project.nodes.values()
@@ -264,9 +264,8 @@ async def controller_http_notifications(request: Request) -> StreamingResponse:
from gns3server.api.server import app
log.info(
f"New client {request.client.host}:{request.client.port} has connected to controller HTTP notification stream"
)
client = f"{request.client.host}:{request.client.port}" if request.client else "unknown"
log.info(f"New client {client} has connected to controller HTTP notification stream")
async def event_stream():
try:
@@ -275,10 +274,7 @@ async def controller_http_notifications(request: Request) -> StreamingResponse:
msg = await queue.get_json(5)
yield f"{msg}\n".encode("utf-8")
finally:
log.info(
f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP "
f"notification stream"
)
log.info(f"Client {client} has disconnected from controller HTTP notification stream")
return StreamingResponse(event_stream(), media_type="application/json")
@@ -294,14 +290,15 @@ async def controller_ws_notifications(
if current_user is None:
return
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
log.info(f"New client {client} has connected to controller WebSocket")
try:
with Controller.instance().notification.controller_queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
log.info(f"Client {client} has disconnected from controller WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
@@ -55,7 +55,7 @@ async def get_user_from_token(
user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
api_keys_repo: ApiKeysRepository = Depends(get_repository(ApiKeysRepository)),
token: Optional[str] = Query(None, include_in_schema=False),
) -> schemas.User:
) -> models.User:
if bearer_token:
# bearer token is used first, then any token passed as a URL parameter
@@ -169,7 +169,7 @@ async def get_current_active_user_from_websocket(
websocket: WebSocket,
token: str = Query(...),
user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> Optional[schemas.User]:
) -> Optional[models.User]:
# Extract requested subprotocols from headers for proper WebSocket negotiation
# This is critical for protocols like xpra that require specific subprotocols
@@ -238,4 +238,5 @@ async def get_current_active_user_from_websocket(
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error)
log.error(err_msg)
return await websocket.close(code=1008)
await websocket.close(code=1008)
return None
@@ -14,7 +14,7 @@
# 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 Callable, Type
from typing import AsyncGenerator, Callable, Type
from fastapi import Depends
from starlette.requests import HTTPConnection
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,7 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.db.repositories.base import BaseRepository
async def get_db_session(request: HTTPConnection) -> AsyncSession:
async def get_db_session(request: HTTPConnection) -> AsyncGenerator[AsyncSession, None]:
async with AsyncSession(request.app.state._db_engine, expire_on_commit=False) as session:
try:
@@ -32,7 +32,7 @@ async def get_db_session(request: HTTPConnection) -> AsyncSession:
def get_repository(repo: Type[BaseRepository]) -> Callable:
def get_repo(db_session: AsyncSession = Depends(get_db_session)) -> Type[BaseRepository]:
def get_repo(db_session: AsyncSession = Depends(get_db_session)) -> BaseRepository:
return repo(db_session)
return get_repo
+4 -2
View File
@@ -20,7 +20,7 @@ API routes for drawings.
from fastapi import APIRouter, Depends, status
from fastapi.encoders import jsonable_encoder
from typing import List
from typing import Any, List
from uuid import UUID
from gns3server.controller import Controller
@@ -30,7 +30,9 @@ from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}
}
router = APIRouter(responses=responses)
+7 -6
View File
@@ -21,7 +21,7 @@ API routes for user groups.
from fastapi import APIRouter, Depends, status
from uuid import UUID
from typing import List
from typing import List, Optional
from gns3server import schemas
from gns3server.controller.controller_error import (
@@ -31,6 +31,7 @@ from gns3server.controller.controller_error import (
ControllerForbiddenError,
)
import gns3server.db.models as models
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
@@ -47,7 +48,7 @@ router = APIRouter()
@router.get("", response_model=List[schemas.UserGroup], dependencies=[Depends(has_privilege("Group.Audit"))])
async def get_user_groups(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> List[schemas.UserGroup]:
) -> List[models.UserGroup]:
"""
Get all user groups.
@@ -65,7 +66,7 @@ async def get_user_groups(
)
async def create_user_group(
user_group_create: schemas.UserGroupCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> schemas.UserGroup:
) -> models.UserGroup:
"""
Create a new user group.
@@ -82,7 +83,7 @@ async def create_user_group(
async def get_user_group(
user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.UserGroup:
) -> models.UserGroup:
"""
Get a user group.
@@ -100,7 +101,7 @@ async def update_user_group(
user_group_id: UUID,
user_group_update: schemas.UserGroupUpdate,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.UserGroup:
) -> Optional[models.UserGroup]:
"""
Update a user group.
@@ -148,7 +149,7 @@ async def delete_user_group(
)
async def get_user_group_members(
user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.User]:
) -> List[models.User]:
"""
Get all user group members.
+6 -5
View File
@@ -40,6 +40,7 @@ from gns3server.utils.images import (
default_images_directory,
get_builtin_disks,
)
import gns3server.db.models as models
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
@@ -109,7 +110,7 @@ async def create_qemu_image(
image_path: str,
image_data: schemas.QemuDiskImageCreate,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image:
) -> models.Image:
"""
Create a new blank Qemu image.
@@ -158,7 +159,7 @@ async def get_images(
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
image_type: Optional[schemas.ImageType] = None,
availability: Optional[Literal["unknown", "available", "missing", "unavailable", "invalid"]] = None,
) -> List[schemas.Image]:
) -> List[models.Image]:
"""
Return all images.
@@ -182,7 +183,7 @@ async def upload_image(
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
install_appliances: Optional[bool] = False,
) -> schemas.Image:
) -> models.Image:
"""
Upload an image.
@@ -306,7 +307,7 @@ async def install_images(
async def get_image(
image_path: str,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image:
) -> models.Image:
"""
Return an image.
@@ -349,7 +350,7 @@ async def delete_image(
templates = await images_repo.get_image_templates(image.image_id)
if templates:
template_names = ", ".join([template.name for template in templates])
template_names = ", ".join([str(template.name) for template in templates])
raise ControllerError(f"Image '{image_path}' is used by one or more templates: {template_names}")
project_names = Controller.instance().find_projects_using_image(image.filename)
+10 -8
View File
@@ -26,7 +26,7 @@ import aiohttp
from fastapi import APIRouter, Depends, Request, status, WebSocket
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.encoders import jsonable_encoder
from typing import List, Union
from typing import Any, List, Union
from uuid import UUID, uuid4
from gns3server.controller import Controller
@@ -46,7 +46,9 @@ import logging
log = logging.getLogger(__name__)
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}
}
router = APIRouter(responses=responses)
@@ -91,7 +93,7 @@ async def get_links(project_id: UUID) -> List[schemas.Link]:
},
dependencies=[Depends(has_privilege("Link.Allocate"))],
)
async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link:
async def create_link(project_id: UUID, link_create: schemas.LinkCreate) -> schemas.Link:
"""
Create a new link.
@@ -100,7 +102,7 @@ async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schema
project = await Controller.instance().get_loaded_project(str(project_id))
link = await project.add_link()
link_data = jsonable_encoder(link_data, exclude_unset=True)
link_data = jsonable_encoder(link_create, exclude_unset=True)
if "filters" in link_data:
await link.update_filters(link_data["filters"])
if "link_style" in link_data:
@@ -156,14 +158,14 @@ async def get_link(link: Link = Depends(dep_link)) -> schemas.Link:
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Link.Modify"))],
)
async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link:
async def update_link(link_update: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link:
"""
Update a link.
Required privilege: Link.Modify
"""
link_data = jsonable_encoder(link_data, exclude_unset=True)
link_data = jsonable_encoder(link_update, exclude_unset=True)
if "filters" in link_data:
await link.update_filters(link_data["filters"])
if "link_style" in link_data:
@@ -293,7 +295,7 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea
pcap_streaming_url = link.pcap_streaming_url()
headers = multidict.MultiDict(request.headers)
headers["Host"] = compute.host
headers["Router-Host"] = request.client.host
headers["Router-Host"] = request.client.host if request.client else ""
body = await request.body()
async def compute_pcap_stream():
@@ -487,7 +489,7 @@ async def update_marker(marker_name: str, marker_data: schemas.MarkerUpdate, lin
response_model=Union[schemas.UDPPortInfo, schemas.EthernetPortInfo],
dependencies=[Depends(has_privilege("Link.Audit"))],
)
async def get_iface(link: Link = Depends(dep_link)) -> Union[schemas.UDPPortInfo, schemas.EthernetPortInfo]:
async def get_iface(link: Link = Depends(dep_link)) -> dict:
"""
Return iface info for links to Cloud or NAT devices.
@@ -206,7 +206,11 @@ async def create_user_llm_model_config(
# Extract config fields (excluding table-level fields)
config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"})
new_config = await llm_repo.create_user_config(
user_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default
user_id,
config_create.name,
config_create.model_type,
config_fields,
is_default=bool(config_create.is_default),
)
return schemas.LLMModelConfigResponse(
@@ -346,6 +350,10 @@ async def set_user_default_llm_model_config(
# Get the updated config
config = await llm_repo.get_user_config(config_id)
if config is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found"
)
return schemas.LLMModelConfigResponse(
config_id=config.config_id,
name=config.name,
@@ -506,7 +514,11 @@ async def create_group_llm_model_config(
# Extract config fields (excluding table-level fields)
config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"})
new_config = await llm_repo.create_group_config(
group_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default
group_id,
config_create.name,
config_create.model_type,
config_fields,
is_default=bool(config_create.is_default),
)
return schemas.LLMModelConfigResponse(
@@ -646,6 +658,10 @@ async def set_group_default_llm_model_config(
# Get the updated config
config = await llm_repo.get_group_config(config_id)
if config is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found"
)
return schemas.LLMModelConfigResponse(
config_id=config.config_id,
name=config.name,
+18 -29
View File
@@ -27,7 +27,7 @@ from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request,
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from fastapi.routing import APIRoute
from typing import List, Callable, Optional
from typing import Any, List, Callable, Optional
from uuid import UUID
from gns3server.controller import Controller
@@ -47,7 +47,7 @@ import logging
log = logging.getLogger(__name__)
node_locks = {}
node_locks: dict[str, dict[str, Any]] = {}
class NodeConcurrency(APIRoute):
@@ -84,7 +84,9 @@ class NodeConcurrency(APIRoute):
return custom_route_handler
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}
}
router = APIRouter(route_class=NodeConcurrency, responses=responses)
@@ -127,7 +129,7 @@ def _check_node_type(node: Node, *required_types: str) -> None:
},
dependencies=[Depends(has_privilege("Node.Allocate"))],
)
async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node:
async def create_node(node_create: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node:
"""
Create a new node.
@@ -135,8 +137,8 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(
"""
controller = Controller.instance()
compute = controller.get_compute(str(node_data.compute_id))
node_data = jsonable_encoder(node_data, exclude_unset=True)
compute = controller.get_compute(str(node_create.compute_id))
node_data = jsonable_encoder(node_create, exclude_unset=True)
node = await project.add_node(compute, node_data.pop("name"), node_data.pop("node_id", None), **node_data)
return node.asdict()
@@ -274,14 +276,14 @@ async def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Node.Modify"))],
)
async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
async def update_node(node_update: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
"""
Update a node.
Required privilege: Node.Modify
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
node_data = jsonable_encoder(node_update, exclude_unset=True)
# Ignore these because we only use them when creating a node
node_data.pop("node_id", None)
@@ -654,9 +656,8 @@ async def ws_console(
return
compute = node.compute
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket"
)
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
log.info(f"New client {client} has connected to controller console WebSocket")
compute_host = compute.host
try:
@@ -690,9 +691,7 @@ async def ws_console(
await ws_console_compute.send_bytes(msg["bytes"])
except WebSocketDisconnect:
pass
log.info(
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller console WebSocket"
)
log.info(f"Client {client} has disconnected from controller console WebSocket")
async def ws_send(ws_console_compute):
"""
@@ -709,10 +708,7 @@ async def ws_console(
break
except WebSocketDisconnect:
# the client disconnected while the compute was still streaming console output
log.info(
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
f" console WebSocket"
)
log.info(f"Client {client} has disconnected from controller console WebSocket")
try:
# forward WebSocket data in both directions between the client and the compute console WebSocket
@@ -763,9 +759,8 @@ async def vnc_console(
return
compute = node.compute
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller VNC console WebSocket"
)
client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
log.info(f"New client {client} has connected to controller VNC console WebSocket")
compute_host = compute.host
try:
@@ -796,10 +791,7 @@ async def vnc_console(
await vnc_console_compute.send_bytes(data)
except WebSocketDisconnect:
pass
log.info(
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
f" VNC console WebSocket"
)
log.info(f"Client {client} has disconnected from controller VNC console WebSocket")
async def vnc_send(vnc_console_compute):
"""
@@ -814,10 +806,7 @@ async def vnc_console(
break
except WebSocketDisconnect:
# the client disconnected while the compute was still streaming VNC console output
log.info(
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
f" VNC console WebSocket"
)
log.info(f"Client {client} has disconnected from controller VNC console WebSocket")
try:
# forward WebSocket data in both directions between the client and the compute VNC console WebSocket
+11 -10
View File
@@ -21,12 +21,13 @@ API routes for resource pools.
from fastapi import APIRouter, Depends, status
from uuid import UUID
from typing import List
from typing import List, Optional
from gns3server import schemas
from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError, ControllerNotFoundError
from gns3server.controller import Controller
import gns3server.db.models as models
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
@@ -43,7 +44,7 @@ router = APIRouter()
@router.get("", response_model=List[schemas.ResourcePool], dependencies=[Depends(has_privilege("Pool.Audit"))])
async def get_resource_pools(
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> List[schemas.ResourcePool]:
) -> List[models.ResourcePool]:
"""
Get all resource pools.
@@ -62,7 +63,7 @@ async def get_resource_pools(
async def create_resource_pool(
resource_pool_create: schemas.ResourcePoolCreate,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> schemas.ResourcePool:
) -> models.ResourcePool:
"""
Create a new resource pool
@@ -80,7 +81,7 @@ async def create_resource_pool(
)
async def get_resource_pool(
resource_pool_id: UUID, pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.ResourcePool:
) -> models.ResourcePool:
"""
Get a resource pool.
@@ -100,7 +101,7 @@ async def update_resource_pool(
resource_pool_id: UUID,
resource_pool_update: schemas.ResourcePoolUpdate,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> schemas.ResourcePool:
) -> Optional[models.ResourcePool]:
"""
Update a resource pool.
@@ -176,7 +177,7 @@ async def delete_resource_pool(
async def get_pool_resources(
resource_pool_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> List[schemas.Resource]:
) -> List[models.Resource]:
"""
Get all resource in a pool.
@@ -215,13 +216,13 @@ async def add_resource_to_pool(
# we only support projects in resource pools for now
project = Controller.instance().get_project(str(resource_id))
resource = await pools_repo.get_resource(resource_id)
if not resource:
db_resource = await pools_repo.get_resource(resource_id)
if not db_resource:
# the resource is not in the database yet, create it
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
resource = await pools_repo.create_resource(resource_create)
db_resource = await pools_repo.create_resource(resource_create)
await pools_repo.add_resource_to_pool(resource_pool_id, resource)
await pools_repo.add_resource_to_pool(resource_pool_id, db_resource)
@router.delete(
@@ -16,6 +16,7 @@ API route for privileges
"""
from typing import List
import gns3server.db.models as models
from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository
from fastapi import APIRouter, Depends
@@ -33,7 +34,7 @@ router = APIRouter()
)
async def get_privileges(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> List[schemas.Privilege]:
) -> List[models.Privilege]:
"""
Get all privileges.
+12 -10
View File
@@ -34,7 +34,7 @@ from fastapi import APIRouter, Depends, Request, Body, HTTPException, status, We
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse, FileResponse
from websockets.exceptions import ConnectionClosed, WebSocketException
from typing import List, Optional
from typing import Any, List, Optional
from uuid import UUID
from gns3server import schemas
@@ -57,7 +57,9 @@ from .dependencies.rbac import has_privilege, has_privilege_on_websocket
from .dependencies.authentication import get_current_active_user
from .dependencies.database import get_repository
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project"}
}
router = APIRouter(responses=responses)
@@ -636,8 +638,8 @@ async def export_project(
include_images: bool = False,
reset_mac_addresses: bool = False,
keep_compute_ids: bool = False,
compression: schemas.ProjectCompression = "zstd",
compression_level: int = None,
compression: schemas.ProjectCompression = schemas.ProjectCompression.zstd,
compression_level: Optional[int] = None,
) -> StreamingResponse:
"""
Export a project as a portable archive.
@@ -650,19 +652,19 @@ async def export_project(
compression_query = compression.lower()
if compression_query == "zip":
compression = zipfile.ZIP_DEFLATED
zip_compression = zipfile.ZIP_DEFLATED
if compression_level is not None and (compression_level < 0 or compression_level > 9):
raise ControllerBadRequestError("Compression level must be between 0 and 9 for ZIP compression")
elif compression_query == "none":
compression = zipfile.ZIP_STORED
zip_compression = zipfile.ZIP_STORED
elif compression_query == "bzip2":
compression = zipfile.ZIP_BZIP2
zip_compression = zipfile.ZIP_BZIP2
if compression_level is not None and (compression_level < 1 or compression_level > 9):
raise ControllerBadRequestError("Compression level must be between 1 and 9 for BZIP2 compression")
elif compression_query == "lzma":
compression = zipfile.ZIP_LZMA
zip_compression = zipfile.ZIP_LZMA
elif compression_query == "zstd":
compression = zipfile.ZIP_ZSTANDARD
zip_compression = zipfile.ZIP_ZSTANDARD
if compression_level is not None and (compression_level < 1 or compression_level > 22):
raise ControllerBadRequestError("Compression level must be between 1 and 22 for Zstandard compression")
@@ -681,7 +683,7 @@ async def export_project(
f"Exporting project '{project.name}' with '{compression_query}' compression (level {compression_level})"
)
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
with aiozipstream.ZipFile(compression=compression, compresslevel=compression_level) as zstream:
with aiozipstream.ZipFile(compression=zip_compression, compresslevel=compression_level) as zstream:
await export_controller_project(
zstream,
project,
+7 -6
View File
@@ -21,7 +21,7 @@ API routes for roles.
from fastapi import APIRouter, Depends, status
from uuid import UUID
from typing import List
from typing import List, Optional
from gns3server import schemas
from gns3server.controller.controller_error import (
@@ -31,6 +31,7 @@ from gns3server.controller.controller_error import (
ControllerForbiddenError,
)
import gns3server.db.models as models
from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
@@ -43,7 +44,7 @@ router = APIRouter()
@router.get("", response_model=List[schemas.Role], dependencies=[Depends(has_privilege("Role.Audit"))])
async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.Role]:
async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[models.Role]:
"""
Get all roles.
@@ -61,7 +62,7 @@ async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepos
)
async def create_role(
role_create: schemas.RoleCreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role:
) -> Optional[models.Role]:
"""
Create a new role.
@@ -78,7 +79,7 @@ async def create_role(
async def get_role(
role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.Role:
) -> models.Role:
"""
Get a role.
@@ -94,7 +95,7 @@ async def get_role(
@router.put("/{role_id}", response_model=schemas.Role, dependencies=[Depends(has_privilege("Role.Modify"))])
async def update_role(
role_id: UUID, role_update: schemas.RoleUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role:
) -> Optional[models.Role]:
"""
Update a role.
@@ -142,7 +143,7 @@ async def delete_role(
)
async def get_role_privileges(
role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Privilege]:
) -> List[models.Privilege]:
"""
Get all role privileges.
@@ -24,7 +24,7 @@ import logging
log = logging.getLogger()
from fastapi import APIRouter, Depends, status
from typing import List
from typing import Any, List
from uuid import UUID
from gns3server.controller.project import Project
@@ -35,7 +35,9 @@ from gns3server.controller import Controller
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}
}
router = APIRouter(responses=responses)
+11 -9
View File
@@ -27,7 +27,7 @@ import logging
log = logging.getLogger(__name__)
from fastapi import APIRouter, Request, HTTPException, Depends, Response, status, Query
from typing import List, Optional
from typing import Any, List, Optional, Union
from uuid import UUID
from gns3server import schemas
@@ -43,7 +43,9 @@ from .dependencies.authentication import get_current_active_user
from .dependencies.rbac import has_privilege
from .dependencies.database import get_repository
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}}
responses: dict[int | str, dict[str, Any]] = {
404: {"model": schemas.ErrorMessage, "description": "Could not find template"}
}
router = APIRouter(responses=responses)
@@ -57,7 +59,7 @@ router = APIRouter(responses=responses)
async def create_template(
template_create: schemas.TemplateCreate,
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
) -> schemas.Template:
) -> dict:
"""
Create a new template.
@@ -80,7 +82,7 @@ async def get_template(
request: Request,
response: Response,
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
) -> schemas.Template:
) -> Union[dict, Response]:
"""
Return a template.
@@ -108,7 +110,7 @@ async def update_template(
template_id: UUID,
template_update: schemas.TemplateUpdate,
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
) -> schemas.Template:
) -> dict:
"""
Update a template.
@@ -125,7 +127,7 @@ async def delete_template(
template_id: UUID,
prune_images: Optional[bool] = False,
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
images_repo: RbacRepository = Depends(get_repository(ImagesRepository)),
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
@@ -155,7 +157,7 @@ async def delete_template(
if str(template.template_id) != str(template_id)
]
if other_templates:
template_names = ", ".join([template.name for template in other_templates])
template_names = ", ".join([str(template.name) for template in other_templates])
raise ControllerError(f"Image '{image.path}' is used by one or more templates: {template_names}")
if referenced_filenames is None:
@@ -193,7 +195,7 @@ async def get_templates(
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
current_user: schemas.User = Depends(get_current_active_user),
tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)"),
) -> List[schemas.Template]:
) -> List[dict]:
"""
Return all templates.
@@ -244,7 +246,7 @@ async def get_templates(
)
async def duplicate_template(
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
) -> schemas.Template:
) -> dict:
"""
Duplicate a template.
+8 -7
View File
@@ -22,7 +22,7 @@ API routes for users.
from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi.security import OAuth2PasswordRequestForm
from uuid import UUID
from typing import List
from typing import List, Optional
from gns3server import schemas
from gns3server.controller.controller_error import (
@@ -32,6 +32,7 @@ from gns3server.controller.controller_error import (
ControllerForbiddenError,
)
import gns3server.db.models as models
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services import auth_service
@@ -168,7 +169,7 @@ async def update_logged_in_user(
user_update: schemas.LoggedInUserUpdate,
current_user: schemas.User = Depends(get_current_active_user),
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.User:
) -> Optional[models.User]:
"""
Update the current active user.
"""
@@ -180,7 +181,7 @@ async def update_logged_in_user(
@router.get("", response_model=List[schemas.User], dependencies=[Depends(has_privilege("User.Audit"))])
async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[schemas.User]:
async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[models.User]:
"""
Get all users.
@@ -198,7 +199,7 @@ async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRe
)
async def create_user(
user_create: schemas.UserCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> schemas.User:
) -> models.User:
"""
Create a new user.
@@ -218,7 +219,7 @@ async def create_user(
async def get_user(
user_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.User:
) -> models.User:
"""
Get a user.
@@ -236,7 +237,7 @@ async def update_user(
user_id: UUID,
user_update: schemas.UserUpdate,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.User:
) -> Optional[models.User]:
"""
Update a user.
@@ -287,7 +288,7 @@ async def delete_user(
)
async def get_user_memberships(
user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.UserGroup]:
) -> List[models.UserGroup]:
"""
Get user memberships.
+4 -2
View File
@@ -19,6 +19,8 @@
FastAPI app
"""
from typing import cast
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
@@ -118,7 +120,7 @@ def handle_exit(*args, **kwargs):
unicorn_exit_handler(*args, **kwargs)
UvicornServer.handle_exit = handle_exit
UvicornServer.handle_exit = handle_exit # type: ignore[method-assign]
# Configure self-hosting JavaScript and CSS for docs
@@ -134,7 +136,7 @@ async def custom_swagger_ui_html():
)
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
@app.get(cast(str, app.swagger_ui_oauth2_redirect_url), include_in_schema=False)
async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html()