Refactor WebSocket console code to work with FastAPI.

Fix endpoint routes.
This commit is contained in:
grossmj 2020-10-19 15:00:41 +10:30
parent 5341ccdbd6
commit bd8565b2b9
27 changed files with 338 additions and 370 deletions

View File

@ -18,8 +18,6 @@
import sys import sys
import os import os
import stat import stat
import logging
import aiohttp
import shutil import shutil
import asyncio import asyncio
import tempfile import tempfile
@ -27,7 +25,7 @@ import psutil
import platform import platform
import re import re
from aiohttp.web import WebSocketResponse from fastapi import WebSocketDisconnect
from gns3server.utils.interfaces import interfaces from gns3server.utils.interfaces import interfaces
from gns3server.compute.compute_error import ComputeError from gns3server.compute.compute_error import ComputeError
from ..compute.port_manager import PortManager from ..compute.port_manager import PortManager
@ -38,7 +36,7 @@ from ..ubridge.ubridge_error import UbridgeError
from .nios.nio_udp import NIOUDP from .nios.nio_udp import NIOUDP
from .error import NodeError from .error import NodeError
import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -414,7 +412,7 @@ class BaseNode:
await self.stop_wrap_console() await self.stop_wrap_console()
await self.start_wrap_console() await self.start_wrap_console()
async def start_websocket_console(self, request): async def start_websocket_console(self, websocket):
""" """
Connect to console using Websocket. Connect to console using Websocket.
@ -428,47 +426,45 @@ class BaseNode:
raise NodeError("Node {} console type is not telnet".format(self.name)) raise NodeError("Node {} console type is not telnet".format(self.name))
try: try:
(telnet_reader, telnet_writer) = await asyncio.open_connection(self._manager.port_manager.console_host, self.console) (telnet_reader, telnet_writer) = await asyncio.open_connection(self._manager.port_manager.console_host,
self.console)
except ConnectionError as e: except ConnectionError as e:
raise NodeError("Cannot connect to node {} telnet server: {}".format(self.name, e)) raise NodeError("Cannot connect to node {} telnet server: {}".format(self.name, e))
log.info("Connected to Telnet server") log.info("Connected to Telnet server")
ws = WebSocketResponse() await websocket.accept()
await ws.prepare(request) log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute"
request.app['websockets'].add(ws) f" console WebSocket")
log.info("New client has connected to console WebSocket")
async def ws_forward(telnet_writer): async def ws_forward(telnet_writer):
async for msg in ws: try:
if msg.type == aiohttp.WSMsgType.TEXT: while True:
telnet_writer.write(msg.data.encode()) data = await websocket.receive_text()
await telnet_writer.drain() if data:
elif msg.type == aiohttp.WSMsgType.BINARY: telnet_writer.write(data.encode())
await telnet_writer.write(msg.data) await telnet_writer.drain()
await telnet_writer.drain() except WebSocketDisconnect:
elif msg.type == aiohttp.WSMsgType.ERROR: log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute"
log.debug("Websocket connection closed with exception {}".format(ws.exception())) f" console WebSocket")
async def telnet_forward(telnet_reader): async def telnet_forward(telnet_reader):
while not ws.closed and not telnet_reader.at_eof(): while not telnet_reader.at_eof():
data = await telnet_reader.read(1024) data = await telnet_reader.read(1024)
if data: if data:
await ws.send_bytes(data) await websocket.send_bytes(data)
try: # keep forwarding WebSocket data in both direction
# keep forwarding websocket data in both direction done, pending = await asyncio.wait([ws_forward(telnet_writer), telnet_forward(telnet_reader)],
await asyncio.wait([ws_forward(telnet_writer), telnet_forward(telnet_reader)], return_when=asyncio.FIRST_COMPLETED) return_when=asyncio.FIRST_COMPLETED)
finally: for task in done:
log.info("Client has disconnected from console WebSocket") if task.exception():
if not ws.closed: log.warning(f"Exception while forwarding WebSocket data to Telnet server {task.exception()}")
await ws.close()
request.app['websockets'].discard(ws)
return ws for task in pending:
task.cancel()
@property @property
def aux(self): def aux(self):

View File

@ -47,7 +47,7 @@ async def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.ATMSwitch, response_model=schemas.ATMSwitch,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create ATM switch node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create ATM switch node"}})

View File

@ -48,7 +48,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.Cloud, response_model=schemas.Cloud,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create cloud node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create cloud node"}})

