From 397c1322b4a3b8c05fea9eff5595d0b73a958e43 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 12 Mar 2018 17:57:13 +0700 Subject: [PATCH 1/8] Base support for TraceNG. --- gns3server/compute/__init__.py | 7 +- gns3server/compute/traceng/__init__.py | 45 ++ gns3server/compute/traceng/traceng_error.py | 27 ++ gns3server/compute/traceng/traceng_vm.py | 410 ++++++++++++++++++ gns3server/controller/__init__.py | 4 + gns3server/controller/link.py | 1 + gns3server/controller/node.py | 4 +- gns3server/handlers/api/compute/__init__.py | 6 +- .../handlers/api/compute/traceng_handler.py | 336 ++++++++++++++ gns3server/run.py | 2 +- gns3server/schemas/node.py | 1 + gns3server/schemas/traceng.py | 126 ++++++ tests/compute/traceng/test_traceng_vm.py | 174 ++++++++ tests/handlers/api/compute/test_traceng.py | 155 +++++++ 14 files changed, 1293 insertions(+), 5 deletions(-) create mode 100644 gns3server/compute/traceng/__init__.py create mode 100644 gns3server/compute/traceng/traceng_error.py create mode 100644 gns3server/compute/traceng/traceng_vm.py create mode 100644 gns3server/handlers/api/compute/traceng_handler.py create mode 100644 gns3server/schemas/traceng.py create mode 100644 tests/compute/traceng/test_traceng_vm.py create mode 100644 tests/handlers/api/compute/test_traceng.py diff --git a/gns3server/compute/__init__.py b/gns3server/compute/__init__.py index 5d454a86..174a35e2 100644 --- a/gns3server/compute/__init__.py +++ b/gns3server/compute/__init__.py @@ -32,7 +32,12 @@ if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.e from .docker import Docker MODULES.append(Docker) - # IOU runs only on Linux but testsuite work on UNIX platform + # IOU only runs on Linux but test suite works on UNIX platform if not sys.platform.startswith("win"): from .iou import IOU MODULES.append(IOU) + + # TODO: TraceNG only runs on Windows but test suite works on UNIX platform + #if sys.platform.startswith("win"): + from .traceng import TraceNG + MODULES.append(TraceNG) diff --git a/gns3server/compute/traceng/__init__.py b/gns3server/compute/traceng/__init__.py new file mode 100644 index 00000000..14b3b1fe --- /dev/null +++ b/gns3server/compute/traceng/__init__.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 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 . + +""" +TraceNG server module. +""" + +import asyncio + +from ..base_manager import BaseManager +from .traceng_error import TraceNGError +from .traceng_vm import TraceNGVM + + +class TraceNG(BaseManager): + + _NODE_CLASS = TraceNGVM + + def __init__(self): + + super().__init__() + + @asyncio.coroutine + def create_node(self, *args, **kwargs): + """ + Creates a new TraceNG VM. + + :returns: TraceNGVM instance + """ + + return (yield from super().create_node(*args, **kwargs)) diff --git a/gns3server/compute/traceng/traceng_error.py b/gns3server/compute/traceng/traceng_error.py new file mode 100644 index 00000000..623f5f59 --- /dev/null +++ b/gns3server/compute/traceng/traceng_error.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 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 . + +""" +Custom exceptions for the TraceNG module. +""" + +from ..error import NodeError + + +class TraceNGError(NodeError): + + pass diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py new file mode 100644 index 00000000..54fac0e4 --- /dev/null +++ b/gns3server/compute/traceng/traceng_vm.py @@ -0,0 +1,410 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 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 . + +""" +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 + + +import logging +log = logging.getLogger(__name__) + + +class TraceNGVM(BaseNode): + module_name = 'traceng' + + """ + TraceNG VM implementation. + + :param name: TraceNG VM name + :param node_id: Node identifier + :param project: Project instance + :param manager: Manager instance + :param console: TCP console port + """ + + def __init__(self, name, node_id, project, manager, console=None): + + super().__init__(name, node_id, project, manager, console=console, wrap_console=True) + self._process = None + self._started = False + self._traceng_stdout_file = "" + self._local_udp_tunnel = None + self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface + + @property + def ethernet_adapter(self): + return self._ethernet_adapter + + @asyncio.coroutine + def close(self): + """ + Closes this TraceNG VM. + """ + + if not (yield from super().close()): + return False + + nio = self._ethernet_adapter.get_nio(0) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) + + if self._local_udp_tunnel: + self.manager.port_manager.release_udp_port(self._local_udp_tunnel[0].lport, self._project) + self.manager.port_manager.release_udp_port(self._local_udp_tunnel[1].lport, self._project) + self._local_udp_tunnel = None + + yield from self._stop_ubridge() + + if self.is_running(): + self._terminate_process() + + return True + + @asyncio.coroutine + def _check_requirements(self): + """ + Check if TraceNG is available. + """ + + path = self._traceng_path() + if not path: + raise TraceNGError("No path to a TraceNG executable has been set") + + # This raise an error if ubridge is not available + self.ubridge_path + + if not os.path.isfile(path): + raise TraceNGError("TraceNG program '{}' is not accessible".format(path)) + + if not os.access(path, os.X_OK): + raise TraceNGError("TraceNG program '{}' is not executable".format(path)) + + def __json__(self): + + return {"name": self.name, + "node_id": self.id, + "node_directory": self.working_path, + "status": self.status, + "console": self._console, + "console_type": "telnet", + "project_id": self.project.id, + "command_line": self.command_line} + + def _traceng_path(self): + """ + Returns the TraceNG executable path. + + :returns: path to TraceNG + """ + + search_path = self._manager.config.get_section_config("TraceNG").get("traceng_path", "traceng") + path = shutil.which(search_path) + # shutil.which return None if the path doesn't exists + if not path: + return search_path + return path + + @asyncio.coroutine + def start(self): + """ + Starts the TraceNG process. + """ + + yield from self._check_requirements() + if not self.is_running(): + nio = self._ethernet_adapter.get_nio(0) + command = self._build_command() + 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 = 0 + #if sys.platform.startswith("win32"): + # flags = subprocess.CREATE_NEW_PROCESS_GROUP + with open(self._traceng_stdout_file, "w", encoding="utf-8") as fd: + self.command_line = ' '.join(command) + self._process = yield from asyncio.create_subprocess_exec(*command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir, + creationflags=flags) + monitor_process(self._process, self._termination_callback) + + yield from self._start_ubridge() + if nio: + yield from self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) + + yield from self.start_wrap_console() + + log.info("TraceNG instance {} started PID={}".format(self.name, self._process.pid)) + self._started = True + self.status = "started" + except (OSError, subprocess.SubprocessError) as e: + traceng_stdout = self.read_traceng_stdout() + log.error("Could not start TraceNG {}: {}\n{}".format(self._traceng_path(), e, traceng_stdout)) + raise TraceNGError("Could not start TraceNG {}: {}\n{}".format(self._traceng_path(), e, traceng_stdout)) + + def _termination_callback(self, returncode): + """ + Called when the process has stopped. + + :param returncode: Process returncode + """ + + if self._started: + log.info("TraceNG process has stopped, return code: %d", returncode) + self._started = False + self.status = "stopped" + self._process = None + if returncode != 0: + self.project.emit("log.error", {"message": "TraceNG process has stopped, return code: {}\n{}".format(returncode, self.read_traceng_stdout())}) + + @asyncio.coroutine + def stop(self): + """ + Stops the TraceNG process. + """ + + yield from self._stop_ubridge() + if self.is_running(): + self._terminate_process() + if self._process.returncode is None: + try: + yield from wait_for_process_termination(self._process, timeout=3) + except asyncio.TimeoutError: + if self._process.returncode is None: + try: + self._process.kill() + except OSError as e: + log.error("Cannot stop the TraceNG process: {}".format(e)) + if self._process.returncode is None: + log.warning('TraceNG VM "{}" with PID={} is still running'.format(self._name, self._process.pid)) + + self._process = None + self._started = False + yield from super().stop() + + @asyncio.coroutine + def reload(self): + """ + Reloads the TraceNG process (stop & start). + """ + + yield from self.stop() + yield from self.start() + + def _terminate_process(self): + """ + Terminate the process if running + """ + + log.info("Stopping TraceNG instance {} PID={}".format(self.name, self._process.pid)) + #if sys.platform.startswith("win32"): + # self._process.send_signal(signal.CTRL_BREAK_EVENT) + #else: + try: + self._process.terminate() + # Sometime the process may already be dead when we garbage collect + 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 + + :returns: True or False + """ + + if self._process and self._process.returncode is None: + return True + return False + + @asyncio.coroutine + def port_add_nio_binding(self, port_number, nio): + """ + Adds a port NIO binding. + + :param port_number: port number + :param nio: NIO instance to add to the slot/port + """ + + if not self._ethernet_adapter.port_exists(port_number): + raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_number=port_number)) + + if self.is_running(): + yield from self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) + + self._ethernet_adapter.add_nio(port_number, nio) + log.info('TraceNG "{name}" [{id}]: {nio} added to port {port_number}'.format(name=self._name, + id=self.id, + nio=nio, + port_number=port_number)) + + return nio + + @asyncio.coroutine + def port_update_nio_binding(self, port_number, nio): + if not self._ethernet_adapter.port_exists(port_number): + raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_number=port_number)) + if self.is_running(): + yield from self.update_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) + + @asyncio.coroutine + def port_remove_nio_binding(self, port_number): + """ + Removes a port NIO binding. + + :param port_number: port number + + :returns: NIO instance + """ + + if not self._ethernet_adapter.port_exists(port_number): + raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_number=port_number)) + + if self.is_running(): + yield from self._ubridge_send("bridge delete {name}".format(name="TraceNG-{}".format(self._id))) + + nio = self._ethernet_adapter.get_nio(port_number) + if isinstance(nio, NIOUDP): + self.manager.port_manager.release_udp_port(nio.lport, self._project) + self._ethernet_adapter.remove_nio(port_number) + + log.info('TraceNG "{name}" [{id}]: {nio} removed from port {port_number}'.format(name=self._name, + id=self.id, + nio=nio, + port_number=port_number)) + return nio + + @asyncio.coroutine + def start_capture(self, port_number, output_file): + """ + Starts a packet capture. + + :param port_number: port number + :param output_file: PCAP destination file for the capture + """ + + if not self._ethernet_adapter.port_exists(port_number): + raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_number=port_number)) + + nio = self._ethernet_adapter.get_nio(0) + + if not nio: + raise TraceNGError("Port {} is not connected".format(port_number)) + + if nio.capturing: + raise TraceNGError("Packet capture is already activated on port {port_number}".format(port_number=port_number)) + + nio.startPacketCapture(output_file) + + if self.ubridge: + yield from self._ubridge_send('bridge start_capture {name} "{output_file}"'.format(name="TraceNG-{}".format(self._id), + output_file=output_file)) + + log.info("TraceNG '{name}' [{id}]: starting packet capture on port {port_number}".format(name=self.name, + id=self.id, + port_number=port_number)) + + @asyncio.coroutine + def stop_capture(self, port_number): + """ + Stops a packet capture. + + :param port_number: port number + """ + + if not self._ethernet_adapter.port_exists(port_number): + raise TraceNGError("Port {port_number} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter, + port_number=port_number)) + + nio = self._ethernet_adapter.get_nio(0) + + if not nio: + raise TraceNGError("Port {} is not connected".format(port_number)) + + nio.stopPacketCapture() + + if self.ubridge: + yield from self._ubridge_send('bridge stop_capture {name}'.format(name="TraceNG-{}".format(self._id))) + + log.info("TraceNG '{name}' [{id}]: stopping packet capture on port {port_number}".format(name=self.name, + id=self.id, + port_number=port_number)) + + def _build_command(self): + """ + Command to start the TraceNG process. + (to be passed to subprocess.Popen()) + """ + + command = [self._traceng_path()] + + #command.extend(["-p", str(self._internal_console_port)]) # listen to console port + + # use the local UDP tunnel to uBridge instead + if not self._local_udp_tunnel: + self._local_udp_tunnel = self._create_local_udp_tunnel() + nio = self._local_udp_tunnel[0] + if nio and isinstance(nio, NIOUDP): + # UDP tunnel + command.extend(["-s", str(nio.lport)]) # source UDP port + command.extend(["-c", str(nio.rport)]) # destination UDP port + try: + command.extend(["-t", socket.gethostbyname(nio.rhost)]) # destination host, we need to resolve the hostname because TraceNG doesn't support it + except socket.gaierror as e: + raise TraceNGError("Can't resolve hostname {}: {}".format(nio.rhost, e)) + + return command diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 5c914f57..60683e00 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -117,6 +117,9 @@ class Controller: for vm in self._settings.get("VPCS", {}).get("nodes", []): vm["node_type"] = "vpcs" vms.append(vm) + for vm in self._settings.get("TraceNG", {}).get("nodes", []): + vm["node_type"] = "traceng" + vms.append(vm) for vm in vms: # remove deprecated properties @@ -151,6 +154,7 @@ class Controller: builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "cloud"), {"node_type": "cloud", "name": "Cloud", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "nat"), {"node_type": "nat", "name": "NAT", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "vpcs"), {"node_type": "vpcs", "name": "VPCS", "default_name_format": "PC-{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {"base_script_file": "vpcs_base_config.txt"}}, builtin=True)) + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"node_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {}}, builtin=True)) # TODO: change default symbol builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_switch"), {"node_type": "ethernet_switch", "name": "Ethernet switch", "category": 1, "symbol": ":/symbols/ethernet_switch.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), {"node_type": "ethernet_hub", "name": "Ethernet hub", "category": 1, "symbol": ":/symbols/hub.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), {"node_type": "frame_relay_switch", "name": "Frame Relay switch", "category": 1, "symbol": ":/symbols/frame_relay_switch.svg"}, builtin=True)) diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index fc3febb4..9f7ac83a 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -414,6 +414,7 @@ class Link: """ for node in self._nodes: if node["node"].node_type in ('vpcs', + 'traceng', 'vmware', 'dynamips', 'qemu', diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index f960a8cf..92dfe316 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -121,7 +121,7 @@ class Node: return self.node_type not in ( "qemu", "docker", "dynamips", "vpcs", "vmware", "virtualbox", - "iou") + "iou", "traceng") @property def id(self): @@ -622,7 +622,7 @@ class Node: for port in self._properties["ports_mapping"]: self._ports.append(PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name="e{}".format(port_number))) port_number += 1 - elif self._node_type in ("vpcs"): + elif self._node_type in ("vpcs", "traceng"): self._ports.append(PortFactory("Ethernet0", 0, 0, 0, "ethernet", short_name="e0")) elif self._node_type in ("cloud", "nat"): port_number = 0 diff --git a/gns3server/handlers/api/compute/__init__.py b/gns3server/handlers/api/compute/__init__.py index e255769a..e8a1f4ba 100644 --- a/gns3server/handlers/api/compute/__init__.py +++ b/gns3server/handlers/api/compute/__init__.py @@ -36,7 +36,11 @@ from .frame_relay_switch_handler import FrameRelaySwitchHandler from .atm_switch_handler import ATMSwitchHandler if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": - # IOU runs only on Linux but test suite works on UNIX platform + # IOU only runs on Linux but test suite works on UNIX platform if not sys.platform.startswith("win"): from .iou_handler import IOUHandler from .docker_handler import DockerHandler + + # TODO: TraceNG only runs on Windows but test suite works on UNIX platform + #if sys.platform.startswith("win"): + from .traceng_handler import TraceNGHandler diff --git a/gns3server/handlers/api/compute/traceng_handler.py b/gns3server/handlers/api/compute/traceng_handler.py new file mode 100644 index 00000000..44ab8809 --- /dev/null +++ b/gns3server/handlers/api/compute/traceng_handler.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 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 . + +import os +from aiohttp.web import HTTPConflict +from gns3server.web.route import Route +from gns3server.schemas.nio import NIO_SCHEMA +from gns3server.schemas.node import NODE_CAPTURE_SCHEMA +from gns3server.compute.traceng import TraceNG + +from gns3server.schemas.traceng import ( + TRACENG_CREATE_SCHEMA, + TRACENG_UPDATE_SCHEMA, + TRACENG_OBJECT_SCHEMA +) + + +class TraceNGHandler: + """ + API entry points for TraceNG. + """ + + @Route.post( + r"/projects/{project_id}/traceng/nodes", + parameters={ + "project_id": "Project UUID" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new TraceNG instance", + input=TRACENG_CREATE_SCHEMA, + output=TRACENG_OBJECT_SCHEMA) + def create(request, response): + + traceng = TraceNG.instance() + vm = yield from traceng.create_node(request.json["name"], + request.match_info["project_id"], + request.json.get("node_id"), + console=request.json.get("console")) + response.set_status(201) + response.json(vm) + + @Route.get( + r"/projects/{project_id}/traceng/nodes/{node_id}", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a TraceNG instance", + output=TRACENG_OBJECT_SCHEMA) + def show(request, response): + + traceng_manager = TraceNG.instance() + vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @Route.put( + r"/projects/{project_id}/traceng/nodes/{node_id}", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a TraceNG instance", + input=TRACENG_UPDATE_SCHEMA, + output=TRACENG_OBJECT_SCHEMA) + def update(request, response): + + 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.updated() + response.json(vm) + + @Route.delete( + r"/projects/{project_id}/traceng/nodes/{node_id}", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a TraceNG instance") + def delete(request, response): + + yield from TraceNG.instance().delete_node(request.match_info["node_id"]) + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/duplicate", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 201: "Instance duplicated", + 404: "Instance doesn't exist" + }, + description="Duplicate a TraceNG instance") + def duplicate(request, response): + + new_node = yield from TraceNG.instance().duplicate_node( + request.match_info["node_id"], + request.json["destination_node_id"] + ) + response.set_status(201) + response.json(new_node) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/start", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a TraceNG instance", + 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() + response.json(vm) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/stop", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a TraceNG instance") + def stop(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.stop() + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/suspend", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID" + }, + status_codes={ + 204: "Instance suspended", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Suspend a TraceNG instance (does nothing)") + def suspend(request, response): + + traceng_manager = TraceNG.instance() + traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/reload", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a TraceNG instance") + def reload(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.reload() + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port where the nio should be added" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a TraceNG instance", + input=NIO_SCHEMA, + output=NIO_SCHEMA) + def create_nio(request, response): + + traceng_manager = TraceNG.instance() + vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + nio_type = request.json["type"] + if nio_type not in ("nio_udp"): + raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type)) + nio = traceng_manager.create_nio(request.json) + yield from vm.port_add_nio_binding(int(request.match_info["port_number"]), nio) + response.set_status(201) + response.json(nio) + + @Route.put( + r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port from where the nio should be updated" + }, + status_codes={ + 201: "NIO updated", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + input=NIO_SCHEMA, + output=NIO_SCHEMA, + description="Update a NIO from a TraceNG instance") + def update_nio(request, response): + + traceng_manager = TraceNG.instance() + vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + nio = vm.ethernet_adapter.get_nio(int(request.match_info["port_number"])) + if "filters" in request.json and nio: + nio.filters = request.json["filters"] + yield from vm.port_update_nio_binding(int(request.match_info["port_number"]), nio) + response.set_status(201) + response.json(request.json) + + @Route.delete( + r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port from where the nio should be removed" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a TraceNG instance") + def delete_nio(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.port_remove_nio_binding(int(request.match_info["port_number"])) + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/start_capture", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + "adapter_number": "Adapter to start a packet capture", + "port_number": "Port on the adapter" + }, + status_codes={ + 200: "Capture started", + 400: "Invalid request", + 404: "Instance doesn't exist", + }, + description="Start a packet capture on a TraceNG instance", + input=NODE_CAPTURE_SCHEMA) + def start_capture(request, response): + + traceng_manager = TraceNG.instance() + vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + port_number = int(request.match_info["port_number"]) + pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) + yield from vm.start_capture(port_number, pcap_file_path) + response.json({"pcap_file_path": pcap_file_path}) + + @Route.post( + r"/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/stop_capture", + parameters={ + "project_id": "Project UUID", + "node_id": "Node UUID", + "adapter_number": "Adapter to stop a packet capture", + "port_number": "Port on the adapter" + }, + status_codes={ + 204: "Capture stopped", + 400: "Invalid request", + 404: "Instance doesn't exist", + }, + description="Stop a packet capture on a TraceNG instance") + def stop_capture(request, response): + + traceng_manager = TraceNG.instance() + vm = traceng_manager.get_node(request.match_info["node_id"], project_id=request.match_info["project_id"]) + port_number = int(request.match_info["port_number"]) + yield from vm.stop_capture(port_number) + response.set_status(204) diff --git a/gns3server/run.py b/gns3server/run.py index 0d23b124..c3b4bc27 100644 --- a/gns3server/run.py +++ b/gns3server/run.py @@ -183,7 +183,7 @@ def kill_ghosts(): """ Kill process from previous GNS3 session """ - detect_process = ["vpcs", "ubridge", "dynamips"] + detect_process = ["vpcs", "traceng", "ubridge", "dynamips"] for proc in psutil.process_iter(): try: name = proc.name().lower().split(".")[0] diff --git a/gns3server/schemas/node.py b/gns3server/schemas/node.py index d39ba747..2b26192d 100644 --- a/gns3server/schemas/node.py +++ b/gns3server/schemas/node.py @@ -30,6 +30,7 @@ NODE_TYPE_SCHEMA = { "docker", "dynamips", "vpcs", + "traceng", "virtualbox", "vmware", "iou", diff --git a/gns3server/schemas/traceng.py b/gns3server/schemas/traceng.py new file mode 100644 index 00000000..38a27de3 --- /dev/null +++ b/gns3server/schemas/traceng.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 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 . + + +TRACENG_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new TraceNG instance", + "type": "object", + "properties": { + "name": { + "description": "TraceNG VM name", + "type": "string", + "minLength": 1, + }, + "node_id": { + "description": "Node UUID", + "oneOf": [ + {"type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"} + ] + }, + "console": { + "description": "Console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, + }, + "additionalProperties": False, + "required": ["name"] +} + +TRACENG_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a TraceNG instance", + "type": "object", + "properties": { + "name": { + "description": "TraceNG VM name", + "type": ["string", "null"], + "minLength": 1, + }, + "console": { + "description": "Console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, + }, + "additionalProperties": False, +} + +TRACENG_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "TraceNG instance", + "type": "object", + "properties": { + "name": { + "description": "TraceNG VM name", + "type": "string", + "minLength": 1, + }, + "node_id": { + "description": "Node UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "node_directory": { + "description": "Path to the VM working directory", + "type": "string" + }, + "status": { + "description": "VM status", + "enum": ["started", "stopped", "suspended"] + }, + "console": { + "description": "Console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, + "project_id": { + "description": "Project UUID", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "command_line": { + "description": "Last command line used by GNS3 to start TraceNG", + "type": "string" + } + }, + "additionalProperties": False, + "required": ["name", "node_id", "status", "console", "console_type", "project_id", "command_line"] +} diff --git a/tests/compute/traceng/test_traceng_vm.py b/tests/compute/traceng/test_traceng_vm.py new file mode 100644 index 00000000..f7870368 --- /dev/null +++ b/tests/compute/traceng/test_traceng_vm.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 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 . + +import pytest +import asyncio +import os +import sys + +from tests.utils import asyncio_patch, AsyncioMagicMock +from gns3server.utils import parse_version +from unittest.mock import patch, MagicMock, ANY + +from gns3server.compute.traceng.traceng_vm import TraceNGVM +from gns3server.compute.traceng.traceng_error import TraceNGError +from gns3server.compute.traceng import TraceNG +from gns3server.compute.notification_manager import NotificationManager + + +@pytest.fixture +def manager(port_manager): + m = TraceNG.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def vm(project, manager, ubridge_path): + vm = TraceNGVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + vm._start_ubridge = AsyncioMagicMock() + vm._ubridge_hypervisor = MagicMock() + vm._ubridge_hypervisor.is_running.return_value = True + return vm + + +def test_vm(project, manager): + vm = TraceNGVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + assert vm.name == "test" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + + +def test_vm_invalid_traceng_path(vm, manager, loop): + with patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._traceng_path", return_value="/tmp/fake/path/traceng"): + with pytest.raises(TraceNGError): + nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.name == "test" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0e" + + +def test_start(loop, vm, async_run): + process = MagicMock() + process.returncode = None + + with NotificationManager.instance().queue() as queue: + async_run(queue.get(0)) # Ping + + 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') + assert vm.is_running() + assert vm.command_line == ' '.join(mock_exec.call_args[0]) + (action, event, kwargs) = async_run(queue.get(0)) + assert action == "node.updated" + assert event == vm + + +def test_stop(loop, vm, async_run): + process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + process.returncode = None + + 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)) + + 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 + + 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 + + (action, event, kwargs) = async_run(queue.get(0)) + assert action == "node.updated" + assert event == vm + + +def test_reload(loop, vm, async_run): + process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + 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() + + 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() + + +def test_add_nio_binding_udp(vm, async_run): + 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)) + assert nio.lport == 4242 + + +def test_port_remove_nio_binding(vm): + nio = TraceNG.instance().create_nio({"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) + vm.port_add_nio_binding(0, nio) + vm.port_remove_nio_binding(0) + assert vm._ethernet_adapter.ports[0] is None + + +def test_close(vm, port_manager, loop): + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + vm.start() + loop.run_until_complete(asyncio.async(vm.close())) + assert vm.is_running() is False diff --git a/tests/handlers/api/compute/test_traceng.py b/tests/handlers/api/compute/test_traceng.py new file mode 100644 index 00000000..380e8be4 --- /dev/null +++ b/tests/handlers/api/compute/test_traceng.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 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 . + +import pytest +import uuid +import sys +import os +from tests.utils import asyncio_patch +from unittest.mock import patch + + +@pytest.fixture(scope="function") +def vm(http_compute, project): + response = http_compute.post("/projects/{project_id}/traceng/nodes".format(project_id=project.id), {"name": "TraceNG TEST 1"}) + assert response.status == 201 + return response.json + + +def test_traceng_create(http_compute, project): + response = http_compute.post("/projects/{project_id}/traceng/nodes".format(project_id=project.id), {"name": "TraceNG TEST 1"}, example=True) + 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_get(http_compute, project, vm): + response = http_compute.get("/projects/{project_id}/traceng/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True) + assert response.status == 200 + assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}" + assert response.json["name"] == "TraceNG TEST 1" + assert response.json["project_id"] == project.id + 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 + assert response.route == "/projects/{project_id}/traceng/nodes" + assert response.json["name"] == "TraceNG TEST 1" + assert response.json["project_id"] == project.id + assert response.json["console"] == free_console_port + + +def test_traceng_nio_create_udp(http_compute, vm): + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.add_ubridge_udp_connection"): + response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_udp" + + +def test_traceng_nio_update_udp(http_compute, vm): + response = http_compute.put("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), + { + "type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1", + "filters": {}}, + example=True) + assert response.status == 201, response.body.decode("utf-8") + assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_udp" + + +def test_traceng_delete_nio(http_compute, vm): + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._ubridge_send"): + http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = http_compute.delete("/projects/{project_id}/traceng/nodes/{node_id}/adapters/0/ports/0/nio".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True) + assert response.status == 204, response.body.decode() + assert response.route == "/projects/{project_id}/traceng/nodes/{node_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + + +def test_traceng_start(http_compute, vm): + + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.start", return_value=True) as mock: + response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/start".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True) + assert mock.called + assert response.status == 200 + assert response.json["name"] == "TraceNG TEST 1" + + +def test_traceng_stop(http_compute, vm): + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.stop", return_value=True) as mock: + response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/stop".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True) + assert mock.called + assert response.status == 204 + + +def test_traceng_reload(http_compute, vm): + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM.reload", return_value=True) as mock: + response = http_compute.post("/projects/{project_id}/traceng/nodes/{node_id}/reload".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True) + assert mock.called + assert response.status == 204 + + +def test_traceng_delete(http_compute, vm): + with asyncio_patch("gns3server.compute.traceng.TraceNG.delete_node", return_value=True) as mock: + response = http_compute.delete("/projects/{project_id}/traceng/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), example=True) + assert mock.called + assert response.status == 204 + + +def test_traceng_duplicate(http_compute, vm): + with asyncio_patch("gns3server.compute.traceng.TraceNG.duplicate_node", return_value=True) as mock: + response = http_compute.post( + "/projects/{project_id}/traceng/nodes/{node_id}/duplicate".format( + project_id=vm["project_id"], + node_id=vm["node_id"]), + body={ + "destination_node_id": str(uuid.uuid4()) + }, + example=True) + assert mock.called + assert response.status == 201 + + +def test_traceng_update(http_compute, vm, tmpdir, free_console_port): + response = http_compute.put("/projects/{project_id}/traceng/nodes/{node_id}".format(project_id=vm["project_id"], node_id=vm["node_id"]), {"name": "test", + "console": free_console_port, + }, + example=True) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port From a4626a2b79819cf9312a6f3a6f15931245722dfd Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 14 Mar 2018 16:56:37 +0700 Subject: [PATCH 2/8] Custom icons and small fixes for TraceNG integration. --- gns3server/compute/__init__.py | 16 +- gns3server/compute/traceng/traceng_vm.py | 6 +- gns3server/controller/__init__.py | 2 +- gns3server/handlers/api/compute/__init__.py | 8 +- gns3server/symbols/traceng.svg | 610 ++++++++++++++++++++ 5 files changed, 627 insertions(+), 15 deletions(-) create mode 100644 gns3server/symbols/traceng.svg diff --git a/gns3server/compute/__init__.py b/gns3server/compute/__init__.py index 174a35e2..65ba86ae 100644 --- a/gns3server/compute/__init__.py +++ b/gns3server/compute/__init__.py @@ -28,16 +28,14 @@ from .vmware import VMware MODULES = [Builtin, VPCS, VirtualBox, Dynamips, Qemu, VMware] if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": - - from .docker import Docker - MODULES.append(Docker) - - # IOU only runs on Linux but test suite works on UNIX platform + # IOU & Docker only runs on Linux but test suite works on UNIX platform if not sys.platform.startswith("win"): + from .docker import Docker + MODULES.append(Docker) from .iou import IOU MODULES.append(IOU) - # TODO: TraceNG only runs on Windows but test suite works on UNIX platform - #if sys.platform.startswith("win"): - from .traceng import TraceNG - MODULES.append(TraceNG) +#if sys.platform.startswith("win") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": +# FIXME: TraceNG only runs on Windows but test suite works on UNIX platform +from .traceng import TraceNG +MODULES.append(TraceNG) diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py index 54fac0e4..7289394c 100644 --- a/gns3server/compute/traceng/traceng_vm.py +++ b/gns3server/compute/traceng/traceng_vm.py @@ -392,7 +392,11 @@ class TraceNGVM(BaseNode): command = [self._traceng_path()] - #command.extend(["-p", str(self._internal_console_port)]) # listen to console port + # TODO: remove when testing with executable + command.extend(["-p", str(self._internal_console_port)]) # listen to console port + command.extend(["-m", "1"]) # the unique ID is used to set the MAC address offset + command.extend(["-i", "1"]) # option to start only one VPC instance + command.extend(["-F"]) # option to avoid the daemonization of VPCS # use the local UDP tunnel to uBridge instead if not self._local_udp_tunnel: diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index 60683e00..e3091374 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -154,7 +154,7 @@ class Controller: builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "cloud"), {"node_type": "cloud", "name": "Cloud", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "nat"), {"node_type": "nat", "name": "NAT", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "vpcs"), {"node_type": "vpcs", "name": "VPCS", "default_name_format": "PC-{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {"base_script_file": "vpcs_base_config.txt"}}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"node_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {}}, builtin=True)) # TODO: change default symbol + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"node_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/traceng.svg", "properties": {}}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_switch"), {"node_type": "ethernet_switch", "name": "Ethernet switch", "category": 1, "symbol": ":/symbols/ethernet_switch.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), {"node_type": "ethernet_hub", "name": "Ethernet hub", "category": 1, "symbol": ":/symbols/hub.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), {"node_type": "frame_relay_switch", "name": "Frame Relay switch", "category": 1, "symbol": ":/symbols/frame_relay_switch.svg"}, builtin=True)) diff --git a/gns3server/handlers/api/compute/__init__.py b/gns3server/handlers/api/compute/__init__.py index e8a1f4ba..e742393e 100644 --- a/gns3server/handlers/api/compute/__init__.py +++ b/gns3server/handlers/api/compute/__init__.py @@ -36,11 +36,11 @@ from .frame_relay_switch_handler import FrameRelaySwitchHandler from .atm_switch_handler import ATMSwitchHandler if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": - # IOU only runs on Linux but test suite works on UNIX platform + # IOU & Docker only runs on Linux but test suite works on UNIX platform if not sys.platform.startswith("win"): from .iou_handler import IOUHandler from .docker_handler import DockerHandler - # TODO: TraceNG only runs on Windows but test suite works on UNIX platform - #if sys.platform.startswith("win"): - from .traceng_handler import TraceNGHandler +#if sys.platform.startswith("win") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": +# FIXME: TraceNG only runs on Windows but test suite works on UNIX platform +from .traceng_handler import TraceNGHandler diff --git a/gns3server/symbols/traceng.svg b/gns3server/symbols/traceng.svg new file mode 100644 index 00000000..5f4474a0 --- /dev/null +++ b/gns3server/symbols/traceng.svg @@ -0,0 +1,610 @@ + + + + + + + + + + + + hash + + hardware + computer + + + + + Andy Fitzsimon + + + + + Andy Fitzsimon + + + + + Andy Fitzsimon + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8e695c8af10d0536343f9e2ebb7d94c3e68cbcb7 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 27 Mar 2018 01:19:24 -0700 Subject: [PATCH 3/8] Have TraceNG start without needing cmd.exe --- gns3server/compute/traceng/traceng_vm.py | 39 +++++++----------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py index 7289394c..bd363d2d 100644 --- a/gns3server/compute/traceng/traceng_vm.py +++ b/gns3server/compute/traceng/traceng_vm.py @@ -57,7 +57,7 @@ class TraceNGVM(BaseNode): def __init__(self, name, node_id, project, manager, console=None): - super().__init__(name, node_id, project, manager, console=console, wrap_console=True) + super().__init__(name, node_id, project, manager, console=console) self._process = None self._started = False self._traceng_stdout_file = "" @@ -151,24 +151,16 @@ class TraceNGVM(BaseNode): 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 = 0 - #if sys.platform.startswith("win32"): - # flags = subprocess.CREATE_NEW_PROCESS_GROUP - with open(self._traceng_stdout_file, "w", encoding="utf-8") as fd: - self.command_line = ' '.join(command) - self._process = yield from asyncio.create_subprocess_exec(*command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir, - creationflags=flags) - monitor_process(self._process, self._termination_callback) - + flags = subprocess.CREATE_NEW_CONSOLE + self.command_line = ' '.join(command) + self._process = yield from asyncio.create_subprocess_exec(*command, + cwd=self.working_dir, + creationflags=flags) + monitor_process(self._process, self._termination_callback) yield from self._start_ubridge() if nio: yield from self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) - yield from self.start_wrap_console() - log.info("TraceNG instance {} started PID={}".format(self.name, self._process.pid)) self._started = True self.status = "started" @@ -232,9 +224,6 @@ class TraceNGVM(BaseNode): """ log.info("Stopping TraceNG instance {} PID={}".format(self.name, self._process.pid)) - #if sys.platform.startswith("win32"): - # self._process.send_signal(signal.CTRL_BREAK_EVENT) - #else: try: self._process.terminate() # Sometime the process may already be dead when we garbage collect @@ -391,24 +380,18 @@ class TraceNGVM(BaseNode): """ command = [self._traceng_path()] - - # TODO: remove when testing with executable - command.extend(["-p", str(self._internal_console_port)]) # listen to console port - command.extend(["-m", "1"]) # the unique ID is used to set the MAC address offset - command.extend(["-i", "1"]) # option to start only one VPC instance - command.extend(["-F"]) # option to avoid the daemonization of VPCS - # use the local UDP tunnel to uBridge instead if not self._local_udp_tunnel: self._local_udp_tunnel = self._create_local_udp_tunnel() nio = self._local_udp_tunnel[0] if nio and isinstance(nio, NIOUDP): # UDP tunnel - command.extend(["-s", str(nio.lport)]) # source UDP port - command.extend(["-c", str(nio.rport)]) # destination UDP port + command.extend(["-c", str(nio.lport)]) # source UDP port + command.extend(["-v", str(nio.rport)]) # destination UDP port try: - command.extend(["-t", socket.gethostbyname(nio.rhost)]) # destination host, we need to resolve the hostname because TraceNG doesn't support it + command.extend(["-b", socket.gethostbyname(nio.rhost)]) # destination host, we need to resolve the hostname because TraceNG doesn't support it except socket.gaierror as e: raise TraceNGError("Can't resolve hostname {}: {}".format(nio.rhost, e)) + #command.extend(["10.0.0.1"]) # TODO: remove return command From d08c08617c92acc1da88385c3c163ab20600352e Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 27 Mar 2018 16:58:49 +0700 Subject: [PATCH 4/8] 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 From 05ec14e8884c1bc5b048522ef8c7736033122426 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 27 Mar 2018 03:43:50 -0700 Subject: [PATCH 5/8] Use the configured IP address to trace. --- gns3server/compute/traceng/traceng_vm.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py index b8055d72..39b5bd50 100644 --- a/gns3server/compute/traceng/traceng_vm.py +++ b/gns3server/compute/traceng/traceng_vm.py @@ -167,7 +167,6 @@ class TraceNGVM(BaseNode): yield from self._check_requirements() if not self.is_running(): nio = self._ethernet_adapter.get_nio(0) - #TODO: validate destination command = self._build_command(destination) try: log.info("Starting TraceNG: {}".format(command)) @@ -185,9 +184,8 @@ class TraceNGVM(BaseNode): self._started = True self.status = "started" except (OSError, subprocess.SubprocessError) as e: - traceng_stdout = self.read_traceng_stdout() - log.error("Could not start TraceNG {}: {}\n{}".format(self._traceng_path(), e, traceng_stdout)) - raise TraceNGError("Could not start TraceNG {}: {}\n{}".format(self._traceng_path(), e, traceng_stdout)) + log.error("Could not start TraceNG {}: {}\n".format(self._traceng_path(), e)) + raise TraceNGError("Could not start TraceNG {}: {}\n".format(self._traceng_path(), e)) def _termination_callback(self, returncode): """ @@ -202,7 +200,7 @@ class TraceNGVM(BaseNode): self.status = "stopped" self._process = None if returncode != 0: - self.project.emit("log.error", {"message": "TraceNG process has stopped, return code: {}\n{}".format(returncode, self.read_traceng_stdout())}) + self.project.emit("log.error", {"message": "TraceNG process has stopped, return code: {}\n".format(returncode)}) @asyncio.coroutine def stop(self): @@ -384,6 +382,12 @@ class TraceNGVM(BaseNode): (to be passed to subprocess.Popen()) """ + # TODO: better validation + if not destination: + raise TraceNGError("Please provide a destination to trace") + if not self._ip_address: + raise TraceNGError("Please provide an IP address for this TraceNG node") + command = [self._traceng_path()] # use the local UDP tunnel to uBridge instead if not self._local_udp_tunnel: @@ -398,5 +402,6 @@ class TraceNGVM(BaseNode): except socket.gaierror as e: raise TraceNGError("Can't resolve hostname {}: {}".format(nio.rhost, e)) - command.extend([destination]) # host or IP to trace + command.extend(["-f", self._ip_address]) # source IP address to trace from + command.extend([destination]) # host or IP to trace return command From c053a2f35007f662fdfbab44c8cfd9c48701e882 Mon Sep 17 00:00:00 2001 From: grossmj Date: Wed, 28 Mar 2018 04:26:40 -0700 Subject: [PATCH 6/8] Enable UDP tunnel option and use ICMP probing by default. --- gns3server/compute/traceng/traceng_vm.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py index 39b5bd50..b9afd71c 100644 --- a/gns3server/compute/traceng/traceng_vm.py +++ b/gns3server/compute/traceng/traceng_vm.py @@ -168,6 +168,7 @@ class TraceNGVM(BaseNode): if not self.is_running(): nio = self._ethernet_adapter.get_nio(0) command = self._build_command(destination) + yield from self._stop_ubridge() # make use we start with a fresh uBridge instance try: log.info("Starting TraceNG: {}".format(command)) flags = subprocess.CREATE_NEW_CONSOLE @@ -176,6 +177,7 @@ class TraceNGVM(BaseNode): cwd=self.working_dir, creationflags=flags) monitor_process(self._process, self._termination_callback) + yield from self._start_ubridge() if nio: yield from self.add_ubridge_udp_connection("TraceNG-{}".format(self._id), self._local_udp_tunnel[1], nio) @@ -242,6 +244,9 @@ class TraceNGVM(BaseNode): """ log.info("Stopping TraceNG instance {} PID={}".format(self.name, self._process.pid)) + #if sys.platform.startswith("win32"): + # self._process.send_signal(signal.CTRL_BREAK_EVENT) + #else: try: self._process.terminate() # Sometime the process may already be dead when we garbage collect @@ -395,6 +400,7 @@ class TraceNGVM(BaseNode): nio = self._local_udp_tunnel[0] if nio and isinstance(nio, NIOUDP): # UDP tunnel + command.extend(["-u"]) # enable UDP tunnel command.extend(["-c", str(nio.lport)]) # source UDP port command.extend(["-v", str(nio.rport)]) # destination UDP port try: @@ -402,6 +408,7 @@ class TraceNGVM(BaseNode): except socket.gaierror as e: raise TraceNGError("Can't resolve hostname {}: {}".format(nio.rhost, e)) + command.extend(["-s", "ICMP"]) # Use ICMP probe type by default command.extend(["-f", self._ip_address]) # source IP address to trace from command.extend([destination]) # host or IP to trace return command From b7780ba7cc1b6a8d2424005b826dfeb22e17e300 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 29 Mar 2018 13:26:43 +0700 Subject: [PATCH 7/8] Check for valid IP address and prevent to run on non-Windows platforms. --- gns3server/compute/__init__.py | 8 ++------ gns3server/compute/traceng/traceng_vm.py | 17 +++++++++++++---- gns3server/controller/__init__.py | 3 ++- gns3server/controller/project.py | 2 +- gns3server/handlers/api/compute/__init__.py | 5 +---- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/gns3server/compute/__init__.py b/gns3server/compute/__init__.py index 65ba86ae..7de7eac9 100644 --- a/gns3server/compute/__init__.py +++ b/gns3server/compute/__init__.py @@ -24,8 +24,9 @@ from .virtualbox import VirtualBox from .dynamips import Dynamips from .qemu import Qemu from .vmware import VMware +from .traceng import TraceNG -MODULES = [Builtin, VPCS, VirtualBox, Dynamips, Qemu, VMware] +MODULES = [Builtin, VPCS, VirtualBox, Dynamips, Qemu, VMware, TraceNG] if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": # IOU & Docker only runs on Linux but test suite works on UNIX platform @@ -34,8 +35,3 @@ if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.e MODULES.append(Docker) from .iou import IOU MODULES.append(IOU) - -#if sys.platform.startswith("win") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": -# FIXME: TraceNG only runs on Windows but test suite works on UNIX platform -from .traceng import TraceNG -MODULES.append(TraceNG) diff --git a/gns3server/compute/traceng/traceng_vm.py b/gns3server/compute/traceng/traceng_vm.py index 39b5bd50..f2bbc00a 100644 --- a/gns3server/compute/traceng/traceng_vm.py +++ b/gns3server/compute/traceng/traceng_vm.py @@ -19,11 +19,13 @@ TraceNG VM management in order to run a TraceNG VM. """ +import sys import os import socket import subprocess import asyncio import shutil +import ipaddress from gns3server.utils.asyncio import wait_for_process_termination from gns3server.utils.asyncio import monitor_process @@ -152,11 +154,17 @@ class TraceNGVM(BaseNode): :param ip_address: IP address """ + try: + if ip_address: + ipaddress.IPv4Address(ip_address) + except ipaddress.AddressValueError: + raise TraceNGError("Invalid IP address: {}\n".format(ip_address)) + + self._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, destination=None): @@ -164,6 +172,8 @@ class TraceNGVM(BaseNode): Starts the TraceNG process. """ + if not sys.platform.startswith("win"): + raise TraceNGError("Sorry, TraceNG can only run on Windows") yield from self._check_requirements() if not self.is_running(): nio = self._ethernet_adapter.get_nio(0) @@ -382,11 +392,10 @@ class TraceNGVM(BaseNode): (to be passed to subprocess.Popen()) """ - # TODO: better validation if not destination: - raise TraceNGError("Please provide a destination to trace") + raise TraceNGError("Please provide a host or IP address to trace") if not self._ip_address: - raise TraceNGError("Please provide an IP address for this TraceNG node") + raise TraceNGError("Please configure an IP address for this TraceNG node") command = [self._traceng_path()] # use the local UDP tunnel to uBridge instead diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index e3091374..d5aca607 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -154,11 +154,12 @@ class Controller: builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "cloud"), {"node_type": "cloud", "name": "Cloud", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "nat"), {"node_type": "nat", "name": "NAT", "category": 2, "symbol": ":/symbols/cloud.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "vpcs"), {"node_type": "vpcs", "name": "VPCS", "default_name_format": "PC-{0}", "category": 2, "symbol": ":/symbols/vpcs_guest.svg", "properties": {"base_script_file": "vpcs_base_config.txt"}}, builtin=True)) - builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"node_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/traceng.svg", "properties": {}}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_switch"), {"node_type": "ethernet_switch", "name": "Ethernet switch", "category": 1, "symbol": ":/symbols/ethernet_switch.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "ethernet_hub"), {"node_type": "ethernet_hub", "name": "Ethernet hub", "category": 1, "symbol": ":/symbols/hub.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "frame_relay_switch"), {"node_type": "frame_relay_switch", "name": "Frame Relay switch", "category": 1, "symbol": ":/symbols/frame_relay_switch.svg"}, builtin=True)) builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "atm_switch"), {"node_type": "atm_switch", "name": "ATM switch", "category": 1, "symbol": ":/symbols/atm_switch.svg"}, builtin=True)) + if sys.platform.startswith("win"): + builtins.append(Appliance(uuid.uuid3(uuid.NAMESPACE_DNS, "traceng"), {"node_type": "traceng", "name": "TraceNG", "default_name_format": "TraceNG-{0}", "category": 2, "symbol": ":/symbols/traceng.svg", "properties": {}}, builtin=True)) for b in builtins: self._appliances[b.id] = b diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index b0b2b5a6..c7b1e4ab 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -906,7 +906,7 @@ 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... + self.controller.notification.emit("log.warning", "TraceNG nodes must be started one by one") continue pool.append(node.start) yield from pool.join() diff --git a/gns3server/handlers/api/compute/__init__.py b/gns3server/handlers/api/compute/__init__.py index e742393e..dc2cc695 100644 --- a/gns3server/handlers/api/compute/__init__.py +++ b/gns3server/handlers/api/compute/__init__.py @@ -34,13 +34,10 @@ from .ethernet_hub_handler import EthernetHubHandler from .ethernet_switch_handler import EthernetSwitchHandler from .frame_relay_switch_handler import FrameRelaySwitchHandler from .atm_switch_handler import ATMSwitchHandler +from .traceng_handler import TraceNGHandler if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": # IOU & Docker only runs on Linux but test suite works on UNIX platform if not sys.platform.startswith("win"): from .iou_handler import IOUHandler from .docker_handler import DockerHandler - -#if sys.platform.startswith("win") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": -# FIXME: TraceNG only runs on Windows but test suite works on UNIX platform -from .traceng_handler import TraceNGHandler From 898f180310472a3199f6ccb25a79a4a8f155e4b1 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 29 Mar 2018 13:32:56 +0700 Subject: [PATCH 8/8] Fix issue with start all. --- gns3server/controller/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index c7b1e4ab..a9f41d90 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -906,7 +906,7 @@ class Project: pool = Pool(concurrency=3) for node in self.nodes.values(): if node.node_type == "traceng": - self.controller.notification.emit("log.warning", "TraceNG nodes must be started one by one") + #self.controller.notification.emit("log.warning", "TraceNG nodes must be started one by one") continue pool.append(node.start) yield from pool.join()