From 831ee5f46831c669f2e73cd55894d1426f60ef66 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 26 Jul 2020 18:27:18 +0930 Subject: [PATCH] Support to reset all console connections. Ref https://github.com/GNS3/gns3-server/issues/1619 --- gns3server/compute/base_node.py | 8 ++++ gns3server/compute/docker/docker_vm.py | 8 ++++ gns3server/compute/iou/iou_vm.py | 36 ++++++++++++++---- .../compute/virtualbox/virtualbox_vm.py | 8 ++++ gns3server/compute/vmware/vmware_vm.py | 8 ++++ gns3server/controller/node.py | 11 ++++++ gns3server/controller/project.py | 11 ++++++ .../handlers/api/compute/docker_handler.py | 20 ++++++++++ .../api/compute/dynamips_vm_handler.py | 20 ++++++++++ .../handlers/api/compute/iou_handler.py | 20 ++++++++++ .../handlers/api/compute/qemu_handler.py | 20 ++++++++++ .../api/compute/virtualbox_handler.py | 20 ++++++++++ .../handlers/api/compute/vmware_handler.py | 20 ++++++++++ .../handlers/api/compute/vpcs_handler.py | 20 ++++++++++ .../handlers/api/controller/node_handler.py | 37 +++++++++++++++++++ tests/controller/test_project.py | 16 ++++++++ tests/handlers/api/controller/test_node.py | 7 ++++ 17 files changed, 283 insertions(+), 7 deletions(-) diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 452a83d9..275f37fe 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -370,6 +370,14 @@ class BaseNode: self._wrapper_telnet_server.close() await self._wrapper_telnet_server.wait_closed() + async def reset_console(self): + """ + Reset console + """ + + await self.stop_wrap_console() + await self.start_wrap_console() + async def start_websocket_console(self, request): """ Connect to console using Websocket. diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 6b3a644e..0704cccd 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -755,6 +755,14 @@ class DockerVM(BaseNode): break await self.stop() + async def reset_console(self): + """ + Reset the console. + """ + + await self._clean_servers() + await self._start_console() + async def is_running(self): """ Checks if the container is running. diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index e08357ef..617b247e 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -569,17 +569,39 @@ class IOUVM(BaseNode): log.error("Could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) raise IOUError("Could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) - if self.console and self.console_type == "telnet": - server = AsyncioTelnetServer(reader=self._iou_process.stdout, writer=self._iou_process.stdin, binary=True, echo=True) - try: - self._telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console) - except OSError as e: - await self.stop() - raise IOUError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.console, e)) + await self.start_console() # configure networking support await self._networking() + async def start_console(self): + """ + Start the Telnet server to provide console access. + """ + + if self.console and self.console_type == "telnet": + server = AsyncioTelnetServer(reader=self._iou_process.stdout, writer=self._iou_process.stdin, binary=True, + echo=True) + try: + self._telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, + self.console) + except OSError as e: + await self.stop() + raise IOUError( + "Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, + self.console, e)) + + async def reset_console(self): + """ + Reset the console. + """ + + if self._telnet_server: + self._telnet_server.close() + await self._telnet_server.wait_closed() + self._telnet_server = None + await self.start_console() + @locking async def _networking(self): """ diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index a1d00597..79a7bc7d 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -979,6 +979,14 @@ class VirtualBoxVM(BaseNode): self._remote_pipe.close() self._telnet_server = None + async def reset_console(self): + """ + Reset the console. + """ + + await self._stop_remote_console() + await self._start_console() + @BaseNode.console_type.setter def console_type(self, new_console_type): """ diff --git a/gns3server/compute/vmware/vmware_vm.py b/gns3server/compute/vmware/vmware_vm.py index 28b67ca2..709ef004 100644 --- a/gns3server/compute/vmware/vmware_vm.py +++ b/gns3server/compute/vmware/vmware_vm.py @@ -875,6 +875,14 @@ class VMwareVM(BaseNode): self._remote_pipe.close() self._telnet_server = None + async def reset_console(self): + """ + Reset the console. + """ + + await self._stop_remote_console() + await self._start_console() + @BaseNode.console_type.setter def console_type(self, new_console_type): """ diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 8de40aa6..85aea85c 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -536,6 +536,17 @@ class Node: except asyncio.TimeoutError: raise aiohttp.web.HTTPRequestTimeout(text="Timeout when reloading {}".format(self._name)) + async def reset_console(self): + """ + Reset the console + """ + + if self._console and self._console_type == "telnet": + try: + await self.post("/console/reset", timeout=240) + except asyncio.TimeoutError: + raise aiohttp.web.HTTPRequestTimeout(text="Timeout when reset console {}".format(self._name)) + async def post(self, path, data=None, **kwargs): """ HTTP post on the node diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 5a289e15..d7fa3bbc 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -1108,6 +1108,17 @@ class Project: pool.append(node.suspend) await pool.join() + @open_required + async def reset_console_all(self): + """ + Reset console for all nodes + """ + + pool = Pool(concurrency=3) + for node in self.nodes.values(): + pool.append(node.reset_console) + await pool.join() + @open_required async def duplicate_node(self, node, x, y, z): """ diff --git a/gns3server/handlers/api/compute/docker_handler.py b/gns3server/handlers/api/compute/docker_handler.py index b7935071..a56922cd 100644 --- a/gns3server/handlers/api/compute/docker_handler.py +++ b/gns3server/handlers/api/compute/docker_handler.py @@ -428,3 +428,23 @@ class DockerHandler: 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) + + @Route.post( + r"/projects/{project_id}/docker/nodes/{node_id}/console/reset", + description="Reset console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Console has been reset", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Container not started" + }) + async def reset_console(request, response): + + docker_manager = Docker.instance() + container = docker_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + await container.reset_console() + response.set_status(204) diff --git a/gns3server/handlers/api/compute/dynamips_vm_handler.py b/gns3server/handlers/api/compute/dynamips_vm_handler.py index ef64e482..f89f8a0d 100644 --- a/gns3server/handlers/api/compute/dynamips_vm_handler.py +++ b/gns3server/handlers/api/compute/dynamips_vm_handler.py @@ -525,3 +525,23 @@ class DynamipsVMHandler: 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) + + @Route.post( + r"/projects/{project_id}/dynamips/nodes/{node_id}/console/reset", + description="Reset console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Console has been reset", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Container not started" + }) + async def reset_console(request, response): + + dynamips_manager = Dynamips.instance() + vm = dynamips_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + await vm.reset_console() + response.set_status(204) diff --git a/gns3server/handlers/api/compute/iou_handler.py b/gns3server/handlers/api/compute/iou_handler.py index 9a6649fc..4721ac89 100644 --- a/gns3server/handlers/api/compute/iou_handler.py +++ b/gns3server/handlers/api/compute/iou_handler.py @@ -466,3 +466,23 @@ class IOUHandler: 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) + + @Route.post( + r"/projects/{project_id}/iou/nodes/{node_id}/console/reset", + description="Reset console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Console has been reset", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Container not started" + }) + async def reset_console(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + await vm.reset_console() + response.set_status(204) diff --git a/gns3server/handlers/api/compute/qemu_handler.py b/gns3server/handlers/api/compute/qemu_handler.py index 7b2ceb24..eff1b3e6 100644 --- a/gns3server/handlers/api/compute/qemu_handler.py +++ b/gns3server/handlers/api/compute/qemu_handler.py @@ -593,3 +593,23 @@ class QEMUHandler: 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) + + @Route.post( + r"/projects/{project_id}/qemu/nodes/{node_id}/console/reset", + description="Reset console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Console has been reset", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Container not started" + }) + async def reset_console(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + await vm.reset_console() + response.set_status(204) diff --git a/gns3server/handlers/api/compute/virtualbox_handler.py b/gns3server/handlers/api/compute/virtualbox_handler.py index 373d3fbd..762f0f83 100644 --- a/gns3server/handlers/api/compute/virtualbox_handler.py +++ b/gns3server/handlers/api/compute/virtualbox_handler.py @@ -437,3 +437,23 @@ class VirtualBoxHandler: 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) + + @Route.post( + r"/projects/{project_id}/virtualbox/nodes/{node_id}/console/reset", + description="Reset console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Console has been reset", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Container not started" + }) + async def reset_console(request, response): + + virtualbox_manager = VirtualBox.instance() + vm = virtualbox_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + await vm.reset_console() + response.set_status(204) diff --git a/gns3server/handlers/api/compute/vmware_handler.py b/gns3server/handlers/api/compute/vmware_handler.py index 5b92f62f..4e82f194 100644 --- a/gns3server/handlers/api/compute/vmware_handler.py +++ b/gns3server/handlers/api/compute/vmware_handler.py @@ -422,3 +422,23 @@ class VMwareHandler: 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) + + @Route.post( + r"/projects/{project_id}/vmware/nodes/{node_id}/console/reset", + description="Reset console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Console has been reset", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Container not started" + }) + async def reset_console(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + await vm.reset_console() + response.set_status(204) diff --git a/gns3server/handlers/api/compute/vpcs_handler.py b/gns3server/handlers/api/compute/vpcs_handler.py index 63075c8a..2a41df9f 100644 --- a/gns3server/handlers/api/compute/vpcs_handler.py +++ b/gns3server/handlers/api/compute/vpcs_handler.py @@ -375,3 +375,23 @@ class VPCSHandler: 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) + + @Route.post( + r"/projects/{project_id}/vpcs/nodes/{node_id}/console/reset", + description="Reset console", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Console has been reset", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Container not started" + }) + async def reset_console(request, response): + + vpcs_manager = VPCS.instance() + vm = vpcs_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + await vm.reset_console() + response.set_status(204) diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index 51d68e7e..705d2806 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -508,3 +508,40 @@ class NodeHandler: request.app['websockets'].discard(ws) return ws + + @Route.post( + r"/projects/{project_id}/nodes/console/reset", + parameters={ + "project_id": "Project UUID" + }, + status_codes={ + 204: "All nodes successfully reset consoles", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reset console for all nodes belonging to the project", + output=NODE_OBJECT_SCHEMA) + async def reset_console_all(request, response): + + project = await Controller.instance().get_loaded_project(request.match_info["project_id"]) + await project.reset_console_all() + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/nodes/{node_id}/console/reset", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 204: "Console reset", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a node instance") + async def console_reset(request, response): + + project = await Controller.instance().get_loaded_project(request.match_info["project_id"]) + node = project.get_node(request.match_info["node_id"]) + await node.post("/console/reset", request.json) + response.set_status(204) diff --git a/tests/controller/test_project.py b/tests/controller/test_project.py index 109a2351..bc729fee 100644 --- a/tests/controller/test_project.py +++ b/tests/controller/test_project.py @@ -829,6 +829,22 @@ async def test_suspend_all(project): assert len(compute.post.call_args_list) == 10 +async def test_console_reset_all(project): + + compute = MagicMock() + compute.id = "local" + response = MagicMock() + response.json = {"console": 2048, "console_type": "telnet"} + compute.post = AsyncioMagicMock(return_value=response) + + for node_i in range(0, 10): + await project.add_node(compute, "test", None, node_type="vpcs", properties={"startup_config": "test.cfg"}) + + compute.post = AsyncioMagicMock() + await project.reset_console_all() + assert len(compute.post.call_args_list) == 10 + + async def test_node_name(project): compute = MagicMock() diff --git a/tests/handlers/api/controller/test_node.py b/tests/handlers/api/controller/test_node.py index 61c7662e..1ef2b5cf 100644 --- a/tests/handlers/api/controller/test_node.py +++ b/tests/handlers/api/controller/test_node.py @@ -140,6 +140,13 @@ async def test_reload_all_nodes(controller_api, project, compute): assert response.status == 204 +async def test_reset_console_all_nodes(controller_api, project, compute): + + compute.post = AsyncioMagicMock() + response = await controller_api.post("/projects/{}/nodes/console/reset".format(project.id)) + assert response.status == 204 + + async def test_start_node(controller_api, project, node, compute): compute.post = AsyncioMagicMock()