mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
Merge branch '3.1' into update-dependencies
This commit is contained in:
34 files changed
+1284
-334
No files matched your search
@@ -64,7 +64,6 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
|
||||
|
||||
dynamips_manager = Dynamips.instance()
|
||||
platform = node_data.platform
|
||||
print(node_data.chassis, platform in DEFAULT_CHASSIS)
|
||||
if not node_data.chassis and platform in DEFAULT_CHASSIS:
|
||||
chassis = DEFAULT_CHASSIS[platform]
|
||||
else:
|
||||
|
||||
@@ -21,6 +21,8 @@ API routes for projects.
|
||||
import os
|
||||
import shutil
|
||||
import urllib.parse
|
||||
import inspect
|
||||
import asyncio
|
||||
|
||||
import logging
|
||||
|
||||
@@ -34,6 +36,7 @@ from uuid import UUID
|
||||
|
||||
from gns3server.compute.project_manager import ProjectManager
|
||||
from gns3server.compute.project import Project
|
||||
from gns3server.compute.base_manager import BaseManager
|
||||
from gns3server.utils.path import is_safe_path
|
||||
from gns3server import schemas
|
||||
|
||||
@@ -131,6 +134,190 @@ async def delete_compute_project(project: Project = Depends(dep_project)) -> Non
|
||||
ProjectManager.instance().remove_project(project.id)
|
||||
|
||||
|
||||
async def _add_nio_binding(node, adapter_number, port_number, nio):
|
||||
"""
|
||||
Unified NIO-binding dispatch across node types. Each node type exposes a
|
||||
different method signature, so centralise the fan-out here for the batch
|
||||
endpoint. Dispatch keys off the manager class name (only dynamips/iou/qemu
|
||||
carry a ``_NODE_TYPE`` attribute, so it can't be used universally).
|
||||
"""
|
||||
|
||||
manager_name = type(node.manager).__name__
|
||||
# Adapter-based nodes: docker / qemu / vmware / virtualbox take
|
||||
# (adapter_number, nio); iou additionally takes port_number.
|
||||
if manager_name in ("Docker", "Qemu", "VMware", "VirtualBox"):
|
||||
await node.adapter_add_nio_binding(adapter_number, nio)
|
||||
elif manager_name == "IOU":
|
||||
await node.adapter_add_nio_binding(adapter_number, port_number, nio)
|
||||
elif manager_name == "VPCS":
|
||||
await node.port_add_nio_binding(port_number, nio)
|
||||
elif manager_name == "Dynamips":
|
||||
# Dynamips routers use slot_add_nio_binding(slot, port, nio);
|
||||
# Dynamips switches/hubs use add_nio(nio, port_number).
|
||||
if hasattr(node, "slot_add_nio_binding"):
|
||||
await node.slot_add_nio_binding(adapter_number, port_number, nio)
|
||||
else:
|
||||
await node.add_nio(nio, port_number)
|
||||
elif manager_name == "Builtin":
|
||||
# ethernet_switch / ethernet_hub / cloud / nat: add_nio(nio, port_number)
|
||||
await node.add_nio(nio, port_number)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Batch NIO creation not supported for node type '{manager_name}'",
|
||||
)
|
||||
|
||||
|
||||
def _get_existing_nio(node, adapter_number, port_number):
|
||||
"""
|
||||
Fetch the already-bound NIO for a port, preserving its UDP endpoints
|
||||
(lport/rhost/rport) so a marker/filter update only changes markers/filters.
|
||||
Dispatch keys off the manager class name, mirroring _add_nio_binding.
|
||||
"""
|
||||
|
||||
manager_name = type(node.manager).__name__
|
||||
if manager_name in ("Docker", "Qemu", "VMware", "VirtualBox"):
|
||||
return node.get_nio(adapter_number)
|
||||
elif manager_name == "IOU":
|
||||
return node.get_nio(adapter_number, port_number)
|
||||
elif manager_name in ("VPCS", "Builtin"):
|
||||
return node.get_nio(port_number)
|
||||
elif manager_name == "Dynamips":
|
||||
# Dynamips routers expose NIOs via the slot/adapter; switches/hubs
|
||||
# via get_nio(port).
|
||||
if hasattr(node, "get_nio"):
|
||||
import inspect as _inspect
|
||||
if len(_inspect.signature(node.get_nio).parameters) >= 2:
|
||||
return node.get_nio(adapter_number, port_number)
|
||||
return node.get_nio(port_number)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Dynamips node '{node.name}' has no get_nio for batch update",
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Batch NIO update not supported for node type '{manager_name}'",
|
||||
)
|
||||
|
||||
|
||||
async def _update_nio_binding(node, adapter_number, port_number, nio):
|
||||
"""
|
||||
Re-apply a NIO binding (filters + markers) to a started node's uBridge.
|
||||
Dispatch keys off the manager class name, mirroring _add_nio_binding.
|
||||
"""
|
||||
|
||||
manager_name = type(node.manager).__name__
|
||||
if manager_name in ("Docker", "Qemu", "VMware", "VirtualBox"):
|
||||
await node.adapter_update_nio_binding(adapter_number, nio)
|
||||
elif manager_name == "IOU":
|
||||
await node.adapter_update_nio_binding(adapter_number, port_number, nio)
|
||||
elif manager_name == "VPCS":
|
||||
await node.port_update_nio_binding(port_number, nio)
|
||||
elif manager_name == "Dynamips":
|
||||
if hasattr(node, "slot_update_nio_binding"):
|
||||
await node.slot_update_nio_binding(adapter_number, port_number, nio)
|
||||
else:
|
||||
await node.update_nio(port_number, nio)
|
||||
elif manager_name == "Builtin":
|
||||
await node.update_nio(port_number, nio)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Batch NIO update not supported for node type '{manager_name}'",
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/nios/batch",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_batch_nios(
|
||||
project_id: UUID,
|
||||
batch: schemas.BatchNIOCreate,
|
||||
project: Project = Depends(dep_project),
|
||||
) -> dict:
|
||||
"""
|
||||
Create many NIO bindings across nodes in a single request.
|
||||
|
||||
Used by the controller during project open to avoid one HTTP round-trip per
|
||||
NIO. Each entry resolves its node via the project, builds the NIO through
|
||||
the node's manager, and binds it. Nodes that are not started perform the
|
||||
binding in memory; started nodes additionally wire uBridge.
|
||||
"""
|
||||
|
||||
# Group entries by node so different nodes' uBridge processes are wired
|
||||
# in parallel (each node has its own AF_UNIX socket). Within a node
|
||||
# entries are serial to respect the per-node uBridge command lock.
|
||||
# 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 = {}
|
||||
for entry in batch.nios:
|
||||
per_node.setdefault(entry.node_id, []).append(entry)
|
||||
|
||||
async def _create_one_node(node_id, entries):
|
||||
node = project.get_node(node_id)
|
||||
# Dynamips.create_nio(self, node, nio_settings) is async and takes
|
||||
# an extra positional 'node'; other managers' create_nio(nio_settings)
|
||||
# is synchronous. Detect once per node via bound-method parameter
|
||||
# count (standard == 1, Dynamips == 2) and await the async variant.
|
||||
sig = inspect.signature(node.manager.create_nio)
|
||||
dynamips_style = len(sig.parameters) >= 2
|
||||
for entry in entries:
|
||||
nio_settings = jsonable_encoder(entry.nio, exclude_unset=True)
|
||||
if dynamips_style:
|
||||
nio = await node.manager.create_nio(node, nio_settings)
|
||||
else:
|
||||
nio = node.manager.create_nio(nio_settings)
|
||||
await _add_nio_binding(node, entry.adapter_number, entry.port_number, nio)
|
||||
|
||||
await asyncio.gather(
|
||||
*[_create_one_node(nid, ents) for nid, ents in per_node.items()]
|
||||
)
|
||||
return {"added": len(batch.nios)}
|
||||
|
||||
|
||||
@router.put(
|
||||
"/projects/{project_id}/nios/batch",
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
async def update_batch_nios(
|
||||
project_id: UUID,
|
||||
batch: schemas.BatchNIOCreate,
|
||||
project: Project = Depends(dep_project),
|
||||
) -> dict:
|
||||
"""
|
||||
Update many NIO bindings (filters + markers) across nodes in a single
|
||||
request, re-applying them to uBridge on started nodes.
|
||||
|
||||
Used by the controller when a project-level marker definition changes and
|
||||
must fan out to every affected link — replacing one PUT /nio round-trip per
|
||||
link end with one round-trip per compute. Each entry fetches the already-
|
||||
bound NIO (preserving its UDP endpoints), overlays the new markers/filters,
|
||||
and re-binds it.
|
||||
"""
|
||||
|
||||
# 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 = {}
|
||||
for entry in batch.nios:
|
||||
per_node.setdefault(entry.node_id, []).append(entry)
|
||||
|
||||
async def _update_one_node(node_id, entries):
|
||||
node = project.get_node(node_id)
|
||||
for e in entries:
|
||||
nio = _get_existing_nio(node, e.adapter_number, e.port_number)
|
||||
nio.filters = e.nio.filters or {}
|
||||
nio.markers = e.nio.markers or {}
|
||||
await _update_nio_binding(node, e.adapter_number, e.port_number, nio)
|
||||
|
||||
await asyncio.gather(
|
||||
*[_update_one_node(nid, ents) for nid, ents in per_node.items()]
|
||||
)
|
||||
return {"updated": len(batch.nios)}
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}/files", response_model=List[schemas.ProjectFile])
|
||||
async def get_compute_project_files(project: Project = Depends(dep_project)) -> List[schemas.ProjectFile]:
|
||||
"""
|
||||
|
||||
@@ -83,7 +83,7 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
|
||||
for disk_index, drive in enumerate(drives):
|
||||
disk_image_backing_file = node_data.get(f"hd{drive}_disk_image_backing_file")
|
||||
if disk_image_backing_file:
|
||||
log.info(f"Updating disk image for drive {drive} with backing file {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
|
||||
|
||||
for name, value in node_data.items():
|
||||
|
||||
@@ -485,6 +485,38 @@ async def project_ws_notifications(
|
||||
await project.close()
|
||||
|
||||
|
||||
@router.websocket("/{project_id}/notifications/markers/ws")
|
||||
async def project_marker_ws_notifications(
|
||||
project_id: UUID,
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(has_privilege_on_websocket("Project.Audit"))
|
||||
) -> None:
|
||||
"""
|
||||
Receive marker notifications (e.g. marker.match) for a project on a
|
||||
dedicated WebSocket, separate from the main project stream so high-frequency
|
||||
marker.matches do not block topology events (node.*/link.*).
|
||||
|
||||
Required privilege: Project.Audit
|
||||
"""
|
||||
|
||||
if current_user is None:
|
||||
return
|
||||
|
||||
controller = Controller.instance()
|
||||
project = controller.get_project(str(project_id))
|
||||
|
||||
log.info(f"New client has connected to the marker notification stream for project ID '{project.id}' (WebSocket method)")
|
||||
try:
|
||||
with controller.notification.project_marker_queue(project.id) as queue:
|
||||
while True:
|
||||
notification = await queue.get_json(5)
|
||||
await websocket.send_text(notification)
|
||||
except (ConnectionClosed, WebSocketDisconnect):
|
||||
log.info(f"Client has disconnected from the marker notification stream for project ID '{project.id}' (WebSocket method)")
|
||||
except WebSocketException as e:
|
||||
log.warning(f"Error while sending marker event to WebSocket client: {e}")
|
||||
|
||||
|
||||
@router.get("/{project_id}/export", dependencies=[Depends(has_privilege("Project.Audit"))])
|
||||
async def export_project(
|
||||
project: Project = Depends(dep_project),
|
||||
|
||||
Reference in new issue
Block a user