diff --git a/gns3server/compute/__init__.py b/gns3server/compute/__init__.py index 5d454a86..7de7eac9 100644 --- a/gns3server/compute/__init__.py +++ b/gns3server/compute/__init__.py @@ -24,15 +24,14 @@ 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": - - from .docker import Docker - MODULES.append(Docker) - - # IOU runs only on Linux but testsuite work 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) 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..cdd97741 --- /dev/null +++ b/gns3server/compute/traceng/traceng_vm.py @@ -0,0 +1,427 @@ +# -*- 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 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 + +from .traceng_error import TraceNGError +from ..adapters.ethernet_adapter import EthernetAdapter +from ..nios.nio_udp import NIOUDP +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, console_type="none"): + + super().__init__(name, node_id, project, manager, console=console, console_type=console_type) + self._process = None + self._started = False + self._ip_address = None + self._destination = None + 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, + "ip_address": self.ip_address, + "node_id": self.id, + "node_directory": self.working_path, + "status": self.status, + "console": self._console, + "console_type": "none", + "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 + + @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 + """ + + 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)) + + @asyncio.coroutine + def start(self, destination=None): + """ + 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) + 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 = 0 + if hasattr(subprocess, "CREATE_NEW_CONSOLE"): + 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) + + log.info("TraceNG instance {} started PID={}".format(self.name, self._process.pid)) + self._started = True + self.status = "started" + except (OSError, subprocess.SubprocessError) as e: + 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): + """ + 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)}) + + @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(self._destination) + + 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 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, destination): + """ + Command to start the TraceNG process. + (to be passed to subprocess.Popen()) + """ + + if not destination: + raise TraceNGError("Please provide a host or IP address to trace") + if not self._ip_address: + raise TraceNGError("Please configure an IP address for this TraceNG node") + + self._destination = destination + command = [self._traceng_path()] + # 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(["-u"]) # enable UDP tunnel + command.extend(["-c", str(nio.lport)]) # source UDP port + command.extend(["-v", str(nio.rport)]) # destination UDP port + try: + 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(["-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 diff --git a/gns3server/controller/__init__.py b/gns3server/controller/__init__.py index a78790ba..e3fea8ff 100644 --- a/gns3server/controller/__init__.py +++ b/gns3server/controller/__init__.py @@ -116,6 +116,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 @@ -154,6 +157,8 @@ class Controller: 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/link.py b/gns3server/controller/link.py index 8578f3ae..a7b2d156 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -417,6 +417,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 d0faf857..441a17e8 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -120,7 +120,7 @@ class Node: return self.node_type not in ( "qemu", "docker", "dynamips", "vpcs", "vmware", "virtualbox", - "iou") + "iou", "traceng") @property def id(self): @@ -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)) @@ -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/controller/project.py b/gns3server/controller/project.py index 77ffbb9e..ab370320 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -939,7 +939,12 @@ class Project: Start all nodes """ pool = Pool(concurrency=3) + emit_warning = True for node in self.nodes.values(): + if node.node_type == "traceng" and emit_warning: + self.controller.notification.emit("log.warning", {"message": "TraceNG nodes must be started one by one"}) + emit_warning = False + 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 e255769a..dc2cc695 100644 --- a/gns3server/handlers/api/compute/__init__.py +++ b/gns3server/handlers/api/compute/__init__.py @@ -34,9 +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 runs only 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 diff --git a/gns3server/handlers/api/compute/traceng_handler.py b/gns3server/handlers/api/compute/traceng_handler.py new file mode 100644 index 00000000..188cde5f --- /dev/null +++ b/gns3server/handlers/api/compute/traceng_handler.py @@ -0,0 +1,339 @@ +# -*- 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_START_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")) + vm.ip_address = request.json.get("ip_address", "") # FIXME, required IP address to create node? + 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.ip_address = request.json.get("ip_address", vm.ip_address) + 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", + 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(request.json["destination"]) + 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/handlers/api/controller/node_handler.py b/gns3server/handlers/api/controller/node_handler.py index 417abe47..50777b52 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/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 d1f56639..9ca659dd 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..6c0f63cc --- /dev/null +++ b/gns3server/schemas/traceng.py @@ -0,0 +1,151 @@ +# -*- 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": ["none"] + }, + "ip_address": { + "description": "Source IP address for tracing", + "type": ["string"] + } + }, + "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": ["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", + "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", "null"] + }, + "console_type": { + "description": "Console type", + "enum": ["none"] + }, + "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" + }, + "ip_address": { + "description": "Source IP address for tracing", + "type": ["string"] + } + }, + "additionalProperties": False, + "required": ["name", "node_id", "status", "console", "console_type", "project_id", "command_line", "ip_address"] +} 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/compute/traceng/test_traceng_vm.py b/tests/compute/traceng/test_traceng_vm.py new file mode 100644 index 00000000..bbf8df55 --- /dev/null +++ b/tests/compute/traceng/test_traceng_vm.py @@ -0,0 +1,179 @@ +# -*- 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 sys + +from tests.utils import asyncio_patch, AsyncioMagicMock +from unittest.mock import patch, MagicMock, ANY, PropertyMock + +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 + + vm.ip_address = "192.168.1.1" + with patch("sys.platform", return_value="win"): + 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: + loop.run_until_complete(asyncio.async(vm.start("192.168.1.2"))) + assert mock_exec.call_args[0] == (vm._traceng_path(), + '-u', + '-c', + ANY, + '-v', + ANY, + '-b', + '127.0.0.1', + '-s', + 'ICMP', + '-f', + '192.168.1.1', + '192.168.1.2') + 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 + + vm.ip_address = "192.168.1.1" + with NotificationManager.instance().queue() as queue: + with patch("sys.platform", return_value="win"): + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True): + 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)) + + vm._ubridge_send = AsyncioMagicMock() + async_run(vm.start("192.168.1.2")) + 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 + + 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 + + vm.ip_address = "192.168.1.1" + with patch("sys.platform", return_value="win"): + with asyncio_patch("gns3server.compute.traceng.traceng_vm.TraceNGVM._check_requirements", return_value=True): + 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)) + + vm._ubridge_send = AsyncioMagicMock() + async_run(vm.start("192.168.1.2")) + assert vm.is_running() + + 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_capabilities.py b/tests/handlers/api/compute/test_capabilities.py index 4f12c6a7..64c9f68e 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', 'traceng', 'docker', 'iou'], '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', 'traceng', 'docker', 'iou'], 'version': __version__, 'platform': sys.platform} diff --git a/tests/handlers/api/compute/test_traceng.py b/tests/handlers/api/compute/test_traceng.py new file mode 100644 index 00000000..e65dcb42 --- /dev/null +++ b/tests/handlers/api/compute/test_traceng.py @@ -0,0 +1,147 @@ +# -*- 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_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"]), {"destination": "192.168.1.2"}, 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", + "ip_address": "192.168.1.1", + }, + example=True) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["ip_address"] == "192.168.1.1"