View File

@ -21,7 +21,7 @@ API endpoints for Docker nodes.
import os import os
from fastapi import APIRouter, Depends, Body, status from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
@ -47,7 +47,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.Docker, response_model=schemas.Docker,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}})
@ -290,14 +290,6 @@ async def stop_capture(adapter_number: int, port_number: int, node: DockerVM = D
await node.stop_capture(adapter_number) await node.stop_capture(adapter_number)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: DockerVM = Depends(dep_node)):
await node.reset_console()
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap", @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
responses=responses) responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)): async def stream_pcap_file(adapter_number: int, port_number: int, node: DockerVM = Depends(dep_node)):
@ -310,18 +302,19 @@ async def stream_pcap_file(adapter_number: int, port_number: int, node: DockerVM
stream = Docker.instance().stream_pcap_file(nio, node.project.id) stream = Docker.instance().stream_pcap_file(nio, node.project.id)
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
# @Route.get(
# r"/projects/{project_id}/docker/nodes/{node_id}/console/ws", @router.websocket("/{node_id}/console/ws")
# description="WebSocket for console", async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)):
# parameters={ """
# "project_id": "Project UUID", Console WebSocket.
# "node_id": "Node UUID", """
# })
# async def console_ws(request, response): await node.start_websocket_console(websocket)
#
# docker_manager = Docker.instance()
# container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
# return await container.start_websocket_console(request)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: DockerVM = Depends(dep_node)):
await node.reset_console()

View File

@ -22,7 +22,7 @@ API endpoints for Dynamips nodes.
import os import os
import sys import sys
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import List from typing import List
@ -56,7 +56,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.Dynamips, response_model=schemas.Dynamips,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}})
@ -299,18 +299,13 @@ async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep
return new_node.__json__() return new_node.__json__()
# @Route.get( @router.websocket("/{node_id}/console/ws")
# r"/projects/{project_id}/dynamips/nodes/{node_id}/console/ws", async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)):
# description="WebSocket for console", """
# parameters={ Console WebSocket.
# "project_id": "Project UUID", """
# "node_id": "Node UUID",
# }) await node.start_websocket_console(websocket)
# async def console_ws(request, response):
#
# dynamips_manager = Dynamips.instance()
# vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
# return await vm.start_websocket_console(request)
@router.post("/{node_id}/console/reset", @router.post("/{node_id}/console/reset",

View File

@ -47,7 +47,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.EthernetHub, response_model=schemas.EthernetHub,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet hub node"}})

View File

@ -47,7 +47,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.EthernetSwitch, response_model=schemas.EthernetSwitch,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet switch node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Ethernet switch node"}})

View File

@ -47,7 +47,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.FrameRelaySwitch, response_model=schemas.FrameRelaySwitch,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Frame Relay switch node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Frame Relay switch node"}})

View File

@ -21,7 +21,7 @@ API endpoints for IOU nodes.
import os import os
from fastapi import APIRouter, Depends, Body, status from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import Union from typing import Union
@ -48,7 +48,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.IOU, response_model=schemas.IOU,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}})
@ -275,23 +275,18 @@ async def stream_pcap_file(adapter_number: int, port_number: int, node: IOUVM =
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)):
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", @router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,
responses=responses) responses=responses)
async def reset_console(node: IOUVM = Depends(dep_node)): async def reset_console(node: IOUVM = Depends(dep_node)):
await node.reset_console() await node.reset_console()
# @Route.get(
# r"/projects/{project_id}/iou/nodes/{node_id}/console/ws",
# description="WebSocket for console",
# parameters={
# "project_id": "Project UUID",
# "node_id": "Node UUID",
# })
# async def console_ws(request, response):
#
# iou_manager = IOU.instance()
# vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
# return await vm.start_websocket_console(request)

View File

@ -48,7 +48,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.NAT, response_model=schemas.NAT,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create NAT node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create NAT node"}})

View File

@ -19,11 +19,10 @@
API endpoints for compute notifications. API endpoints for compute notifications.
""" """
from fastapi import APIRouter, WebSocket, WebSocketDisconnect import asyncio
from websockets.exceptions import WebSocketException from fastapi import APIRouter, WebSocket
from typing import List
from gns3server.compute.notification_manager import NotificationManager from gns3server.compute.notification_manager import NotificationManager
from starlette.endpoints import WebSocketEndpoint
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -31,48 +30,63 @@ log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
class ConnectionManager: @router.websocket_route("/notifications/ws")
def __init__(self): class ComputeWebSocketNotifications(WebSocketEndpoint):
self.active_connections: List[WebSocket] = [] """
Receive compute notifications about the controller from WebSocket stream.
"""
async def on_connect(self, websocket: WebSocket) -> None:
async def connect(self, websocket: WebSocket):
await websocket.accept() await websocket.accept()
self.active_connections.append(websocket) log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
self._notification_task = asyncio.ensure_future(self._stream_notifications(websocket))
def disconnect(self, websocket: WebSocket): async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
self.active_connections.remove(websocket) self._notification_task.cancel()
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket"
f" with close code {close_code}")
async def close_active_connections(self): async def _stream_notifications(self, websocket: WebSocket) -> None:
for websocket in self.active_connections: with NotificationManager.instance().queue() as queue:
await websocket.close()
async def send_text(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@router.websocket("/notifications/ws")
async def compute_notifications(websocket: WebSocket):
log.info("Client has disconnected from compute WebSocket")
notifications = NotificationManager.instance()
await manager.connect(websocket)
try:
log.info("New client has connected to compute WebSocket")
with notifications.queue() as queue:
while True: while True:
notification = await queue.get_json(5) notification = await queue.get_json(5)
await manager.send_text(notification, websocket) await websocket.send_text(notification)
except (WebSocketException, WebSocketDisconnect) as e:
log.info("Client has disconnected from compute WebSocket: {}".format(e))
finally: if __name__ == '__main__':
await websocket.close()
manager.disconnect(websocket) import uvicorn
from fastapi import FastAPI
from starlette.responses import HTMLResponse
app = FastAPI()
app.include_router(router)
html = """
<!DOCTYPE html>
<html>
<body>
<ul id='messages'>
</ul>
<script>
var ws = new WebSocket("ws://localhost:8000/notifications/ws");
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
</script>
</body>
</html>
"""
@app.get("/")
async def get() -> HTMLResponse:
return HTMLResponse(html)
uvicorn.run(app, host="localhost", port=8000)

View File

@ -19,8 +19,6 @@
API endpoints for projects. API endpoints for projects.
""" """
import shutil
import aiohttp
import os import os
import logging import logging
@ -108,6 +106,7 @@ async def close_project(project: Project = Depends(dep_project)):
Close a project on the compute. Close a project on the compute.
""" """
# FIXME
if _notifications_listening.setdefault(project.id, 0) <= 1: if _notifications_listening.setdefault(project.id, 0) <= 1:
await project.close() await project.close()
ProjectManager.instance().remove_project(project.id) ProjectManager.instance().remove_project(project.id)
@ -234,6 +233,6 @@ async def write_file(file_path: str, request: Request, project: Project = Depend
pass # FIXME pass # FIXME
except FileNotFoundError: except FileNotFoundError:
raise aiohttp.web.HTTPNotFound() raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except PermissionError: except PermissionError:
raise aiohttp.web.HTTPForbidden() raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)

View File

@ -22,14 +22,13 @@ API endpoints for Qemu nodes.
import os import os
import sys import sys
from fastapi import APIRouter, Depends, Body, status from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
from gns3server.endpoints import schemas from gns3server.endpoints import schemas
from gns3server.compute.project_manager import ProjectManager from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.compute_error import ComputeError
from gns3server.compute.qemu import Qemu from gns3server.compute.qemu import Qemu
from gns3server.compute.qemu.qemu_vm import QemuVM from gns3server.compute.qemu.qemu_vm import QemuVM
@ -50,7 +49,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.Qemu, response_model=schemas.Qemu,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}})
@ -281,14 +280,6 @@ async def stop_capture(adapter_number: int, port_number: int, node: QemuVM = Dep
await node.stop_capture(adapter_number) await node.stop_capture(adapter_number)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: QemuVM = Depends(dep_node)):
await node.reset_console()
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap", @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
responses=responses) responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)): async def stream_pcap_file(adapter_number: int, port_number: int, node: QemuVM = Depends(dep_node)):
@ -302,16 +293,18 @@ async def stream_pcap_file(adapter_number: int, port_number: int, node: QemuVM =
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
# @Route.get( @router.websocket("/{node_id}/console/ws")
# r"/projects/{project_id}/qemu/nodes/{node_id}/console/ws", async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)):
# description="WebSocket for console", """
# parameters={ Console WebSocket.
# "project_id": "Project UUID", """
# "node_id": "Node UUID",
# })
# async def console_ws(request, response):
#
# qemu_manager = Qemu.instance()
# vm = qemu_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
# return await vm.start_websocket_console(request)
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: QemuVM = Depends(dep_node)):
await node.reset_console()

View File

@ -21,7 +21,7 @@ API endpoints for VirtualBox nodes.
import os import os
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
@ -49,7 +49,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.VirtualBox, response_model=schemas.VirtualBox,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}})
@ -288,14 +288,6 @@ async def stop_capture(adapter_number: int, port_number: int, node: VirtualBoxVM
await node.stop_capture(adapter_number) await node.stop_capture(adapter_number)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: VirtualBoxVM = Depends(dep_node)):
await node.reset_console()
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap", @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
responses=responses) responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)): async def stream_pcap_file(adapter_number: int, port_number: int, node: VirtualBoxVM = Depends(dep_node)):
@ -309,15 +301,18 @@ async def stream_pcap_file(adapter_number: int, port_number: int, node: VirtualB
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
# @Route.get( @router.websocket("/{node_id}/console/ws")
# r"/projects/{project_id}/virtualbox/nodes/{node_id}/console/ws", async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node)):
# description="WebSocket for console", """
# parameters={ Console WebSocket.
# "project_id": "Project UUID", """
# "node_id": "Node UUID",
# }) await node.start_websocket_console(websocket)
# async def console_ws(request, response):
#
# virtualbox_manager = VirtualBox.instance() @router.post("/{node_id}/console/reset",
# vm = virtualbox_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) status_code=status.HTTP_204_NO_CONTENT,
# return await vm.start_websocket_console(request) responses=responses)
async def reset_console(node: VirtualBoxVM = Depends(dep_node)):
await node.reset_console()

View File

@ -21,7 +21,7 @@ API endpoints for VMware nodes.
import os import os
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
@ -48,7 +48,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.VMware, response_model=schemas.VMware,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
@ -253,14 +253,6 @@ async def stop_capture(adapter_number: int, port_number: int, node: VMwareVM = D
await node.stop_capture(adapter_number) await node.stop_capture(adapter_number)
@router.post("/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
responses=responses)
async def reset_console(node: VMwareVM = Depends(dep_node)):
await node.reset_console()
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap", @router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/pcap",
responses=responses) responses=responses)
async def stream_pcap_file(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)): async def stream_pcap_file(adapter_number: int, port_number: int, node: VMwareVM = Depends(dep_node)):
@ -289,16 +281,18 @@ def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
return {"vmnet": vmnet} return {"vmnet": vmnet}
# @Route.get( @router.websocket("/{node_id}/console/ws")
# r"/projects/{project_id}/vmware/nodes/{node_id}/console/ws", async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)):
# description="WebSocket for console", """
# parameters={ Console WebSocket.
# "project_id": "Project UUID", """
# "node_id": "Node UUID",
# }) await node.start_websocket_console(websocket)
# async def console_ws(request, response):
#
# vmware_manager = VMware.instance() @router.post("/{node_id}/console/reset",
# vm = vmware_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) status_code=status.HTTP_204_NO_CONTENT,
# return await vm.start_websocket_console(request) responses=responses)
# async def reset_console(node: VMwareVM = Depends(dep_node)):
await node.reset_console()

View File

@ -21,14 +21,13 @@ API endpoints for VPCS nodes.
import os import os
from fastapi import APIRouter, Depends, Body, status from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
from gns3server.endpoints import schemas from gns3server.endpoints import schemas
from gns3server.compute.vpcs import VPCS from gns3server.compute.vpcs import VPCS
from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.vpcs.vpcs_vm import VPCSVM from gns3server.compute.vpcs.vpcs_vm import VPCSVM
router = APIRouter() router = APIRouter()
@ -48,7 +47,7 @@ def dep_node(project_id: UUID, node_id: UUID):
return node return node
@router.post("/", @router.post("",
response_model=schemas.VPCS, response_model=schemas.VPCS,
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}}) responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}})
@ -258,15 +257,10 @@ async def stream_pcap_file(adapter_number: int, port_number: int, node: VPCSVM =
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap") return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
# @Route.get( @router.websocket("/{node_id}/console/ws")
# r"/projects/{project_id}/vpcs/nodes/{node_id}/console/ws", async def console_ws(websocket: WebSocket, node: VPCSVM = Depends(dep_node)):
# description="WebSocket for console", """
# parameters={ Console WebSocket.
# "project_id": "Project UUID", """
# "node_id": "Node UUID",
# }) await node.start_websocket_console(websocket)
# async def console_ws(request, response):
#
# vpcs_manager = VPCS.instance()
# vm = vpcs_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"])
# return await vm.start_websocket_console(request)

View File

@ -25,7 +25,7 @@ from typing import Optional
router = APIRouter() router = APIRouter()
@router.get("/") @router.get("")
async def get_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic"): async def get_appliances(update: Optional[bool] = None, symbol_theme: Optional[str] = "Classic"):
""" """
Return all appliances known by the controller. Return all appliances known by the controller.

View File

@ -35,7 +35,7 @@ responses = {
} }
@router.post("/", @router.post("",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
response_model=schemas.Compute, response_model=schemas.Compute,
responses={404: {"model": ErrorMessage, "description": "Could not connect to compute"}, responses={404: {"model": ErrorMessage, "description": "Could not connect to compute"},
@ -64,7 +64,7 @@ def get_compute(compute_id: Union[str, UUID]):
return compute.__json__() return compute.__json__()
@router.get("/", @router.get("",
response_model=List[schemas.Compute], response_model=List[schemas.Compute],
response_model_exclude_unset=True) response_model_exclude_unset=True)
async def get_computes(): async def get_computes():

View File

@ -35,7 +35,7 @@ responses = {
} }
@router.get("/", @router.get("",
response_model=List[Drawing], response_model=List[Drawing],
response_model_exclude_unset=True) response_model_exclude_unset=True)
async def get_drawings(project_id: UUID): async def get_drawings(project_id: UUID):
@ -47,7 +47,7 @@ async def get_drawings(project_id: UUID):
return [v.__json__() for v in project.drawings.values()] return [v.__json__() for v in project.drawings.values()]
@router.post("/", @router.post("",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
response_model=Drawing, response_model=Drawing,
responses=responses) responses=responses)

View File

@ -48,8 +48,7 @@ async def get_vms(engine: str):
return vms return vms
@router.get("/", @router.get("", response_model=GNS3VM)
response_model=GNS3VM)
async def get_gns3vm_settings(): async def get_gns3vm_settings():
""" """
Return the GNS3 VM settings. Return the GNS3 VM settings.
@ -58,9 +57,7 @@ async def get_gns3vm_settings():
return Controller.instance().gns3vm.__json__() return Controller.instance().gns3vm.__json__()
@router.put("/", @router.put("", response_model=GNS3VM, response_model_exclude_unset=True)
response_model=GNS3VM,
response_model_exclude_unset=True)
async def update_gns3vm_settings(gns3vm_data: GNS3VM): async def update_gns3vm_settings(gns3vm_data: GNS3VM):
""" """
Update the GNS3 VM settings. Update the GNS3 VM settings.

View File

@ -48,7 +48,7 @@ async def dep_link(project_id: UUID, link_id: UUID):
return link return link
@router.get("/", @router.get("",
response_model=List[schemas.Link], response_model=List[schemas.Link],
response_model_exclude_unset=True) response_model_exclude_unset=True)
async def get_links(project_id: UUID): async def get_links(project_id: UUID):
@ -60,7 +60,7 @@ async def get_links(project_id: UUID):
return [v.__json__() for v in project.links.values()] return [v.__json__() for v in project.links.values()]
@router.post("/", @router.post("",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
response_model=schemas.Link, response_model=schemas.Link,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}, responses={404: {"model": ErrorMessage, "description": "Could not find project"},
@ -214,4 +214,4 @@ async def reset_link(link: Link = Depends(dep_link)):
# break # break
# await proxied_response.write(data) # await proxied_response.write(data)
# #
# #return StreamingResponse(file_like, media_type="video/mp4")) # #return StreamingResponse(file_like, media_type="video/mp4"))

View File

@ -19,9 +19,10 @@
API endpoints for nodes. API endpoints for nodes.
""" """
import aiohttp
import asyncio import asyncio
from fastapi import APIRouter, Depends, Request, Response, status from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from typing import List, Callable from typing import List, Callable
@ -35,6 +36,9 @@ from gns3server.controller.controller_error import ControllerForbiddenError
from gns3server.endpoints.schemas.common import ErrorMessage from gns3server.endpoints.schemas.common import ErrorMessage
from gns3server.endpoints import schemas from gns3server.endpoints import schemas
import logging
log = logging.getLogger(__name__)
node_locks = {} node_locks = {}
@ -97,7 +101,7 @@ async def dep_node(node_id: UUID, project: Project = Depends(dep_project)):
return node return node
@router.post("/", @router.post("",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
response_model=schemas.Node, response_model=schemas.Node,
responses={404: {"model": ErrorMessage, "description": "Could not find project"}, responses={404: {"model": ErrorMessage, "description": "Could not find project"},
@ -117,7 +121,7 @@ async def create_node(node_data: schemas.Node, project: Project = Depends(dep_pr
return node.__json__() return node.__json__()
@router.get("/", @router.get("",
response_model=List[schemas.Node], response_model=List[schemas.Node],
response_model_exclude_unset=True) response_model_exclude_unset=True)
async def get_nodes(project: Project = Depends(dep_project)): async def get_nodes(project: Project = Depends(dep_project)):
@ -367,59 +371,47 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
raw=True) raw=True)
# @Route.get( @router.websocket("/{node_id}/console/ws")
# r"/projects/{project_id}/nodes/{node_id}/console/ws", async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)):
# parameters={ """
# "project_id": "Project UUID", WebSocket console.
# "node_id": "Node UUID" """
# },
# description="Connect to WebSocket console", compute = node.compute
# status_codes={ await websocket.accept()
# 200: "File returned", log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket")
# 403: "Permission denied", ws_console_compute_url = f"ws://{compute.host}:{compute.port}/v2/compute/projects/" \
# 404: "The file doesn't exist" f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/ws"
# })
# async def ws_console(request, response): async def ws_receive(ws_console_compute):
# """
# project = await Controller.instance().get_loaded_project(request.match_info["project_id"]) Receive WebSocket data from client and forward to compute console WebSocket.
# node = project.get_node(request.match_info["node_id"]) """
# compute = node.compute
# ws = aiohttp.web.WebSocketResponse() try:
# await ws.prepare(request) while True:
# request.app['websockets'].add(ws) data = await websocket.receive_text()
# if data:
# ws_console_compute_url = "ws://{compute_host}:{compute_port}/v2/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/ws".format(compute_host=compute.host, await ws_console_compute.send_str(data)
# compute_port=compute.port, except WebSocketDisconnect:
# project_id=project.id, await ws_console_compute.close()
# node_type=node.node_type, log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
# node_id=node.id) f" console WebSocket")
#
# async def ws_forward(ws_client): try:
# async for msg in ws: # receive WebSocket data from compute console WebSocket and forward to client.
# if msg.type == aiohttp.WSMsgType.TEXT: async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=None, force_close=True)) as session:
# await ws_client.send_str(msg.data) async with session.ws_connect(ws_console_compute_url) as ws_console_compute:
# elif msg.type == aiohttp.WSMsgType.BINARY: asyncio.ensure_future(ws_receive(ws_console_compute))
# await ws_client.send_bytes(msg.data) async for msg in ws_console_compute:
# elif msg.type == aiohttp.WSMsgType.ERROR: if msg.type == aiohttp.WSMsgType.TEXT:
# break await websocket.send_text(msg.data)
# elif msg.type == aiohttp.WSMsgType.BINARY:
# try: await websocket.send_bytes(msg.data)
# async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=None, force_close=True)) as session: elif msg.type == aiohttp.WSMsgType.ERROR:
# async with session.ws_connect(ws_console_compute_url) as ws_client: break
# asyncio.ensure_future(ws_forward(ws_client)) except aiohttp.client_exceptions.ClientResponseError as e:
# async for msg in ws_client: log.error(f"Client response error received when forwarding to compute console WebSocket: {e}")
# if msg.type == aiohttp.WSMsgType.TEXT:
# await ws.send_str(msg.data)
# elif msg.type == aiohttp.WSMsgType.BINARY:
# await ws.send_bytes(msg.data)
# elif msg.type == aiohttp.WSMsgType.ERROR:
# break
# finally:
# if not ws.closed:
# await ws.close()
# request.app['websockets'].discard(ws)
#
# return ws
@router.post("/console/reset", @router.post("/console/reset",

View File

@ -19,51 +19,58 @@
API endpoints for controller notifications. API endpoints for controller notifications.
""" """
import asyncio
from fastapi import APIRouter, WebSocket
from fastapi.responses import StreamingResponse
from starlette.endpoints import WebSocketEndpoint
from fastapi import APIRouter, Request, Response, WebSocket, WebSocketDisconnect
from websockets.exceptions import WebSocketException
from gns3server.controller import Controller from gns3server.controller import Controller
router = APIRouter()
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter()
# @router.get("/")
# async def notification(request: Request):
# """
# Receive notifications about the controller from HTTP
# """
#
# controller = Controller.instance()
#
# await response.prepare(request)
# response = Response(content, media_type="application/json")
#
# with controller.notification.controller_queue() as queue:
# while True:
# msg = await queue.get_json(5)
# await response.write(("{}\n".format(msg)).encode("utf-8"))
#
#
# await response(scope, receive, send)
@router.websocket("/ws") @router.get("")
async def notification_ws(websocket: WebSocket): async def http_notification():
""" """
Receive notifications about the controller from a Websocket Receive controller notifications about the controller from HTTP stream.
""" """
controller = Controller.instance() async def event_stream():
await websocket.accept()
log.info("New client has connected to controller WebSocket") with Controller.instance().notification.controller_queue() as queue:
try: while True:
with controller.notification.controller_queue() as queue: msg = await queue.get_json(5)
yield ("{}\n".format(msg)).encode("utf-8")
return StreamingResponse(event_stream(), media_type="application/json")
@router.websocket_route("/ws")
class ControllerWebSocketNotifications(WebSocketEndpoint):
"""
Receive controller notifications about the controller from WebSocket stream.
"""
async def on_connect(self, websocket: WebSocket) -> None:
await websocket.accept()
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
self._notification_task = asyncio.ensure_future(self._stream_notifications(websocket=websocket))
async def on_disconnect(self, websocket: WebSocket, close_code: int) -> None:
self._notification_task.cancel()
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket"
f" with close code {close_code}")
async def _stream_notifications(self, websocket: WebSocket) -> None:
with Controller.instance().notifications.queue() as queue:
while True: while True:
notification = await queue.get_json(5) notification = await queue.get_json(5)
await websocket.send_text(notification) await websocket.send_text(notification)
except (WebSocketException, WebSocketDisconnect):
log.info("Client has disconnected from controller WebSocket")
await websocket.close()

View File

@ -32,7 +32,7 @@ log = logging.getLogger()
from fastapi import APIRouter, Depends, Request, Body, HTTPException, status, WebSocket, WebSocketDisconnect from fastapi import APIRouter, Depends, Request, Body, HTTPException, status, WebSocket, WebSocketDisconnect
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse, FileResponse from fastapi.responses import StreamingResponse, FileResponse
from websockets.exceptions import WebSocketException from websockets.exceptions import ConnectionClosed, WebSocketException
from typing import List from typing import List
from uuid import UUID from uuid import UUID
@ -66,7 +66,19 @@ def dep_project(project_id: UUID):
CHUNK_SIZE = 1024 * 8 # 8KB CHUNK_SIZE = 1024 * 8 # 8KB
@router.post("/", @router.get("",
response_model=List[schemas.Project],
response_model_exclude_unset=True)
def get_projects():
"""
Return all projects.
"""
controller = Controller.instance()
return [p.__json__() for p in controller.projects.values()]
@router.post("",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
response_model=schemas.Project, response_model=schemas.Project,
response_model_exclude_unset=True, response_model_exclude_unset=True,
@ -78,22 +90,9 @@ async def create_project(project_data: schemas.ProjectCreate):
controller = Controller.instance() controller = Controller.instance()
project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True)) project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True))
print(project.__json__()["variables"])
return project.__json__() return project.__json__()
@router.get("/",
response_model=List[schemas.Project],
response_model_exclude_unset=True)
def get_projects():
"""
Return all projects.
"""
controller = Controller.instance()
return [p.__json__() for p in controller.projects.values()]
@router.get("/{project_id}", @router.get("/{project_id}",
response_model=schemas.Project, response_model=schemas.Project,
responses=responses) responses=responses)
@ -193,52 +192,57 @@ async def load_project(path: str = Body(..., embed=True)):
return project.__json__() return project.__json__()
# @router.get("/projects/{project_id}/notifications", @router.get("/{project_id}/notifications")
# summary="Receive notifications about projects", async def notification(project_id: UUID):
# responses={404: {"model": ErrorMessage, "description": "Could not find project"}}) """
# async def notification(project_id: UUID): Receive project notifications about the controller from HTTP stream.
# """
# controller = Controller.instance()
# project = controller.get_project(str(project_id)) controller = Controller.instance()
# #response.content_type = "application/json" project = controller.get_project(str(project_id))
# #response.set_status(200)
# #response.enable_chunked_encoding() log.info("New client has connected to the notification stream for project ID '{}' (HTTP steam method)".format(project.id))
# #await response.prepare(request)
# log.info("New client has connected to the notification stream for project ID '{}' (HTTP long-polling method)".format(project.id)) async def event_stream():
#
# try: try:
# with controller.notification.project_queue(project.id) as queue: with controller.notification.project_queue(project.id) as queue:
# while True: while True:
# msg = await queue.get_json(5) msg = await queue.get_json(5)
# await response.write(("{}\n".format(msg)).encode("utf-8")) yield ("{}\n".format(msg)).encode("utf-8")
# finally: finally:
# log.info("Client has disconnected from notification for project ID '{}' (HTTP long-polling method)".format(project.id)) log.info("Client has disconnected from notification for project ID '{}' (HTTP stream method)".format(project.id))
# if project.auto_close: if project.auto_close:
# # To avoid trouble with client connecting disconnecting we sleep few seconds before checking # To avoid trouble with client connecting disconnecting we sleep few seconds before checking
# # if someone else is not connected # if someone else is not connected
# await asyncio.sleep(5) await asyncio.sleep(5)
# if not controller.notification.project_has_listeners(project.id): if not controller.notification.project_has_listeners(project.id):
# log.info("Project '{}' is automatically closing due to no client listening".format(project.id)) log.info("Project '{}' is automatically closing due to no client listening".format(project.id))
# await project.close() await project.close()
return StreamingResponse(event_stream(), media_type="application/json")
@router.websocket("/{project_id}/notifications/ws") @router.websocket("/{project_id}/notifications/ws")
async def notification_ws(project_id: UUID, websocket: WebSocket): async def notification_ws(project_id: UUID, websocket: WebSocket):
"""
Receive project notifications about the controller from WebSocket.
"""
controller = Controller.instance() controller = Controller.instance()
project = controller.get_project(str(project_id)) project = controller.get_project(str(project_id))
await websocket.accept() await websocket.accept()
#request.app['websockets'].add(ws)
#asyncio.ensure_future(process_websocket(ws))
log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project.id)) log.info("New client has connected to the notification stream for project ID '{}' (WebSocket method)".format(project.id))
try: try:
with controller.notification.project_queue(project.id) as queue: with controller.notification.project_queue(project.id) as queue:
while True: while True:
notification = await queue.get_json(5) notification = await queue.get_json(5)
await websocket.send_text(notification) await websocket.send_text(notification)
except (WebSocketException, WebSocketDisconnect): except (ConnectionClosed, WebSocketDisconnect):
log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id)) log.info("Client has disconnected from notification stream for project ID '{}' (WebSocket method)".format(project.id))
except WebSocketException as e:
log.warning("Error while sending to project event to WebSocket client: '{}'".format(e))
finally: finally:
await websocket.close() await websocket.close()
if project.auto_close: if project.auto_close:

View File

@ -19,7 +19,6 @@
API endpoints for snapshots. API endpoints for snapshots.
""" """
import logging import logging
log = logging.getLogger() log = logging.getLogger()
@ -48,7 +47,7 @@ def dep_project(project_id: UUID):
return project return project
@router.post("/", @router.post("",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
response_model=schemas.Snapshot, response_model=schemas.Snapshot,
responses=responses) responses=responses)
@ -61,7 +60,7 @@ async def create_snapshot(snapshot_data: schemas.SnapshotCreate, project: Projec
return snapshot.__json__() return snapshot.__json__()
@router.get("/", @router.get("",
response_model=List[schemas.Snapshot], response_model=List[schemas.Snapshot],
response_model_exclude_unset=True, response_model_exclude_unset=True,
responses=responses) responses=responses)

View File

@ -35,7 +35,7 @@ log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("/") @router.get("")
def get_symbols(): def get_symbols():
controller = Controller.instance() controller = Controller.instance()

View File

@ -94,9 +94,10 @@ class ProjectUpdate(ProjectBase):
class Project(ProjectBase): class Project(ProjectBase):
project_id: UUID
name: Optional[str] = None name: Optional[str] = None
project_id = UUID
status: Optional[ProjectStatus] = None status: Optional[ProjectStatus] = None
filename: Optional[str] = None
class ProjectFile(BaseModel): class ProjectFile(BaseModel):