From 12a8831c44e9dc48938a90a7f334ea3953ad5584 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 16 Jan 2020 18:06:51 +0800 Subject: [PATCH 1/6] Change version to 2.3.0dev1 on 2.3 branch --- gns3server/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/version.py b/gns3server/version.py index d683b51c..ca28cccf 100644 --- a/gns3server/version.py +++ b/gns3server/version.py @@ -23,8 +23,8 @@ # or negative for a release candidate or beta (after the base version # number has been incremented) -__version__ = "2.2.6dev1" -__version_info__ = (2, 2, 6, 99) +__version__ = "2.3.0dev1" +__version_info__ = (2, 3, 0, 99) if "dev" in __version__: try: From 941bed9605e44b00553fcbf0d3399caab173c670 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 17 Jan 2020 16:50:17 +0800 Subject: [PATCH 2/6] Server statistics implementation --- .../handlers/api/compute/server_handler.py | 39 +++++++++ .../handlers/api/controller/server_handler.py | 18 ++++ gns3server/schemas/server_statistics.py | 82 +++++++++++++++++++ tests/handlers/api/compute/test_server.py | 5 ++ tests/handlers/api/controller/test_server.py | 5 ++ 5 files changed, 149 insertions(+) create mode 100644 gns3server/schemas/server_statistics.py diff --git a/gns3server/handlers/api/compute/server_handler.py b/gns3server/handlers/api/compute/server_handler.py index 82e69a73..4f043f95 100644 --- a/gns3server/handlers/api/compute/server_handler.py +++ b/gns3server/handlers/api/compute/server_handler.py @@ -21,8 +21,11 @@ import platform from gns3server.web.route import Route from gns3server.config import Config from gns3server.schemas.version import VERSION_SCHEMA +from gns3server.schemas.server_statistics import SERVER_STATISTICS_SCHEMA from gns3server.compute.port_manager import PortManager from gns3server.version import __version__ +from aiohttp.web import HTTPConflict +from psutil._common import bytes2human class ServerHandler: @@ -37,6 +40,42 @@ class ServerHandler: local_server = config.get_section_config("Server").getboolean("local", False) response.json({"version": __version__, "local": local_server}) + @Route.get( + r"/statistics", + description="Retrieve server statistics", + output=SERVER_STATISTICS_SCHEMA, + status_codes={ + 200: "Statistics information returned", + 409: "Conflict" + }) + def statistics(request, response): + + try: + memory_total = psutil.virtual_memory().total + memory_free = psutil.virtual_memory().available + memory_used = memory_total - memory_free # actual memory usage in a cross platform fashion + swap_total = psutil.swap_memory().total + swap_free = psutil.swap_memory().free + swap_used = psutil.swap_memory().used + cpu_percent = int(psutil.cpu_percent()) + load_average_percent = [int(x / psutil.cpu_count() * 100) for x in psutil.getloadavg()] + memory_percent = int(psutil.virtual_memory().percent) + swap_percent = int(psutil.swap_memory().percent) + disk_usage_percent = int(psutil.disk_usage('/').percent) + except psutil.Error as e: + raise HTTPConflict(text="Psutil error detected: {}".format(e)) + response.json({"memory_total": memory_total, + "memory_free": memory_free, + "memory_used": memory_used, + "swap_total": swap_total, + "swap_free": swap_free, + "swap_used": swap_used, + "cpu_usage_percent": cpu_percent, + "memory_usage_percent": memory_percent, + "swap_usage_percent": swap_percent, + "disk_usage_percent": disk_usage_percent, + "load_average_percent": load_average_percent}) + @Route.get( r"/debug", description="Return debug information about the compute", diff --git a/gns3server/handlers/api/controller/server_handler.py b/gns3server/handlers/api/controller/server_handler.py index ae1160c4..a3bcf478 100644 --- a/gns3server/handlers/api/controller/server_handler.py +++ b/gns3server/handlers/api/controller/server_handler.py @@ -130,6 +130,24 @@ class ServerHandler: response.json(iou_license) response.set_status(201) + @Route.get( + r"/statistics", + description="Retrieve server statistics", + status_codes={ + 200: "Statistics information returned", + 409: "Conflict" + }) + async def statistics(request, response): + + compute_statistics = {} + for compute in list(Controller.instance().computes.values()): + try: + r = await compute.get("/statistics") + compute_statistics[compute.name] = r.json + except HTTPConflict as e: + log.error("Could not retrieve statistics on compute {}: {}".format(compute.name, e.text)) + response.json(compute_statistics) + @Route.post( r"/debug", description="Dump debug information to disk (debug directory in config directory). Work only for local server", diff --git a/gns3server/schemas/server_statistics.py b/gns3server/schemas/server_statistics.py new file mode 100644 index 00000000..048799b3 --- /dev/null +++ b/gns3server/schemas/server_statistics.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +SERVER_STATISTICS_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": ["memory_total", + "memory_free", + "memory_used", + "swap_total", + "swap_free", + "swap_used", + "cpu_usage_percent", + "memory_usage_percent", + "swap_usage_percent", + "disk_usage_percent", + "load_average_percent"], + "additionalProperties": False, + "properties": { + "memory_total": { + "description": "Total physical memory (exclusive swap) in bytes", + "type": "integer", + }, + "memory_free": { + "description": "Free memory in bytes", + "type": "integer", + }, + "memory_used": { + "description": "Memory used in bytes", + "type": "integer", + }, + "swap_total": { + "description": "Total swap memory in bytes", + "type": "integer", + }, + "swap_free": { + "description": "Free swap memory in bytes", + "type": "integer", + }, + "swap_used": { + "description": "Swap memory used in bytes", + "type": "integer", + }, + "cpu_usage_percent": { + "description": "CPU usage in percent", + "type": "integer", + }, + "memory_usage_percent": { + "description": "Memory usage in percent", + "type": "integer", + }, + "swap_usage_percent": { + "description": "Swap usage in percent", + "type": "integer", + }, + "disk_usage_percent": { + "description": "Disk usage in percent", + "type": "integer", + }, + "load_average_percent": { + "description": "Average system load over the last 1, 5 and 15 minutes", + "type": "array", + "items": [{"type": "integer"}], + "minItems": 3, + "maxItems": 3 + }, + } +} diff --git a/tests/handlers/api/compute/test_server.py b/tests/handlers/api/compute/test_server.py index e39c2913..c811d907 100644 --- a/tests/handlers/api/compute/test_server.py +++ b/tests/handlers/api/compute/test_server.py @@ -37,3 +37,8 @@ def test_version_output(http_compute): def test_debug_output(http_compute): response = http_compute.get('/debug') assert response.status == 200 + + +def test_statistics_output(http_compute): + response = http_compute.get('/statistics') + assert response.status == 200 diff --git a/tests/handlers/api/controller/test_server.py b/tests/handlers/api/controller/test_server.py index 32c56725..80453f0e 100644 --- a/tests/handlers/api/controller/test_server.py +++ b/tests/handlers/api/controller/test_server.py @@ -70,3 +70,8 @@ def test_debug_non_local(http_controller, config, tmpdir): config.set("Server", "local", False) response = http_controller.post('/debug') assert response.status == 403 + + +def test_statistics_output(http_controller): + response = http_controller.get('/statistics') + assert response.status == 200 From c3b2128faee8c0e7ab94ac0e445b03aea7f7eb51 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 17 Jan 2020 17:07:30 +0800 Subject: [PATCH 3/6] Return array for controller statistics endpoint --- gns3server/handlers/api/controller/server_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/api/controller/server_handler.py b/gns3server/handlers/api/controller/server_handler.py index a3bcf478..a033ac88 100644 --- a/gns3server/handlers/api/controller/server_handler.py +++ b/gns3server/handlers/api/controller/server_handler.py @@ -139,11 +139,11 @@ class ServerHandler: }) async def statistics(request, response): - compute_statistics = {} + compute_statistics = [] for compute in list(Controller.instance().computes.values()): try: r = await compute.get("/statistics") - compute_statistics[compute.name] = r.json + compute_statistics.append({"compute_id": compute.id, "compute_name": compute.name, "statistics": r.json}) except HTTPConflict as e: log.error("Could not retrieve statistics on compute {}: {}".format(compute.name, e.text)) response.json(compute_statistics) From c313475f686e01c1dd33b04eb8278dda38cd8c69 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 31 Jan 2020 17:31:27 +0800 Subject: [PATCH 4/6] Support for WebSocket consoles --- gns3server/compute/base_node.py | 61 ++++++++++++++++++- .../handlers/api/compute/docker_handler.py | 13 ++++ .../api/compute/dynamips_vm_handler.py | 12 ++++ .../handlers/api/compute/iou_handler.py | 13 ++++ .../handlers/api/compute/qemu_handler.py | 13 ++++ .../api/compute/virtualbox_handler.py | 13 ++++ .../handlers/api/compute/vmware_handler.py | 13 ++++ .../handlers/api/compute/vpcs_handler.py | 13 ++++ .../handlers/api/controller/node_handler.py | 55 +++++++++++++++++ gns3server/web/route.py | 5 +- 10 files changed, 207 insertions(+), 4 deletions(-) diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 3fae6e0f..452a83d9 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -27,6 +27,7 @@ import psutil import platform import re +from aiohttp.web import WebSocketResponse from gns3server.utils.interfaces import interfaces from ..compute.port_manager import PortManager from ..utils.asyncio import wait_run_in_executor, locking @@ -339,8 +340,8 @@ class BaseNode: async def start_wrap_console(self): """ - Start a telnet proxy for the console allowing multiple client - connected at the same time + Start a telnet proxy for the console allowing multiple telnet clients + to be connected at the same time """ if not self._wrap_console or self._console_type != "telnet": @@ -369,6 +370,62 @@ class BaseNode: self._wrapper_telnet_server.close() await self._wrapper_telnet_server.wait_closed() + async def start_websocket_console(self, request): + """ + Connect to console using Websocket. + + :param ws: Websocket object + """ + + if self.status != "started": + raise NodeError("Node {} is not started".format(self.name)) + + if self._console_type != "telnet": + raise NodeError("Node {} console type is not telnet".format(self.name)) + + try: + (telnet_reader, telnet_writer) = await asyncio.open_connection(self._manager.port_manager.console_host, self.console) + except ConnectionError as e: + raise NodeError("Cannot connect to node {} telnet server: {}".format(self.name, e)) + + log.info("Connected to Telnet server") + + ws = WebSocketResponse() + await ws.prepare(request) + request.app['websockets'].add(ws) + + log.info("New client has connected to console WebSocket") + + async def ws_forward(telnet_writer): + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + telnet_writer.write(msg.data.encode()) + await telnet_writer.drain() + elif msg.type == aiohttp.WSMsgType.BINARY: + await telnet_writer.write(msg.data) + await telnet_writer.drain() + elif msg.type == aiohttp.WSMsgType.ERROR: + log.debug("Websocket connection closed with exception {}".format(ws.exception())) + + async def telnet_forward(telnet_reader): + + while not ws.closed and not telnet_reader.at_eof(): + data = await telnet_reader.read(1024) + if data: + await ws.send_bytes(data) + + try: + # keep forwarding websocket data in both direction + await asyncio.wait([ws_forward(telnet_writer), telnet_forward(telnet_reader)], return_when=asyncio.FIRST_COMPLETED) + finally: + log.info("Client has disconnected from console WebSocket") + if not ws.closed: + await ws.close() + request.app['websockets'].discard(ws) + + return ws + @property def allocate_aux(self): """ diff --git a/gns3server/handlers/api/compute/docker_handler.py b/gns3server/handlers/api/compute/docker_handler.py index 83a5dd0d..e510a9f2 100644 --- a/gns3server/handlers/api/compute/docker_handler.py +++ b/gns3server/handlers/api/compute/docker_handler.py @@ -412,3 +412,16 @@ class DockerHandler: docker_manager = Docker.instance() images = await docker_manager.list_images() response.json(images) + + @Route.get( + r"/projects/{project_id}/docker/nodes/{node_id}/console/ws", + description="WebSocket for console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }) + async def console_ws(request, response): + + 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) diff --git a/gns3server/handlers/api/compute/dynamips_vm_handler.py b/gns3server/handlers/api/compute/dynamips_vm_handler.py index 88bb1a9d..ef64e482 100644 --- a/gns3server/handlers/api/compute/dynamips_vm_handler.py +++ b/gns3server/handlers/api/compute/dynamips_vm_handler.py @@ -513,3 +513,15 @@ class DynamipsVMHandler: response.set_status(201) response.json(new_node) + @Route.get( + r"/projects/{project_id}/dynamips/nodes/{node_id}/console/ws", + description="WebSocket for console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }) + 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) diff --git a/gns3server/handlers/api/compute/iou_handler.py b/gns3server/handlers/api/compute/iou_handler.py index 43b1c07f..18bbaf02 100644 --- a/gns3server/handlers/api/compute/iou_handler.py +++ b/gns3server/handlers/api/compute/iou_handler.py @@ -452,3 +452,16 @@ class IOUHandler: raise aiohttp.web.HTTPForbidden() await response.stream_file(image_path) + + @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) diff --git a/gns3server/handlers/api/compute/qemu_handler.py b/gns3server/handlers/api/compute/qemu_handler.py index 0c3779cc..7b2ceb24 100644 --- a/gns3server/handlers/api/compute/qemu_handler.py +++ b/gns3server/handlers/api/compute/qemu_handler.py @@ -580,3 +580,16 @@ class QEMUHandler: raise aiohttp.web.HTTPForbidden() await response.stream_file(image_path) + + @Route.get( + r"/projects/{project_id}/qemu/nodes/{node_id}/console/ws", + description="WebSocket for console", + parameters={ + "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) diff --git a/gns3server/handlers/api/compute/virtualbox_handler.py b/gns3server/handlers/api/compute/virtualbox_handler.py index 366778e4..373d3fbd 100644 --- a/gns3server/handlers/api/compute/virtualbox_handler.py +++ b/gns3server/handlers/api/compute/virtualbox_handler.py @@ -424,3 +424,16 @@ class VirtualBoxHandler: vbox_manager = VirtualBox.instance() vms = await vbox_manager.list_vms() response.json(vms) + + @Route.get( + r"/projects/{project_id}/virtualbox/nodes/{node_id}/console/ws", + description="WebSocket for console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }) + async def console_ws(request, response): + + virtualbox_manager = VirtualBox.instance() + vm = virtualbox_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + return await vm.start_websocket_console(request) diff --git a/gns3server/handlers/api/compute/vmware_handler.py b/gns3server/handlers/api/compute/vmware_handler.py index 9c596deb..5b92f62f 100644 --- a/gns3server/handlers/api/compute/vmware_handler.py +++ b/gns3server/handlers/api/compute/vmware_handler.py @@ -409,3 +409,16 @@ class VMwareHandler: vmware_manager = VMware.instance() vms = await vmware_manager.list_vms() response.json(vms) + + @Route.get( + r"/projects/{project_id}/vmware/nodes/{node_id}/console/ws", + description="WebSocket for console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }) + async def console_ws(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + return await vm.start_websocket_console(request) diff --git a/gns3server/handlers/api/compute/vpcs_handler.py b/gns3server/handlers/api/compute/vpcs_handler.py index 51a3ca7e..63075c8a 100644 --- a/gns3server/handlers/api/compute/vpcs_handler.py +++ b/gns3server/handlers/api/compute/vpcs_handler.py @@ -362,3 +362,16 @@ class VPCSHandler: port_number = int(request.match_info["port_number"]) nio = vm.get_nio(port_number) await vpcs_manager.stream_pcap_file(nio, vm.project.id, request, response) + + @Route.get( + r"/projects/{project_id}/vpcs/nodes/{node_id}/console/ws", + description="WebSocket for console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }) + 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) diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index db3ae4a6..51d68e7e 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import aiohttp +import asyncio from gns3server.web.route import Route from gns3server.controller import Controller @@ -453,3 +454,57 @@ class NodeHandler: data = await request.content.read() #FIXME: are we handling timeout or large files correctly? await node.compute.http_query("POST", "/projects/{project_id}/files{path}".format(project_id=project.id, path=path), data=data, timeout=None, raw=True) response.set_status(201) + + @Route.get( + r"/projects/{project_id}/nodes/{node_id}/console/ws", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + description="Connect to WebSocket console", + status_codes={ + 200: "File returned", + 403: "Permission denied", + 404: "The file doesn't exist" + }) + async def ws_console(request, response): + + project = await Controller.instance().get_loaded_project(request.match_info["project_id"]) + node = project.get_node(request.match_info["node_id"]) + compute = node.compute + ws = aiohttp.web.WebSocketResponse() + await ws.prepare(request) + request.app['websockets'].add(ws) + + 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, + compute_port=compute.port, + project_id=project.id, + node_type=node.node_type, + node_id=node.id) + + async def ws_forward(ws_client): + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + await ws_client.send_str(msg.data) + elif msg.type == aiohttp.WSMsgType.BINARY: + await ws_client.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.ERROR: + break + + try: + async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=None, force_close=True)) as session: + async with session.ws_connect(ws_console_compute_url) as ws_client: + asyncio.ensure_future(ws_forward(ws_client)) + async for msg in ws_client: + 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 diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 8efd1bf6..19f62791 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -253,10 +253,11 @@ class Route(object): """ To avoid strange effect we prevent concurrency between the same instance of the node - (excepting when streaming a PCAP file). + (excepting when streaming a PCAP file and WebSocket consoles). """ - if "node_id" in request.match_info and not "pcap" in request.path: + #FIXME: ugly exceptions for capture and websocket console + if "node_id" in request.match_info and not "pcap" in request.path and not "ws" in request.path: node_id = request.match_info.get("node_id") if "compute" in request.path: From 3484a7dd3dad694f8a80e04db3d87a2a4637a1a8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Fri, 31 Jan 2020 18:30:26 +0800 Subject: [PATCH 5/6] Unprotected access for websocket consoles. Ref https://github.com/GNS3/gns3-gui/issues/2883#issuecomment-580677552 --- gns3server/handlers/api/controller/node_handler.py | 1 + gns3server/web/route.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index 51d68e7e..d03029b8 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -469,6 +469,7 @@ class NodeHandler: }) async def ws_console(request, response): + print("HERE!") project = await Controller.instance().get_loaded_project(request.match_info["project_id"]) node = project.get_node(request.match_info["node_id"]) compute = node.compute diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 19f62791..59fb5f8e 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -106,7 +106,8 @@ class Route(object): :returns: Response if you need to auth the user otherwise None """ - if not server_config.getboolean("auth", False): + # FIXME: ugly exception to not require authentication for websocket consoles + if not server_config.getboolean("auth", False) or request.path.endswith("console/ws"): return None user = server_config.get("user", "").strip() @@ -257,7 +258,7 @@ class Route(object): """ #FIXME: ugly exceptions for capture and websocket console - if "node_id" in request.match_info and not "pcap" in request.path and not "ws" in request.path: + if "node_id" in request.match_info and not "pcap" in request.path and not request.path.endswith("console/ws"): node_id = request.match_info.get("node_id") if "compute" in request.path: From 55a5ce77bac06660b7cfe2631711dea66691097a Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 6 Apr 2020 11:51:59 +0930 Subject: [PATCH 6/6] Remove debug message --- gns3server/handlers/api/controller/node_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index d03029b8..51d68e7e 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -469,7 +469,6 @@ class NodeHandler: }) async def ws_console(request, response): - print("HERE!") project = await Controller.instance().get_loaded_project(request.match_info["project_id"]) node = project.get_node(request.match_info["node_id"]) compute = node.compute