diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 5bd4c110..dd628589 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -20,8 +20,9 @@ from .base import IModule from .dynamips import Dynamips from .vpcs import VPCS from .virtualbox import VirtualBox +from .qemu import Qemu -MODULES = [Dynamips, VPCS, VirtualBox] +MODULES = [Dynamips, VPCS, VirtualBox, Qemu] if sys.platform.startswith("linux"): # IOU runs only on Linux diff --git a/gns3server/modules/iou/nios/nio.py b/gns3server/modules/iou/nios/nio.py index 197d4817..059d56a3 100644 --- a/gns3server/modules/iou/nios/nio.py +++ b/gns3server/modules/iou/nios/nio.py @@ -22,7 +22,7 @@ Base interface for NIOs. class NIO(object): """ - IOU NIO. + Network Input/Output. """ def __init__(self): diff --git a/gns3server/modules/iou/nios/nio_generic_ethernet.py b/gns3server/modules/iou/nios/nio_generic_ethernet.py index 45c89b4e..068e9fc3 100644 --- a/gns3server/modules/iou/nios/nio_generic_ethernet.py +++ b/gns3server/modules/iou/nios/nio_generic_ethernet.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_GenericEthernet(NIO): """ - NIO generic Ethernet NIO. + Generic Ethernet NIO. :param ethernet_device: Ethernet device name (e.g. eth0) """ diff --git a/gns3server/modules/iou/nios/nio_tap.py b/gns3server/modules/iou/nios/nio_tap.py index 3164e933..95ec631d 100644 --- a/gns3server/modules/iou/nios/nio_tap.py +++ b/gns3server/modules/iou/nios/nio_tap.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_TAP(NIO): """ - IOU TAP NIO. + TAP NIO. :param tap_device: TAP device name (e.g. tap0) """ diff --git a/gns3server/modules/iou/nios/nio_udp.py b/gns3server/modules/iou/nios/nio_udp.py index 41ffbc4f..2c850351 100644 --- a/gns3server/modules/iou/nios/nio_udp.py +++ b/gns3server/modules/iou/nios/nio_udp.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_UDP(NIO): """ - IOU UDP NIO. + UDP NIO. :param lport: local port number :param rhost: remote address/host diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py new file mode 100644 index 00000000..1935b10c --- /dev/null +++ b/gns3server/modules/qemu/__init__.py @@ -0,0 +1,684 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +""" +QEMU server module. +""" + +import os +import socket +import shutil +import subprocess +import re + +from gns3server.modules import IModule +from gns3server.config import Config +from .qemu_vm import QemuVM +from .qemu_error import QemuError +from .nios.nio_udp import NIO_UDP +from ..attic import find_unused_port + +from .schemas import QEMU_CREATE_SCHEMA +from .schemas import QEMU_DELETE_SCHEMA +from .schemas import QEMU_UPDATE_SCHEMA +from .schemas import QEMU_START_SCHEMA +from .schemas import QEMU_STOP_SCHEMA +from .schemas import QEMU_SUSPEND_SCHEMA +from .schemas import QEMU_RELOAD_SCHEMA +from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA +from .schemas import QEMU_ADD_NIO_SCHEMA +from .schemas import QEMU_DELETE_NIO_SCHEMA + +QEMU_BINARIES = ["qemu.exe", + "qemu-system-arm", + "qemu-system-mips", + "qemu-system-ppc", + "qemu-system-sparc", + "qemu-system-x86", + "qemu-system-i386", + "qemu-system-x86_64"] + +import logging +log = logging.getLogger(__name__) + + +class Qemu(IModule): + """ + QEMU module. + + :param name: module name + :param args: arguments for the module + :param kwargs: named arguments for the module + """ + + def __init__(self, name, *args, **kwargs): + + # get the qemu-img location + config = Config.instance() + qemu_config = config.get_section_config(name.upper()) + self._qemu_img_path = qemu_config.get("qemu_img_path") + if not self._qemu_img_path or not os.path.isfile(self._qemu_img_path): + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for qemu-img in the current working directory and $PATH + for path in paths: + try: + if "qemu-img" in os.listdir(path) and os.access(os.path.join(path, "qemu-img"), os.X_OK): + self._qemu_img_path = os.path.join(path, "qemu-img") + break + except OSError: + continue + + if not self._qemu_img_path: + log.warning("qemu-img couldn't be found!") + elif not os.access(self._qemu_img_path, os.X_OK): + log.warning("qemu-img is not executable") + + # a new process start when calling IModule + IModule.__init__(self, name, *args, **kwargs) + self._qemu_instances = {} + + config = Config.instance() + qemu_config = config.get_section_config(name.upper()) + self._console_start_port_range = qemu_config.get("console_start_port_range", 5001) + self._console_end_port_range = qemu_config.get("console_end_port_range", 5500) + self._allocated_udp_ports = [] + self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001) + self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500) + self._host = qemu_config.get("host", kwargs["host"]) + self._projects_dir = kwargs["projects_dir"] + self._tempdir = kwargs["temp_dir"] + self._working_dir = self._projects_dir + + def stop(self, signum=None): + """ + Properly stops the module. + + :param signum: signal number (if called by the signal handler) + """ + + # delete all QEMU instances + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.delete() + + IModule.stop(self, signum) # this will stop the I/O loop + + def get_qemu_instance(self, qemu_id): + """ + Returns a QEMU VM instance. + + :param qemu_id: QEMU VM identifier + + :returns: QemuVM instance + """ + + if qemu_id not in self._qemu_instances: + log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1) + self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id)) + return None + return self._qemu_instances[qemu_id] + + @IModule.route("qemu.reset") + def reset(self, request): + """ + Resets the module. + + :param request: JSON request + """ + + # delete all QEMU instances + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.delete() + + # resets the instance IDs + QemuVM.reset() + + self._qemu_instances.clear() + self._allocated_udp_ports.clear() + + log.info("QEMU module has been reset") + + @IModule.route("qemu.settings") + def settings(self, request): + """ + Set or update settings. + + Optional request parameters: + - working_dir (path to a working directory) + - project_name + - console_start_port_range + - console_end_port_range + - udp_start_port_range + - udp_end_port_range + + :param request: JSON request + """ + + if request is None: + self.send_param_error() + return + + if "qemu_img_path" in request and request["qemu_img_path"]: + self._qemu_img_path = request["qemu_img_path"] + log.info("QEMU image utility path set to {}".format(self._qemu_img_path)) + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.qemu_img_path = self._qemu_img_path + + if "working_dir" in request: + new_working_dir = request["working_dir"] + log.info("this server is local with working directory path to {}".format(new_working_dir)) + else: + new_working_dir = os.path.join(self._projects_dir, request["project_name"]) + log.info("this server is remote with working directory path to {}".format(new_working_dir)) + if self._projects_dir != self._working_dir != new_working_dir: + if not os.path.isdir(new_working_dir): + try: + shutil.move(self._working_dir, new_working_dir) + except OSError as e: + log.error("could not move working directory from {} to {}: {}".format(self._working_dir, + new_working_dir, + e)) + return + + # update the working directory if it has changed + if self._working_dir != new_working_dir: + self._working_dir = new_working_dir + for qemu_id in self._qemu_instances: + qemu_instance = self._qemu_instances[qemu_id] + qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_instance.id)) + + if "console_start_port_range" in request and "console_end_port_range" in request: + self._console_start_port_range = request["console_start_port_range"] + self._console_end_port_range = request["console_end_port_range"] + + if "udp_start_port_range" in request and "udp_end_port_range" in request: + self._udp_start_port_range = request["udp_start_port_range"] + self._udp_end_port_range = request["udp_end_port_range"] + + log.debug("received request {}".format(request)) + + @IModule.route("qemu.create") + def qemu_create(self, request): + """ + Creates a new QEMU VM instance. + + Mandatory request parameters: + - name (QEMU VM name) + - qemu_path (path to the Qemu binary) + + Optional request parameters: + - console (QEMU VM console port) + + Response parameters: + - id (QEMU VM instance identifier) + - name (QEMU VM name) + - default settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_CREATE_SCHEMA): + return + + name = request["name"] + qemu_path = request["qemu_path"] + console = request.get("console") + qemu_id = request.get("qemu_id") + + try: + qemu_instance = QemuVM(name, + qemu_path, + self._qemu_img_path, + self._working_dir, + self._host, + qemu_id, + console, + self._console_start_port_range, + self._console_end_port_range) + + except QemuError as e: + self.send_custom_error(str(e)) + return + + response = {"name": qemu_instance.name, + "id": qemu_instance.id} + + defaults = qemu_instance.defaults() + response.update(defaults) + self._qemu_instances[qemu_instance.id] = qemu_instance + self.send_response(response) + + @IModule.route("qemu.delete") + def qemu_delete(self, request): + """ + Deletes a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameter: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_DELETE_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.clean_delete() + del self._qemu_instances[request["id"]] + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response(True) + + @IModule.route("qemu.update") + def qemu_update(self, request): + """ + Updates a QEMU VM instance + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Optional request parameters: + - any setting to update + + Response parameters: + - updated settings + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_UPDATE_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + # update the QEMU VM settings + response = {} + for name, value in request.items(): + if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value: + try: + setattr(qemu_instance, name, value) + response[name] = value + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response(response) + + @IModule.route("qemu.start") + def qemu_start(self, request): + """ + Starts a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_START_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.start() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.stop") + def qemu_stop(self, request): + """ + Stops a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_STOP_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.stop() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.reload") + def qemu_reload(self, request): + """ + Reloads a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_RELOAD_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.reload() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.stop") + def qemu_stop(self, request): + """ + Stops a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_STOP_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.stop() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.suspend") + def qemu_suspend(self, request): + """ + Suspends a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_SUSPEND_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + qemu_instance.suspend() + except QemuError as e: + self.send_custom_error(str(e)) + return + self.send_response(True) + + @IModule.route("qemu.allocate_udp_port") + def allocate_udp_port(self, request): + """ + Allocates a UDP port in order to create an UDP NIO. + + Mandatory request parameters: + - id (QEMU VM identifier) + - port_id (unique port identifier) + + Response parameters: + - port_id (unique port identifier) + - lport (allocated local port) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_ALLOCATE_UDP_PORT_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + try: + port = find_unused_port(self._udp_start_port_range, + self._udp_end_port_range, + host=self._host, + socket_type="UDP", + ignore_ports=self._allocated_udp_ports) + except Exception as e: + self.send_custom_error(str(e)) + return + + self._allocated_udp_ports.append(port) + log.info("{} [id={}] has allocated UDP port {} with host {}".format(qemu_instance.name, + qemu_instance.id, + port, + self._host)) + + response = {"lport": port, + "port_id": request["port_id"]} + self.send_response(response) + + @IModule.route("qemu.add_nio") + def add_nio(self, request): + """ + Adds an NIO (Network Input/Output) for a QEMU VM instance. + + Mandatory request parameters: + - id (QEMU VM instance identifier) + - port (port number) + - port_id (unique port identifier) + - nio (one of the following) + - type "nio_udp" + - lport (local port) + - rhost (remote host) + - rport (remote port) + + Response parameters: + - port_id (unique port identifier) + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_ADD_NIO_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + port = request["port"] + try: + nio = None + if request["nio"]["type"] == "nio_udp": + lport = request["nio"]["lport"] + rhost = request["nio"]["rhost"] + rport = request["nio"]["rport"] + try: + #TODO: handle IPv6 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.connect((rhost, rport)) + except OSError as e: + raise QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) + nio = NIO_UDP(lport, rhost, rport) + if not nio: + raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) + except QemuError as e: + self.send_custom_error(str(e)) + return + + try: + qemu_instance.port_add_nio_binding(port, nio) + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response({"port_id": request["port_id"]}) + + @IModule.route("qemu.delete_nio") + def delete_nio(self, request): + """ + Deletes an NIO (Network Input/Output). + + Mandatory request parameters: + - id (QEMU VM instance identifier) + - port (port identifier) + + Response parameters: + - True on success + + :param request: JSON request + """ + + # validate the request + if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA): + return + + # get the instance + qemu_instance = self.get_qemu_instance(request["id"]) + if not qemu_instance: + return + + port = request["port"] + try: + nio = qemu_instance.port_remove_nio_binding(port) + if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports: + self._allocated_udp_ports.remove(nio.lport) + except QemuError as e: + self.send_custom_error(str(e)) + return + + self.send_response(True) + + def _get_qemu_version(self, qemu_path): + """ + Gets the Qemu version. + + :param qemu_path: path to Qemu + """ + + try: + output = subprocess.check_output([qemu_path, "--version"]) + match = re.search("QEMU emulator version ([0-9a-z\-\.]+)", output.decode("utf-8")) + if match: + version = match.group(1) + return version + else: + raise QemuError("Could not determine the Qemu version for {}".format(qemu_path)) + except (OSError, subprocess.CalledProcessError) as e: + raise QemuError("Error while looking for the Qemu version: {}".format(e)) + + @IModule.route("qemu.qemu_list") + def qemu_list(self, request): + """ + Gets QEMU binaries list. + + Response parameters: + - Server address/host + - List of Qemu binaries + """ + + qemus = [] + paths = [os.getcwd()] + os.environ["PATH"].split(":") + # look for Qemu binaries in the current working directory and $PATH + for path in paths: + for qemu_binary in QEMU_BINARIES: + try: + if qemu_binary in os.listdir(path) and os.access(os.path.join(path, qemu_binary), os.X_OK): + qemu_path = os.path.join(path, qemu_binary) + version = self._get_qemu_version(qemu_path) + qemus.append({"path": qemu_path, "version": version}) + except OSError: + continue + + response = {"server": self._host, + "qemus": qemus} + self.send_response(response) + + @IModule.route("qemu.echo") + def echo(self, request): + """ + Echo end point for testing purposes. + + :param request: JSON request + """ + + if request is None: + self.send_param_error() + else: + log.debug("received request {}".format(request)) + self.send_response(request) diff --git a/gns3server/modules/qemu/adapters/__init__.py b/gns3server/modules/qemu/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/qemu/adapters/adapter.py b/gns3server/modules/qemu/adapters/adapter.py new file mode 100644 index 00000000..cf439427 --- /dev/null +++ b/gns3server/modules/qemu/adapters/adapter.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + + +class Adapter(object): + """ + Base class for adapters. + + :param interfaces: number of interfaces supported by this adapter. + """ + + def __init__(self, interfaces=1): + + self._interfaces = interfaces + + self._ports = {} + for port_id in range(0, interfaces): + self._ports[port_id] = None + + def removable(self): + """ + Returns True if the adapter can be removed from a slot + and False if not. + + :returns: boolean + """ + + return True + + def port_exists(self, port_id): + """ + Checks if a port exists on this adapter. + + :returns: True is the port exists, + False otherwise. + """ + + if port_id in self._ports: + return True + return False + + def add_nio(self, port_id, nio): + """ + Adds a NIO to a port on this adapter. + + :param port_id: port ID (integer) + :param nio: NIO instance + """ + + self._ports[port_id] = nio + + def remove_nio(self, port_id): + """ + Removes a NIO from a port on this adapter. + + :param port_id: port ID (integer) + """ + + self._ports[port_id] = None + + def get_nio(self, port_id): + """ + Returns the NIO assigned to a port. + + :params port_id: port ID (integer) + + :returns: NIO instance + """ + + return self._ports[port_id] + + @property + def ports(self): + """ + Returns port to NIO mapping + + :returns: dictionary port -> NIO + """ + + return self._ports + + @property + def interfaces(self): + """ + Returns the number of interfaces supported by this adapter. + + :returns: number of interfaces + """ + + return self._interfaces diff --git a/gns3server/modules/qemu/adapters/ethernet_adapter.py b/gns3server/modules/qemu/adapters/ethernet_adapter.py new file mode 100644 index 00000000..27426ec2 --- /dev/null +++ b/gns3server/modules/qemu/adapters/ethernet_adapter.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +from .adapter import Adapter + + +class EthernetAdapter(Adapter): + """ + QEMU Ethernet adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=1) + + def __str__(self): + + return "QEMU Ethernet adapter" diff --git a/gns3server/modules/qemu/nios/__init__.py b/gns3server/modules/qemu/nios/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gns3server/modules/qemu/nios/nio.py b/gns3server/modules/qemu/nios/nio.py new file mode 100644 index 00000000..eee5f1d5 --- /dev/null +++ b/gns3server/modules/qemu/nios/nio.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 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 . + +""" +Base interface for NIOs. +""" + + +class NIO(object): + """ + Network Input/Output. + """ + + def __init__(self): + + self._capturing = False + self._pcap_output_file = "" + + def startPacketCapture(self, pcap_output_file): + """ + + :param pcap_output_file: PCAP destination file for the capture + """ + + self._capturing = True + self._pcap_output_file = pcap_output_file + + def stopPacketCapture(self): + + self._capturing = False + self._pcap_output_file = "" + + @property + def capturing(self): + """ + Returns either a capture is configured on this NIO. + + :returns: boolean + """ + + return self._capturing + + @property + def pcap_output_file(self): + """ + Returns the path to the PCAP output file. + + :returns: path to the PCAP output file + """ + + return self._pcap_output_file diff --git a/gns3server/modules/qemu/nios/nio_udp.py b/gns3server/modules/qemu/nios/nio_udp.py new file mode 100644 index 00000000..2c850351 --- /dev/null +++ b/gns3server/modules/qemu/nios/nio_udp.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2013 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 . + +""" +Interface for UDP NIOs. +""" + +from .nio import NIO + + +class NIO_UDP(NIO): + """ + UDP NIO. + + :param lport: local port number + :param rhost: remote address/host + :param rport: remote port number + """ + + _instance_count = 0 + + def __init__(self, lport, rhost, rport): + + NIO.__init__(self) + self._lport = lport + self._rhost = rhost + self._rport = rport + + @property + def lport(self): + """ + Returns the local port + + :returns: local port number + """ + + return self._lport + + @property + def rhost(self): + """ + Returns the remote host + + :returns: remote address/host + """ + + return self._rhost + + @property + def rport(self): + """ + Returns the remote port + + :returns: remote port number + """ + + return self._rport + + def __str__(self): + + return "NIO UDP" diff --git a/gns3server/modules/qemu/qemu_error.py b/gns3server/modules/qemu/qemu_error.py new file mode 100644 index 00000000..55135a34 --- /dev/null +++ b/gns3server/modules/qemu/qemu_error.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 QEMU module. +""" + + +class QemuError(Exception): + + def __init__(self, message, original_exception=None): + + Exception.__init__(self, message) + if isinstance(message, Exception): + message = str(message) + self._message = message + self._original_exception = original_exception + + def __repr__(self): + + return self._message + + def __str__(self): + + return self._message diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py new file mode 100644 index 00000000..2a32b035 --- /dev/null +++ b/gns3server/modules/qemu/qemu_vm.py @@ -0,0 +1,616 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +""" +QEMU VM instance. +""" + +import os +import shutil +import random +import subprocess + +from .qemu_error import QemuError +from .adapters.ethernet_adapter import EthernetAdapter +from .nios.nio_udp import NIO_UDP +from ..attic import find_unused_port + +import logging +log = logging.getLogger(__name__) + + +class QemuVM(object): + """ + QEMU VM implementation. + + :param name: name of this QEMU VM + :param qemu_path: path to the QEMU binary + :param qemu_img_path: path to the QEMU IMG binary + :param working_dir: path to a working directory + :param host: host/address to bind for console and UDP connections + :param qemu_id: QEMU VM instance ID + :param console: TCP console port + :param console_start_port_range: TCP console port range start + :param console_end_port_range: TCP console port range end + """ + + _instances = [] + _allocated_console_ports = [] + + def __init__(self, + name, + qemu_path, + qemu_img_path, + working_dir, + host="127.0.0.1", + qemu_id=None, + console=None, + console_start_port_range=5001, + console_end_port_range=5500): + + if not qemu_id: + self._id = 0 + for identifier in range(1, 1024): + if identifier not in self._instances: + self._id = identifier + self._instances.append(self._id) + break + + if self._id == 0: + raise QemuError("Maximum number of QEMU VM instances reached") + else: + if qemu_id in self._instances: + raise QemuError("QEMU identifier {} is already used by another QEMU VM instance".format(qemu_id)) + self._id = qemu_id + self._instances.append(self._id) + + self._name = name + self._working_dir = None + self._host = host + self._command = [] + self._started = False + self._process = None + self._stdout_file = "" + self._qemu_img_path = qemu_img_path + self._console_start_port_range = console_start_port_range + self._console_end_port_range = console_end_port_range + + # QEMU settings + self._qemu_path = qemu_path + self._disk_image = "" + self._options = "" + self._ram = 256 + self._console = console + self._ethernet_adapters = [] + self._adapter_type = "e1000" + + working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id)) + + if qemu_id and not os.path.isdir(working_dir_path): + raise QemuError("Working directory {} doesn't exist".format(working_dir_path)) + + # create the device own working directory + self.working_dir = working_dir_path + + if not self._console: + # allocate a console port + try: + self._console = find_unused_port(self._console_start_port_range, + self._console_end_port_range, + self._host, + ignore_ports=self._allocated_console_ports) + except Exception as e: + raise QemuError(e) + + if self._console in self._allocated_console_ports: + raise QemuError("Console port {} is already used by another QEMU VM".format(console)) + self._allocated_console_ports.append(self._console) + + self.adapters = 1 # creates 1 adapter by default + log.info("QEMU VM {name} [id={id}] has been created".format(name=self._name, + id=self._id)) + + def defaults(self): + """ + Returns all the default attribute values for this QEMU VM. + + :returns: default values (dictionary) + """ + + qemu_defaults = {"name": self._name, + "qemu_path": self._qemu_path, + "ram": self._ram, + "disk_image": self._disk_image, + "options": self._options, + "adapters": self.adapters, + "adapter_type": self._adapter_type, + "console": self._console} + + return qemu_defaults + + @property + def id(self): + """ + Returns the unique ID for this QEMU VM. + + :returns: id (integer) + """ + + return self._id + + @classmethod + def reset(cls): + """ + Resets allocated instance list. + """ + + cls._instances.clear() + cls._allocated_console_ports.clear() + + @property + def name(self): + """ + Returns the name of this QEMU VM. + + :returns: name + """ + + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this QEMU VM. + + :param new_name: name + """ + + log.info("QEMU VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, + id=self._id, + new_name=new_name)) + + self._name = new_name + + @property + def working_dir(self): + """ + Returns current working directory + + :returns: path to the working directory + """ + + return self._working_dir + + @working_dir.setter + def working_dir(self, working_dir): + """ + Sets the working directory this QEMU VM. + + :param working_dir: path to the working directory + """ + + try: + os.makedirs(working_dir) + except FileExistsError: + pass + except OSError as e: + raise QemuError("Could not create working directory {}: {}".format(working_dir, e)) + + self._working_dir = working_dir + log.info("QEMU VM {name} [id={id}]: working directory changed to {wd}".format(name=self._name, + id=self._id, + wd=self._working_dir)) + + @property + def console(self): + """ + Returns the TCP console port. + + :returns: console port (integer) + """ + + return self._console + + @console.setter + def console(self, console): + """ + Sets the TCP console port. + + :param console: console port (integer) + """ + + if console in self._allocated_console_ports: + raise QemuError("Console port {} is already used by another QEMU VM".format(console)) + + self._allocated_console_ports.remove(self._console) + self._console = console + self._allocated_console_ports.append(self._console) + + log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name, + id=self._id, + port=console)) + + def delete(self): + """ + Deletes this QEMU VM. + """ + + self.stop() + if self._id in self._instances: + self._instances.remove(self._id) + + if self.console and self.console in self._allocated_console_ports: + self._allocated_console_ports.remove(self.console) + + log.info("QEMU VM {name} [id={id}] has been deleted".format(name=self._name, + id=self._id)) + + def clean_delete(self): + """ + Deletes this QEMU VM & all files. + """ + + self.stop() + if self._id in self._instances: + self._instances.remove(self._id) + + if self.console: + self._allocated_console_ports.remove(self.console) + + try: + shutil.rmtree(self._working_dir) + except OSError as e: + log.error("could not delete QEMU VM {name} [id={id}]: {error}".format(name=self._name, + id=self._id, + error=e)) + return + + log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name, + id=self._id)) + + @property + def qemu_path(self): + """ + Returns the QEMU binary path for this QEMU VM. + + :returns: QEMU path + """ + + return self._qemu_path + + @qemu_path.setter + def qemu_path(self, qemu_path): + """ + Sets the QEMU binary path this QEMU VM. + + :param qemu_path: QEMU path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name, + id=self._id, + qemu_path=qemu_path)) + self._qemu_path = qemu_path + + @property + def qemu_img_path(self): + """ + Returns the QEMU IMG binary path for this QEMU VM. + + :returns: QEMU IMG path + """ + + return self._qemu_img_path + + @qemu_img_path.setter + def qemu_img_path(self, qemu_img_path): + """ + Sets the QEMU IMG binary path this QEMU VM. + + :param qemu_img_path: QEMU IMG path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU IMG path to {qemu_img_path}".format(name=self._name, + id=self._id, + qemu_img_path=qemu_img_path)) + self._qemu_img_path = qemu_img_path + + @property + def disk_image(self): + """ + Returns the disk image path for this QEMU VM. + + :returns: QEMU disk image path + """ + + return self._disk_image + + @disk_image.setter + def disk_image(self, disk_image): + """ + Sets the disk image for this QEMU VM. + + :param disk_image: QEMU disk image path + """ + + log.info("QEMU VM {name} [id={id}] has set the QEMU disk image path to {disk_image}".format(name=self._name, + id=self._id, + disk_image=disk_image)) + self._disk_image = disk_image + + @property + def adapters(self): + """ + Returns the number of Ethernet adapters for this QEMU VM instance. + + :returns: number of adapters + """ + + return len(self._ethernet_adapters) + + @adapters.setter + def adapters(self, adapters): + """ + Sets the number of Ethernet adapters for this QEMU VM instance. + + :param adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for adapter_id in range(0, adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=adapters)) + + @property + def adapter_type(self): + """ + Returns the adapter type for this QEMU VM instance. + + :returns: adapter type (string) + """ + + return self._adapter_type + + @adapter_type.setter + def adapter_type(self, adapter_type): + """ + Sets the adapter type for this QEMU VM instance. + + :param adapter_type: adapter type (string) + """ + + self._adapter_type = adapter_type + + log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name, + id=self._id, + adapter_type=adapter_type)) + + def start(self): + """ + Starts this QEMU VM. + """ + + if not self.is_running(): + + if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): + raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path)) + + #TODO: check binary image is valid? + self._command = self._build_command() + + try: + log.info("starting QEMU: {}".format(self._command)) + self._stdout_file = os.path.join(self._working_dir, "qemu.log") + log.info("logging to {}".format(self._stdout_file)) + with open(self._stdout_file, "w") as fd: + self._process = subprocess.Popen(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self._working_dir) + log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid)) + self._started = True + except OSError as e: + stdout = self.read_stdout() + log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) + raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout)) + + def stop(self): + """ + Stops this QEMU VM. + """ + + # stop the QEMU process + if self.is_running(): + log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid)) + try: + self._process.terminate() + self._process.wait(1) + except subprocess.TimeoutExpired: + self._process.kill() + if self._process.poll() is None: + log.warn("QEMU VM instance {} PID={} is still running".format(self._id, + self._process.pid)) + self._process = None + self._started = False + + def suspend(self): + """ + Suspends this QEMU VM. + """ + + pass + + def reload(self): + """ + Reloads this QEMU VM. + """ + + pass + + def resume(self): + """ + Resumes this QEMU VM. + """ + + pass + + def port_add_nio_binding(self, adapter_id, nio): + """ + Adds a port NIO binding. + + :param adapter_id: adapter ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + adapter.add_nio(0, nio) + log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + + def port_remove_nio_binding(self, adapter_id): + """ + Removes a port NIO binding. + + :param adapter_id: adapter ID + + :returns: NIO instance + """ + + try: + adapter = self._ethernet_adapters[adapter_id] + except IndexError: + raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name, + adapter_id=adapter_id)) + + nio = adapter.get_nio(0) + adapter.remove_nio(0) + log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name, + id=self._id, + nio=nio, + adapter_id=adapter_id)) + return nio + + @property + def started(self): + """ + Returns either this QEMU VM has been started or not. + + :returns: boolean + """ + + return self._started + + def read_stdout(self): + """ + Reads the standard output of the QEMU process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._stdout_file: + try: + with open(self._stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._stdout_file, e)) + return output + + def is_running(self): + """ + Checks if the QEMU process is running + + :returns: True or False + """ + + if self._process and self._process.poll() is None: + return True + return False + + def command(self): + """ + Returns the QEMU command line. + + :returns: QEMU command line (string) + """ + + return " ".join(self._build_command()) + + def _serial_options(self): + + if self._console: + return ["-serial", "telnet:{}:{},server,nowait".format(self._host, self._console)] + else: + return [] + + def _disk_options(self): + + hda_disk = os.path.join(self._working_dir, "hda.disk") + if not os.path.exists(hda_disk): + try: + retcode = subprocess.call([self._qemu_img_path, "create", "-o", + "backing_file={}".format(self._disk_image), + "-f", "qcow2", hda_disk]) + log.info("{} returned with {}".format(self._qemu_img_path, retcode)) + except OSError as e: + raise QemuError("Could not create disk image {}".format(e)) + + return ["-hda", hda_disk] + + def _network_options(self): + + network_options = [] + adapter_id = 0 + for adapter in self._ethernet_adapters: + nio = adapter.get_nio(0) + if nio: + #TODO: let users specific the base mac address + mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) + network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)]) + if isinstance(nio, NIO_UDP): + network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, + nio.rhost, + nio.rport, + self._host, + nio.lport)]) + else: + network_options.extend(["-device", "{}".format(self._adapter_type)]) + adapter_id += 1 + + return network_options + + def _build_command(self): + """ + Command to start the QEMU process. + (to be passed to subprocess.Popen()) + """ + + command = [self._qemu_path] + command.extend(["-name", self._name]) + command.extend(["-m", str(self._ram)]) + command.extend(self._disk_options()) + command.extend(self._serial_options()) + command.extend(self._network_options()) + return command diff --git a/gns3server/modules/qemu/schemas.py b/gns3server/modules/qemu/schemas.py new file mode 100644 index 00000000..bfcbfdff --- /dev/null +++ b/gns3server/modules/qemu/schemas.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + + +QEMU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new QEMU VM instance", + "type": "object", + "properties": { + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "Path to QEMU", + "type": "string", + "minLength": 1, + }, + "qemu_id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "qemu_path"], +} + +QEMU_DELETE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "path to QEMU", + "type": "string", + "minLength": 1, + }, + "disk_image": { + "description": "QEMU disk image path", + "type": "string", + "minLength": 1, + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 1, + "maximum": 8, + }, + "adapter_type": { + "description": "QEMU adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "options": { + "description": "additional QEMU options", + "type": "string", + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_STOP_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to stop a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_SUSPEND_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to suspend a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_RELOAD_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to reload a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_ALLOCATE_UDP_PORT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to allocate an UDP port for a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the QEMU VM instance", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id", "port_id"] +} + +QEMU_ADD_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a QEMU VM instance", + "type": "object", + + "definitions": { + "UDP": { + "description": "UDP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_udp"] + }, + "lport": { + "description": "Local port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "rhost": { + "description": "Remote host", + "type": "string", + "minLength": 1 + }, + "rport": { + "description": "Remote port", + "type": "integer", + "minimum": 1, + "maximum": 65535 + } + }, + "required": ["type", "lport", "rhost", "rport"], + "additionalProperties": False + }, + "Ethernet": { + "description": "Generic Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_generic_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "LinuxEthernet": { + "description": "Linux Ethernet Network Input/Output", + "properties": { + "type": { + "enum": ["nio_linux_ethernet"] + }, + "ethernet_device": { + "description": "Ethernet device name e.g. eth0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "ethernet_device"], + "additionalProperties": False + }, + "TAP": { + "description": "TAP Network Input/Output", + "properties": { + "type": { + "enum": ["nio_tap"] + }, + "tap_device": { + "description": "TAP device name e.g. tap0", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "tap_device"], + "additionalProperties": False + }, + "UNIX": { + "description": "UNIX Network Input/Output", + "properties": { + "type": { + "enum": ["nio_unix"] + }, + "local_file": { + "description": "path to the UNIX socket file (local)", + "type": "string", + "minLength": 1 + }, + "remote_file": { + "description": "path to the UNIX socket file (remote)", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "local_file", "remote_file"], + "additionalProperties": False + }, + "VDE": { + "description": "VDE Network Input/Output", + "properties": { + "type": { + "enum": ["nio_vde"] + }, + "control_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + "local_file": { + "description": "path to the VDE control file", + "type": "string", + "minLength": 1 + }, + }, + "required": ["type", "control_file", "local_file"], + "additionalProperties": False + }, + "NULL": { + "description": "NULL Network Input/Output", + "properties": { + "type": { + "enum": ["nio_null"] + }, + }, + "required": ["type"], + "additionalProperties": False + }, + }, + + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "port_id": { + "description": "Unique port identifier for the QEMU VM instance", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 8 + }, + "nio": { + "type": "object", + "description": "Network Input/Output", + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/LinuxEthernet"}, + {"$ref": "#/definitions/TAP"}, + {"$ref": "#/definitions/UNIX"}, + {"$ref": "#/definitions/VDE"}, + {"$ref": "#/definitions/NULL"}, + ] + }, + }, + "additionalProperties": False, + "required": ["id", "port_id", "port", "nio"] +} + + +QEMU_DELETE_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to delete a NIO for a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + "port": { + "description": "Port number", + "type": "integer", + "minimum": 0, + "maximum": 8 + }, + }, + "additionalProperties": False, + "required": ["id", "port"] +} diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index cfd8a6aa..d7af5c48 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -68,7 +68,7 @@ class VirtualBox(IModule): self._vboxwrapper_path = vbox_config.get("vboxwrapper_path") if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path): paths = [os.getcwd()] + os.environ["PATH"].split(":") - # look for iouyap in the current working directory and $PATH + # look for vboxwrapper in the current working directory and $PATH for path in paths: try: if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK): @@ -172,9 +172,9 @@ class VirtualBox(IModule): """ Returns a VirtualBox VM instance. - :param vbox_id: VirtualBox device identifier + :param vbox_id: VirtualBox VM identifier - :returns: VBoxDevice instance + :returns: VirtualBoxVM instance """ if vbox_id not in self._vbox_instances: @@ -271,6 +271,7 @@ class VirtualBox(IModule): Mandatory request parameters: - name (VirtualBox VM name) + - vmname (VirtualBox VM name in VirtualBox) Optional request parameters: - console (VirtualBox VM console port) @@ -653,7 +654,7 @@ class VirtualBox(IModule): Deletes an NIO (Network Input/Output). Mandatory request parameters: - - id (VPCS instance identifier) + - id (VirtualBox instance identifier) - port (port identifier) Response parameters: @@ -688,7 +689,7 @@ class VirtualBox(IModule): Starts a packet capture. Mandatory request parameters: - - id (vm identifier) + - id (VirtualBox VM identifier) - port (port number) - port_id (port identifier) - capture_file_name @@ -729,7 +730,7 @@ class VirtualBox(IModule): Stops a packet capture. Mandatory request parameters: - - id (vm identifier) + - id (VirtualBox VM identifier) - port (port number) - port_id (port identifier) diff --git a/gns3server/modules/virtualbox/nios/nio.py b/gns3server/modules/virtualbox/nios/nio.py index c85569bd..eee5f1d5 100644 --- a/gns3server/modules/virtualbox/nios/nio.py +++ b/gns3server/modules/virtualbox/nios/nio.py @@ -22,7 +22,7 @@ Base interface for NIOs. class NIO(object): """ - IOU NIO. + Network Input/Output. """ def __init__(self): diff --git a/gns3server/modules/virtualbox/nios/nio_udp.py b/gns3server/modules/virtualbox/nios/nio_udp.py index 41ffbc4f..2c850351 100644 --- a/gns3server/modules/virtualbox/nios/nio_udp.py +++ b/gns3server/modules/virtualbox/nios/nio_udp.py @@ -24,7 +24,7 @@ from .nio import NIO class NIO_UDP(NIO): """ - IOU UDP NIO. + UDP NIO. :param lport: local port number :param rhost: remote address/host diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py index ee550e7b..4c3ed6b2 100644 --- a/gns3server/modules/vpcs/nios/nio_tap.py +++ b/gns3server/modules/vpcs/nios/nio_tap.py @@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only). class NIO_TAP(object): """ - IOU TAP NIO. + TAP NIO. :param tap_device: TAP device name (e.g. tap0) """ diff --git a/gns3server/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py index 3142d70e..0527f675 100644 --- a/gns3server/modules/vpcs/nios/nio_udp.py +++ b/gns3server/modules/vpcs/nios/nio_udp.py @@ -22,7 +22,7 @@ Interface for UDP NIOs. class NIO_UDP(object): """ - IOU UDP NIO. + UDP NIO. :param lport: local port number :param rhost: remote address/host diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 33d5d8e0..d5ad8c09 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -338,11 +338,10 @@ class VPCSDevice(object): """ try: - output = subprocess.check_output([self._path, "-v"], stderr=subprocess.STDOUT, cwd=self._working_dir) + output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir) match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8")) if match: version = match.group(1) - print(version) if parse_version(version) < parse_version("0.5b1"): raise VPCSError("VPCS executable version must be >= 0.5b1") else: