From d08c08617c92acc1da88385c3c163ab20600352e Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 27 Mar 2018 16:58:49 +0700 Subject: [PATCH] Support for source and destination for traceNG. --- gns3server/compute/traceng/traceng_vm.py | 63 +++++++------- gns3server/controller/node.py | 4 +- gns3server/controller/project.py | 3 + .../handlers/api/compute/traceng_handler.py | 7 +- .../handlers/api/controller/node_handler.py | 2 +- gns3server/schemas/node.py | 2 +- gns3server/schemas/traceng.py | 35 ++++++-- tests/compute/traceng/test_traceng_vm.py | 82 +++++++++---------- .../handlers/api/compute/test_capabilities.py | 4 +- tests/handlers/api/compute/test_traceng.py | 8 -- 10 files changed, 117 insertions(+), 93 deletions(-) diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py index bd363d2d..b8055d72 100644 --- a/gns3server/compute/traceng/traceng_vm.py +++ b/gns3server/compute/traceng/traceng_vm.py @@ -20,21 +20,17 @@ TraceNG VM management in order to run a TraceNG VM. """ import os -import sys import socket import subprocess -import signal import asyncio import shutil from gns3server.utils.asyncio import wait_for_process_termination from gns3server.utils.asyncio import monitor_process -from gns3server.utils import parse_version from .traceng_error import TraceNGError from ..adapters.ethernet_adapter import EthernetAdapter from ..nios.nio_udp import NIOUDP -from ..nios.nio_tap import NIOTAP from ..base_node import BaseNode @@ -55,12 +51,12 @@ class TraceNGVM(BaseNode): :param console: TCP console port """ - def __init__(self, name, node_id, project, manager, console=None): + def __init__(self, name, node_id, project, manager, console=None, console_type="none"): - super().__init__(name, node_id, project, manager, console=console) + super().__init__(name, node_id, project, manager, console=console, console_type=console_type) self._process = None self._started = False - self._traceng_stdout_file = "" + self._ip_address = None self._local_udp_tunnel = None self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface @@ -115,11 +111,12 @@ class TraceNGVM(BaseNode): def __json__(self): return {"name": self.name, + "ip_address": self.ip_address, "node_id": self.id, "node_directory": self.working_path, "status": self.status, "console": self._console, - "console_type": "telnet", + "console_type": "none", "project_id": self.project.id, "command_line": self.command_line} @@ -137,8 +134,32 @@ class TraceNGVM(BaseNode): return search_path return path + @property + def ip_address(self): + """ + Returns the IP address for this node. + + :returns: IP address + """ + + return self._ip_address + + @ip_address.setter + def ip_address(self, ip_address): + """ + Sets the IP address of this node. + + :param ip_address: IP address + """ + + log.info("{module}: {name} [{id}] set IP address to {ip_address}".format(module=self.manager.module_name, + name=self.name, + id=self.id, + ip_address=ip_address)) + self._ip_address = ip_address + @asyncio.coroutine - def start(self): + def start(self, destination=None): """ Starts the TraceNG process. """ @@ -146,11 +167,10 @@ class TraceNGVM(BaseNode): yield from self._check_requirements() if not self.is_running(): nio = self._ethernet_adapter.get_nio(0) - command = self._build_command() + #TODO: validate destination + command = self._build_command(destination) try: log.info("Starting TraceNG: {}".format(command)) - self._traceng_stdout_file = os.path.join(self.working_dir, "traceng.log") - log.info("Logging to {}".format(self._traceng_stdout_file)) flags = subprocess.CREATE_NEW_CONSOLE self.command_line = ' '.join(command) self._process = yield from asyncio.create_subprocess_exec(*command, @@ -230,21 +250,6 @@ class TraceNGVM(BaseNode): except ProcessLookupError: pass - def read_traceng_stdout(self): - """ - Reads the standard output of the TraceNG process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._traceng_stdout_file: - try: - with open(self._traceng_stdout_file, "rb") as file: - output = file.read().decode("utf-8", errors="replace") - except OSError as e: - log.warning("Could not read {}: {}".format(self._traceng_stdout_file, e)) - return output - def is_running(self): """ Checks if the TraceNG process is running @@ -373,7 +378,7 @@ class TraceNGVM(BaseNode): id=self.id, port_number=port_number)) - def _build_command(self): + def _build_command(self, destination): """ Command to start the TraceNG process. (to be passed to subprocess.Popen()) @@ -393,5 +398,5 @@ class TraceNGVM(BaseNode): except socket.gaierror as e: raise TraceNGError("Can't resolve hostname {}: {}".format(nio.rhost, e)) - #command.extend(["10.0.0.1"]) # TODO: remove + command.extend([destination]) # host or IP to trace return command diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 92dfe316..298f95e5 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -454,7 +454,7 @@ class Node: yield from self.delete() @asyncio.coroutine - def start(self): + def start(self, data=None): """ Start a node """ @@ -467,7 +467,7 @@ class Node: raise aiohttp.web.HTTPConflict(text="IOU licence is not configured") yield from self.post("/start", timeout=240, data={"iourc_content": licence}) else: - yield from self.post("/start", timeout=240) + yield from self.post("/start", data=data, timeout=240) except asyncio.TimeoutError: raise aiohttp.web.HTTPRequestTimeout(text="Timeout when starting {}".format(self._name)) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 46348083..b0b2b5a6 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -905,6 +905,9 @@ class Project: """ pool = Pool(concurrency=3) for node in self.nodes.values(): + if node.node_type == "traceng": + #FIXME: maybe not the right place to do this... + continue pool.append(node.start) yield from pool.join() diff --git a/gns3server/handlers/api/compute/traceng_handler.py b/gns3server/handlers/api/compute/traceng_handler.py index 44ab8809..188cde5f 100644 --- a/gns3server/handlers/api/compute/traceng_handler.py +++ b/gns3server/handlers/api/compute/traceng_handler.py @@ -25,6 +25,7 @@ from gns3server.compute.traceng import TraceNG from gns3server.schemas.traceng import ( TRACENG_CREATE_SCHEMA, TRACENG_UPDATE_SCHEMA, + TRACENG_START_SCHEMA, TRACENG_OBJECT_SCHEMA ) @@ -54,6 +55,7 @@ class TraceNGHandler: request.match_info["project_id"], request.json.get("node_id"), console=request.json.get("console")) + vm.ip_address = request.json.get("ip_address", "") # FIXME, required IP address to create node? response.set_status(201) response.json(vm) @@ -96,7 +98,7 @@ class TraceNGHandler: traceng_manager = TraceNG.instance() vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) vm.name = request.json.get("name", vm.name) - vm.console = request.json.get("console", vm.console) + vm.ip_address = request.json.get("ip_address", vm.ip_address) vm.updated() response.json(vm) @@ -149,12 +151,13 @@ class TraceNGHandler: 404: "Instance doesn't exist" }, description="Start a TraceNG instance", + input=TRACENG_START_SCHEMA, output=TRACENG_OBJECT_SCHEMA) def start(request, response): traceng_manager = TraceNG.instance() vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) - yield from vm.start() + yield from vm.start(request.json["destination"]) response.json(vm) @Route.post( diff --git a/gns3server/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index 808390ab..e02b45f2 100644 --- a/gns3server/handlers/api/controller/node_handler.py +++ b/gns3server/handlers/api/controller/node_handler.py @@ -223,7 +223,7 @@ class NodeHandler: project = yield from Controller.instance().get_loaded_project(request.match_info["project_id"]) node = project.get_node(request.match_info["node_id"]) - yield from node.start() + yield from node.start(data=request.json) response.json(node) response.set_status(201) diff --git a/gns3server/schemas/node.py b/gns3server/schemas/node.py index 2b26192d..1f964519 100644 --- a/gns3server/schemas/node.py +++ b/gns3server/schemas/node.py @@ -145,7 +145,7 @@ NODE_OBJECT_SCHEMA = { }, "console_type": { "description": "Console type", - "enum": ["vnc", "telnet", "http", "https", "spice", None] + "enum": ["vnc", "telnet", "http", "https", "spice", "none", None] }, "properties": { "description": "Properties specific to an emulator", diff --git a/gns3server/schemas/traceng.py b/gns3server/schemas/traceng.py index 38a27de3..6c0f63cc 100644 --- a/gns3server/schemas/traceng.py +++ b/gns3server/schemas/traceng.py @@ -43,8 +43,12 @@ TRACENG_CREATE_SCHEMA = { }, "console_type": { "description": "Console type", - "enum": ["telnet"] + "enum": ["none"] }, + "ip_address": { + "description": "Source IP address for tracing", + "type": ["string"] + } }, "additionalProperties": False, "required": ["name"] @@ -68,12 +72,29 @@ TRACENG_UPDATE_SCHEMA = { }, "console_type": { "description": "Console type", - "enum": ["telnet"] + "enum": ["none"] }, + "ip_address": { + "description": "Source IP address for tracing", + "type": ["string"] + } }, "additionalProperties": False, } +TRACENG_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a TraceNG instance", + "type": "object", + "properties": { + "destination": { + "description": "Host or IP address to trace", + "type": ["string"] + } + }, + "required": ["destination"], +} + TRACENG_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "TraceNG instance", @@ -103,11 +124,11 @@ TRACENG_OBJECT_SCHEMA = { "description": "Console TCP port", "minimum": 1, "maximum": 65535, - "type": "integer" + "type": ["integer", "null"] }, "console_type": { "description": "Console type", - "enum": ["telnet"] + "enum": ["none"] }, "project_id": { "description": "Project UUID", @@ -119,8 +140,12 @@ TRACENG_OBJECT_SCHEMA = { "command_line": { "description": "Last command line used by GNS3 to start TraceNG", "type": "string" + }, + "ip_address": { + "description": "Source IP address for tracing", + "type": ["string"] } }, "additionalProperties": False, - "required": ["name", "node_id", "status", "console", "console_type", "project_id", "command_line"] + "required": ["name", "node_id", "status", "console", "console_type", "project_id", "command_line", "ip_address"] } diff --git a/tests/compute/traceng/test_traceng_vm.py b/tests/compute/traceng/test_traceng_vm.py index f7870368..e56a684a 100644 --- a/tests/compute/traceng/test_traceng_vm.py +++ b/tests/compute/traceng/test_traceng_vm.py @@ -71,17 +71,15 @@ def test_start(loop, vm, async_run): with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process) as mock_exec: - with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.start_wrap_console"): - loop.run_until_complete(asyncio.async(vm.start())) - assert mock_exec.call_args[0] == (vm._traceng_path(), - '-p', - str(vm._internal_console_port), - '-s', - ANY, - '-c', - ANY, - '-t', - '127.0.0.1') + loop.run_until_complete(asyncio.async(vm.start())) + assert mock_exec.call_args[0] == (vm._traceng_path(), + '-c', + ANY, + '-v', + ANY, + '-b', + '127.0.0.1', + '10.0.0.1') assert vm.is_running() assert vm.command_line == ' '.join(mock_exec.call_args[0]) (action, event, kwargs) = async_run(queue.get(0)) @@ -100,29 +98,28 @@ def test_stop(loop, vm, async_run): with NotificationManager.instance().queue() as queue: with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True): - with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.start_wrap_console"): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) - async_run(vm.port_add_nio_binding(0, nio)) + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + async_run(vm.port_add_nio_binding(0, nio)) - async_run(vm.start()) - assert vm.is_running() + async_run(vm.start()) + assert vm.is_running() - with asyncio_patch("gns3server.utils.asyncio.wait_for_process_termination"): - loop.run_until_complete(asyncio.async(vm.stop())) - assert vm.is_running() is False + with asyncio_patch("gns3server.utils.asyncio.wait_for_process_termination"): + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() is False - if sys.platform.startswith("win"): - process.send_signal.assert_called_with(1) - else: - process.terminate.assert_called_with() + if sys.platform.startswith("win"): + process.send_signal.assert_called_with(1) + else: + process.terminate.assert_called_with() - async_run(queue.get(0)) #  Ping - async_run(queue.get(0)) #  Started + async_run(queue.get(0)) #  Ping + async_run(queue.get(0)) #  Started - (action, event, kwargs) = async_run(queue.get(0)) - assert action == "node.updated" - assert event == vm + (action, event, kwargs) = async_run(queue.get(0)) + assert action == "node.updated" + assert event == vm def test_reload(loop, vm, async_run): @@ -135,22 +132,21 @@ def test_reload(loop, vm, async_run): process.returncode = None with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True): - with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.start_wrap_console"): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) - async_run(vm.port_add_nio_binding(0, nio)) - async_run(vm.start()) - assert vm.is_running() + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1", "filters": {}}) + async_run(vm.port_add_nio_binding(0, nio)) + async_run(vm.start()) + assert vm.is_running() - vm._ubridge_send = AsyncioMagicMock() - with asyncio_patch("gns3server.utils.asyncio.wait_for_process_termination"): - async_run(vm.reload()) - assert vm.is_running() is True + vm._ubridge_send = AsyncioMagicMock() + with asyncio_patch("gns3server.utils.asyncio.wait_for_process_termination"): + async_run(vm.reload()) + assert vm.is_running() is True - #if sys.platform.startswith("win"): - # process.send_signal.assert_called_with(1) - #else: - process.terminate.assert_called_with() + #if sys.platform.startswith("win"): + # process.send_signal.assert_called_with(1) + #else: + process.terminate.assert_called_with() def test_add_nio_binding_udp(vm, async_run): diff --git a/tests/handlers/api/compute/test_capabilities.py b/tests/handlers/api/compute/test_capabilities.py index 4f12c6a7..25e1ecd9 100644 --- a/tests/handlers/api/compute/test_capabilities.py +++ b/tests/handlers/api/compute/test_capabilities.py @@ -31,11 +31,11 @@ from gns3server.version import __version__ def test_get(http_compute, windows_platform): response = http_compute.get('/capabilities', example=True) assert response.status == 200 - assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou'], 'version': __version__, 'platform': sys.platform} + assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou', 'traceng'], 'version': __version__, 'platform': sys.platform} @pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows") def test_get_on_gns3vm(http_compute, on_gns3vm): response = http_compute.get('/capabilities', example=True) assert response.status == 200 - assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou'], 'version': __version__, 'platform': sys.platform} + assert response.json == {'node_types': ['cloud', 'ethernet_hub', 'ethernet_switch', 'nat', 'vpcs', 'virtualbox', 'dynamips', 'frame_relay_switch', 'atm_switch', 'qemu', 'vmware', 'docker', 'iou', 'traceng'], 'version': __version__, 'platform': sys.platform} diff --git a/tests/handlers/api/compute/test_traceng.py b/tests/handlers/api/compute/test_traceng.py index 380e8be4..8d20bffe 100644 --- a/tests/handlers/api/compute/test_traceng.py +++ b/tests/handlers/api/compute/test_traceng.py @@ -47,14 +47,6 @@ def test_traceng_get(http_compute, project, vm): assert response.json["status"] == "stopped" -def test_traceng_create_startup_script(http_compute, project): - response = http_compute.post("/projects/{project_id}/traceng/nodes".format(project_id=project.id), {"name": "TraceNG TEST 1", "startup_script": "ip 192.168.1.2\necho TEST"}) - assert response.status == 201 - assert response.route == "/projects/{project_id}/traceng/nodes" - assert response.json["name"] == "TraceNG TEST 1" - assert response.json["project_id"] == project.id - - def test_traceng_create_port(http_compute, project, free_console_port): response = http_compute.post("/projects/{project_id}/traceng/nodes".format(project_id=project.id), {"name": "TraceNG TEST 1", "console": free_console_port}) assert response.status == 201