mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Merge branch '3.1' into image-manager-out-of-sync
This commit is contained in:
129 files changed
+944
-863
No files matched your search
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user