mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
feat(compute): add SPICE WebSocket console support
Add WebSocket-based console support for SPICE protocol, enabling
browser-based graphical console access with enhanced features
like clipboard sharing, USB redirection, and audio streaming.
Changes:
- Add start_spice_websocket_console() method in BaseNode for SPICE
WebSocket to TCP bridging with bidirectional binary forwarding
- Add /console/spice WebSocket endpoints in QEMU and Docker compute APIs
- Add /console/spice WebSocket proxy endpoint in controller API
- Add comprehensive API documentation in docs/features/
Supported console types:
- spice: Basic SPICE protocol support
- spice+agent: SPICE with spice-vdagent for enhanced features
Architecture:
- Browser WebSocket → Controller (JWT + RBAC) → Compute (Basic Auth)
- No external websockify processes required
- Consistent with existing VNC WebSocket implementation
This commit is contained in:
1 parent
ba92b405da
commit
c98f802362
5 files changed
+1046
No files matched your search
@@ -399,6 +399,21 @@ async def vnc_console_ws(
|
||||
await node.start_vnc_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.websocket(
|
||||
"/{node_id}/console/spice"
|
||||
)
|
||||
async def spice_console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: DockerVM = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
SPICE Console WebSocket.
|
||||
"""
|
||||
|
||||
if websocket:
|
||||
await node.start_spice_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
|
||||
@@ -430,6 +430,21 @@ async def vnc_console_ws(
|
||||
await node.start_vnc_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.websocket(
|
||||
"/{node_id}/console/spice"
|
||||
)
|
||||
async def spice_console_ws(
|
||||
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
|
||||
node: QemuVM = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
SPICE Console WebSocket.
|
||||
"""
|
||||
|
||||
if websocket:
|
||||
await node.start_spice_websocket_console(websocket)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{node_id}/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
|
||||
@@ -721,6 +721,81 @@ async def vnc_console(
|
||||
except aiohttp.ClientError as e:
|
||||
log.error(f"Client error received when forwarding to compute VNC console WebSocket: {e}")
|
||||
|
||||
@router.websocket("/{node_id}/console/spice")
|
||||
async def spice_console(
|
||||
websocket: WebSocket,
|
||||
current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")),
|
||||
node: Node = Depends(dep_node)
|
||||
) -> None:
|
||||
"""
|
||||
SPICE WebSocket console.
|
||||
|
||||
Required privilege: Node.Console
|
||||
"""
|
||||
|
||||
if current_user is None:
|
||||
return
|
||||
|
||||
compute = node.compute
|
||||
log.info(
|
||||
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller SPICE console WebSocket"
|
||||
)
|
||||
|
||||
compute_host = compute.host
|
||||
try:
|
||||
# handle IPv6 address
|
||||
ip = ipaddress.ip_address(compute_host)
|
||||
if isinstance(ip, ipaddress.IPv6Address):
|
||||
compute_host = '[' + compute_host + ']'
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
spice_console_compute_url = (
|
||||
f"{websocket.url.scheme}://{compute_host}:{compute.port}/v3/compute/projects/"
|
||||
f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/spice"
|
||||
)
|
||||
|
||||
async def spice_receive(spice_console_compute):
|
||||
"""
|
||||
Receive binary WebSocket data from client and forward to compute SPICE console WebSocket.
|
||||
"""
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_bytes()
|
||||
if data:
|
||||
await spice_console_compute.send_bytes(data)
|
||||
except WebSocketDisconnect:
|
||||
await spice_console_compute.close()
|
||||
log.info(
|
||||
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
|
||||
f" SPICE console WebSocket"
|
||||
)
|
||||
|
||||
try:
|
||||
# receive binary data from compute SPICE console WebSocket and forward to client
|
||||
log.info(f"Forwarding SPICE console WebSocket to '{spice_console_compute_url}'")
|
||||
server_config = Config.instance().settings.Server
|
||||
user = server_config.compute_username
|
||||
password = server_config.compute_password
|
||||
if not user:
|
||||
raise ControllerForbiddenError("Compute username is not set")
|
||||
user = user.strip()
|
||||
if user and password:
|
||||
auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8")
|
||||
else:
|
||||
auth = aiohttp.BasicAuth(user, "")
|
||||
ssl_context = Controller.instance().ssl_context()
|
||||
async with HTTPClient.get_client().ws_connect(spice_console_compute_url, auth=auth, ssl_context=ssl_context) as ws:
|
||||
asyncio.ensure_future(spice_receive(ws))
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.BINARY:
|
||||
await websocket.send_bytes(msg.data)
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
break
|
||||
except aiohttp.ClientError as e:
|
||||
log.error(f"Client error received when forwarding to compute SPICE console WebSocket: {e}")
|
||||
|
||||
@router.post(
|
||||
"/console/reset",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
|
||||
Reference in new issue
Block a user