From 2e99ef69a9e550cba6bceaa15a5ba48c336a9853 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 14:31:21 +0100 Subject: [PATCH 01/11] Modules support start iou process (not ioucon and iouyap) --- gns3server/modules/__init__.py | 3 +- .../modules/adapters/ethernet_adapter.py | 2 +- gns3server/modules/adapters/serial_adapter.py | 32 ++ gns3server/modules/dynamips/dynamips_error.py | 16 +- gns3server/modules/iou/__init__.py | 64 +++ gns3server/modules/iou/iou_error.py | 26 + gns3server/modules/iou/iou_vm.py | 460 ++++++++++++++++++ .../modules/virtualbox/virtualbox_error.py | 16 +- gns3server/modules/vm_error.py | 17 +- gns3server/modules/vpcs/vpcs_error.py | 16 +- tests/modules/iou/test_iou_manager.py | 73 +++ tests/modules/iou/test_iou_vm.py | 164 +++++++ 12 files changed, 841 insertions(+), 48 deletions(-) create mode 100644 gns3server/modules/adapters/serial_adapter.py create mode 100644 gns3server/modules/iou/__init__.py create mode 100644 gns3server/modules/iou/iou_error.py create mode 100644 gns3server/modules/iou/iou_vm.py create mode 100644 tests/modules/iou/test_iou_manager.py create mode 100644 tests/modules/iou/test_iou_vm.py diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 4e2f51bb..25c1012f 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -18,5 +18,6 @@ from .vpcs import VPCS from .virtualbox import VirtualBox from .dynamips import Dynamips +from .iou import IOU -MODULES = [VPCS, VirtualBox, Dynamips] +MODULES = [VPCS, VirtualBox, Dynamips, IOU] diff --git a/gns3server/modules/adapters/ethernet_adapter.py b/gns3server/modules/adapters/ethernet_adapter.py index 9d3ee003..f1a06c63 100644 --- a/gns3server/modules/adapters/ethernet_adapter.py +++ b/gns3server/modules/adapters/ethernet_adapter.py @@ -29,4 +29,4 @@ class EthernetAdapter(Adapter): def __str__(self): - return "VPCS Ethernet adapter" + return "Ethernet adapter" diff --git a/gns3server/modules/adapters/serial_adapter.py b/gns3server/modules/adapters/serial_adapter.py new file mode 100644 index 00000000..5bb00dc1 --- /dev/null +++ b/gns3server/modules/adapters/serial_adapter.py @@ -0,0 +1,32 @@ +# -*- 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 SerialAdapter(Adapter): + + """ + VPCS Ethernet adapter. + """ + + def __init__(self): + Adapter.__init__(self, interfaces=1) + + def __str__(self): + + return "Serial adapter" diff --git a/gns3server/modules/dynamips/dynamips_error.py b/gns3server/modules/dynamips/dynamips_error.py index 58c306ee..15e20796 100644 --- a/gns3server/modules/dynamips/dynamips_error.py +++ b/gns3server/modules/dynamips/dynamips_error.py @@ -22,18 +22,4 @@ Custom exceptions for Dynamips module. class DynamipsError(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 + pass diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py new file mode 100644 index 00000000..c3ba15b4 --- /dev/null +++ b/gns3server/modules/iou/__init__.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 . + +""" +IOU server module. +""" + +import asyncio + +from ..base_manager import BaseManager +from .iou_error import IOUError +from .iou_vm import IOUVM + + +class IOU(BaseManager): + _VM_CLASS = IOUVM + + def __init__(self): + super().__init__() + self._free_application_ids = list(range(1, 512)) + self._used_application_ids = {} + + @asyncio.coroutine + def create_vm(self, *args, **kwargs): + + vm = yield from super().create_vm(*args, **kwargs) + try: + self._used_application_ids[vm.id] = self._free_application_ids.pop(0) + except IndexError: + raise IOUError("No mac address available") + return vm + + @asyncio.coroutine + def delete_vm(self, vm_id, *args, **kwargs): + + vm = self.get_vm(vm_id) + i = self._used_application_ids[vm_id] + self._free_application_ids.insert(0, i) + del self._used_application_ids[vm_id] + yield from super().delete_vm(vm_id, *args, **kwargs) + + def get_application_id(self, vm_id): + """ + Get an unique IOU mac id + + :param vm_id: ID of the IOU VM + :returns: IOU MAC id + """ + + return self._used_application_ids.get(vm_id, 1) diff --git a/gns3server/modules/iou/iou_error.py b/gns3server/modules/iou/iou_error.py new file mode 100644 index 00000000..cd43bdb9 --- /dev/null +++ b/gns3server/modules/iou/iou_error.py @@ -0,0 +1,26 @@ +# -*- 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 . + +""" +Custom exceptions for IOU module. +""" + +from ..vm_error import VMError + + +class IOUError(VMError): + pass diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py new file mode 100644 index 00000000..5abf0b44 --- /dev/null +++ b/gns3server/modules/iou/iou_vm.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 . + +""" +IOU VM management (creates command line, processes, files etc.) in +order to run an IOU instance. +""" + +import os +import sys +import subprocess +import signal +import re +import asyncio +import shutil + +from pkg_resources import parse_version +from .iou_error import IOUError +from ..adapters.ethernet_adapter import EthernetAdapter +from ..adapters.serial_adapter import SerialAdapter +from ..base_vm import BaseVM + + +import logging +log = logging.getLogger(__name__) + + +class IOUVM(BaseVM): + module_name = 'iou' + + """ + IOU vm implementation. + + :param name: name of this IOU vm + :param vm_id: IOU instance identifier + :param project: Project instance + :param manager: parent VM Manager + :param console: TCP console port + """ + + def __init__(self, name, vm_id, project, manager, console=None): + + super().__init__(name, vm_id, project, manager) + + self._console = console + self._command = [] + self._iouyap_process = None + self._iou_process = None + self._iou_stdout_file = "" + self._started = False + self._iou_path = None + self._iourc = None + self._ioucon_thread = None + + # IOU settings + self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces + self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces + self._slots = self._ethernet_adapters + self._serial_adapters + self._use_default_iou_values = True # for RAM & NVRAM values + self._nvram = 128 # Kilobytes + self._initial_config = "" + self._ram = 256 # Megabytes + self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). + + if self._console is not None: + self._console = self._manager.port_manager.reserve_console_port(self._console) + else: + self._console = self._manager.port_manager.get_free_console_port() + + def close(self): + + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = None + + @property + def iou_path(self): + """Path of the iou binary""" + + return self._iou_path + + @iou_path.setter + def iou_path(self, path): + """ + Path of the iou binary + + :params path: Path to the binary + """ + + self._iou_path = path + if not os.path.isfile(self._iou_path) or not os.path.exists(self._iou_path): + if os.path.islink(self._iou_path): + raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._iou_path, os.path.realpath(self._iou_path))) + else: + raise IOUError("IOU image '{}' is not accessible".format(self._iou_path)) + + try: + with open(self._iou_path, "rb") as f: + # read the first 7 bytes of the file. + elf_header_start = f.read(7) + except OSError as e: + raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._iou_path, e)) + + # IOU images must start with the ELF magic number, be 32-bit, little endian + # and have an ELF version of 1 normal IOS image are big endian! + if elf_header_start != b'\x7fELF\x01\x01\x01': + raise IOUError("'{}' is not a valid IOU image".format(self._iou_path)) + + if not os.access(self._iou_path, os.X_OK): + raise IOUError("IOU image '{}' is not executable".format(self._iou_path)) + + @property + def iourc(self): + """ + Returns the path to the iourc file. + :returns: path to the iourc file + """ + + return self._iourc + + @property + def use_default_iou_values(self): + """ + Returns if this device uses the default IOU image values. + :returns: boolean + """ + + return self._use_default_iou_values + + @use_default_iou_values.setter + def use_default_iou_values(self, state): + """ + Sets if this device uses the default IOU image values. + :param state: boolean + """ + + self._use_default_iou_values = state + if state: + log.info("IOU {name} [id={id}]: uses the default IOU image values".format(name=self._name, id=self._id)) + else: + log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) + + @iourc.setter + def iourc(self, iourc): + """ + Sets the path to the iourc file. + :param iourc: path to the iourc file. + """ + + self._iourc = iourc + log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, + id=self._id, + path=self._iourc)) + + def _check_requirements(self): + """ + Check if IOUYAP is available + """ + path = self.iouyap_path + if not path: + raise IOUError("No path to a IOU executable has been set") + + if not os.path.isfile(path): + raise IOUError("IOU program '{}' is not accessible".format(path)) + + if not os.access(path, os.X_OK): + raise IOUError("IOU program '{}' is not executable".format(path)) + + def __json__(self): + + return {"name": self.name, + "vm_id": self.id, + "console": self._console, + "project_id": self.project.id, + } + + @property + def iouyap_path(self): + """ + Returns the IOUYAP executable path. + + :returns: path to IOUYAP + """ + + path = self._manager.config.get_section_config("IOU").get("iouyap_path", "iouyap") + if path == "iouyap": + path = shutil.which("iouyap") + return path + + @property + def console(self): + """ + Returns the console port of this IOU vm. + + :returns: console port + """ + + return self._console + + @console.setter + def console(self, console): + """ + Change console port + + :params console: Console port (integer) + """ + + if console == self._console: + return + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(console) + + @property + def application_id(self): + return self._manager.get_application_id(self.id) + + #TODO: ASYNCIO + def _library_check(self): + """ + Checks for missing shared library dependencies in the IOU image. + """ + + try: + output = subprocess.check_output(["ldd", self._iou_path]) + except (FileNotFoundError, subprocess.SubprocessError) as e: + log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) + return + + p = re.compile("([\.\w]+)\s=>\s+not found") + missing_libs = p.findall(output.decode("utf-8")) + if missing_libs: + raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, + ", ".join(missing_libs))) + + @asyncio.coroutine + def start(self): + """ + Starts the IOU process. + """ + + self._check_requirements() + if not self.is_running(): + + # TODO: ASYNC + #self._library_check() + + if not self._iourc or not os.path.isfile(self._iourc): + raise IOUError("A valid iourc file is necessary to start IOU") + + iouyap_path = self.iouyap_path + if not iouyap_path or not os.path.isfile(iouyap_path): + raise IOUError("iouyap is necessary to start IOU") + + self._create_netmap_config() + # created a environment variable pointing to the iourc file. + env = os.environ.copy() + env["IOURC"] = self._iourc + self._command = self._build_command() + try: + log.info("Starting IOU: {}".format(self._command)) + self._iou_stdout_file = os.path.join(self.working_dir, "iou.log") + log.info("Logging to {}".format(self._iou_stdout_file)) + with open(self._iou_stdout_file, "w") as fd: + self._iou_process = yield from asyncio.create_subprocess_exec(self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir, + env=env) + log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid)) + self._started = True + except FileNotFoundError as e: + raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e)) + except (OSError, subprocess.SubprocessError) as e: + iou_stdout = self.read_iou_stdout() + log.error("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) + raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) + + # start console support + #self._start_ioucon() + # connections support + #self._start_iouyap() + + + @asyncio.coroutine + def stop(self): + """ + Stops the IOU process. + """ + + # stop console support + if self._ioucon_thread: + self._ioucon_thread_stop_event.set() + if self._ioucon_thread.is_alive(): + self._ioucon_thread.join(timeout=3.0) # wait for the thread to free the console port + self._ioucon_thread = None + + if self.is_running(): + self._terminate_process() + try: + yield from asyncio.wait_for(self._iou_process.wait(), timeout=3) + except asyncio.TimeoutError: + self._iou_process.kill() + if self._iou_process.returncode is None: + log.warn("IOU process {} is still running".format(self._iou_process.pid)) + + self._iou_process = None + self._started = False + + def _terminate_process(self): + """Terminate the process if running""" + + if self._iou_process: + log.info("Stopping IOU instance {} PID={}".format(self.name, self._iou_process.pid)) + try: + self._iou_process.terminate() + # Sometime the process can already be dead when we garbage collect + except ProcessLookupError: + pass + + @asyncio.coroutine + def reload(self): + """ + Reload the IOU process. (Stop / Start) + """ + + yield from self.stop() + yield from self.start() + + def is_running(self): + """ + Checks if the IOU process is running + + :returns: True or False + """ + + if self._iou_process: + return True + return False + + def _create_netmap_config(self): + """ + Creates the NETMAP file. + """ + + netmap_path = os.path.join(self.working_dir, "NETMAP") + try: + with open(netmap_path, "w") as f: + for bay in range(0, 16): + for unit in range(0, 4): + f.write("{iouyap_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format(iouyap_id=str(self.application_id + 512), + bay=bay, + unit=unit, + iou_id=self.application_id)) + log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name, + id=self._id)) + except OSError as e: + raise IOUError("Could not create {}: {}".format(netmap_path, e)) + + def _build_command(self): + """ + Command to start the IOU process. + (to be passed to subprocess.Popen()) + IOU command line: + Usage: [options] + : unix-js-m | unix-is-m | unix-i-m | ... + : instance identifier (0 < id <= 1024) + Options: + -e Number of Ethernet interfaces (default 2) + -s Number of Serial interfaces (default 2) + -n Size of nvram in Kb (default 64KB) + -b IOS debug string + -c Configuration file name + -d Generate debug information + -t Netio message trace + -q Suppress informational messages + -h Display this help + -C Turn off use of host clock + -m Megabytes of router memory (default 256MB) + -L Disable local console, use remote console + -l Enable Layer 1 keepalive messages + -u UDP port base for distributed networks + -R Ignore options from the IOURC file + -U Disable unix: file system location + -W Disable watchdog timer + -N Ignore the NETMAP file + """ + + command = [self._iou_path] + if len(self._ethernet_adapters) != 2: + command.extend(["-e", str(len(self._ethernet_adapters))]) + if len(self._serial_adapters) != 2: + command.extend(["-s", str(len(self._serial_adapters))]) + if not self.use_default_iou_values: + command.extend(["-n", str(self._nvram)]) + command.extend(["-m", str(self._ram)]) + command.extend(["-L"]) # disable local console, use remote console + if self._initial_config: + command.extend(["-c", self._initial_config]) + if self._l1_keepalives: + self._enable_l1_keepalives(command) + command.extend([str(self.application_id)]) + return command + + def read_iou_stdout(self): + """ + Reads the standard output of the IOU process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._iou_stdout_file: + try: + with open(self._iou_stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._iou_stdout_file, e)) + return output + + def read_iouyap_stdout(self): + """ + Reads the standard output of the iouyap process. + Only use when the process has been stopped or has crashed. + """ + + output = "" + if self._iouyap_stdout_file: + try: + with open(self._iouyap_stdout_file, errors="replace") as file: + output = file.read() + except OSError as e: + log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e)) + return output + + def _start_ioucon(self): + """ + Starts ioucon thread (for console connections). + """ + + if not self._ioucon_thread: + telnet_server = "{}:{}".format(self._console_host, self.console) + log.info("Starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) + args = argparse.Namespace(appl_id=str(self.application_id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) + self._ioucon_thread_stop_event = threading.Event() + self._ioucon_thread = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event)) + self._ioucon_thread.start() diff --git a/gns3server/modules/virtualbox/virtualbox_error.py b/gns3server/modules/virtualbox/virtualbox_error.py index ec05bfb6..df481c21 100644 --- a/gns3server/modules/virtualbox/virtualbox_error.py +++ b/gns3server/modules/virtualbox/virtualbox_error.py @@ -24,18 +24,4 @@ from ..vm_error import VMError class VirtualBoxError(VMError): - 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 + pass diff --git a/gns3server/modules/vm_error.py b/gns3server/modules/vm_error.py index d7b71e14..55cfc4cf 100644 --- a/gns3server/modules/vm_error.py +++ b/gns3server/modules/vm_error.py @@ -17,4 +17,19 @@ class VMError(Exception): - pass + + 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/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py index f32afdaa..b8e99d4b 100644 --- a/gns3server/modules/vpcs/vpcs_error.py +++ b/gns3server/modules/vpcs/vpcs_error.py @@ -24,18 +24,4 @@ from ..vm_error import VMError class VPCSError(VMError): - 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 + pass diff --git a/tests/modules/iou/test_iou_manager.py b/tests/modules/iou/test_iou_manager.py new file mode 100644 index 00000000..7817a297 --- /dev/null +++ b/tests/modules/iou/test_iou_manager.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 + + +from gns3server.modules.iou import IOU +from gns3server.modules.iou.iou_error import IOUError +from gns3server.modules.project_manager import ProjectManager + + +def test_get_application_id(loop, project, port_manager): + # Cleanup the IOU object + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + vm1_id = str(uuid.uuid4()) + vm2_id = str(uuid.uuid4()) + vm3_id = str(uuid.uuid4()) + loop.run_until_complete(iou.create_vm("PC 1", project.id, vm1_id)) + loop.run_until_complete(iou.create_vm("PC 2", project.id, vm2_id)) + assert iou.get_application_id(vm1_id) == 1 + assert iou.get_application_id(vm1_id) == 1 + assert iou.get_application_id(vm2_id) == 2 + loop.run_until_complete(iou.delete_vm(vm1_id)) + loop.run_until_complete(iou.create_vm("PC 3", project.id, vm3_id)) + assert iou.get_application_id(vm3_id) == 1 + + +def test_get_application_id_multiple_project(loop, port_manager): + # Cleanup the IOU object + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + vm1_id = str(uuid.uuid4()) + vm2_id = str(uuid.uuid4()) + vm3_id = str(uuid.uuid4()) + project1 = ProjectManager.instance().create_project() + project2 = ProjectManager.instance().create_project() + loop.run_until_complete(iou.create_vm("PC 1", project1.id, vm1_id)) + loop.run_until_complete(iou.create_vm("PC 2", project1.id, vm2_id)) + loop.run_until_complete(iou.create_vm("PC 2", project2.id, vm3_id)) + assert iou.get_application_id(vm1_id) == 1 + assert iou.get_application_id(vm2_id) == 2 + assert iou.get_application_id(vm3_id) == 3 + + +def test_get_application_id_no_id_available(loop, project, port_manager): + # Cleanup the IOU object + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + with pytest.raises(IOUError): + for i in range(1, 513): + vm_id = str(uuid.uuid4()) + loop.run_until_complete(iou.create_vm("PC {}".format(i), project.id, vm_id)) + assert iou.get_application_id(vm_id) == i diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py new file mode 100644 index 00000000..5f5d4093 --- /dev/null +++ b/tests/modules/iou/test_iou_vm.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 aiohttp +import asyncio +import os +import stat +from tests.utils import asyncio_patch + + +from unittest.mock import patch, MagicMock +from gns3server.modules.iou.iou_vm import IOUVM +from gns3server.modules.iou.iou_error import IOUError +from gns3server.modules.iou import IOU + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = IOU.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def vm(project, manager, tmpdir, fake_iou_bin): + fake_file = str(tmpdir / "iourc") + with open(fake_file, "w+") as f: + f.write("1") + + vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + config = manager.config.get_section_config("IOU") + config["iouyap_path"] = fake_file + manager.config.set_section_config("IOU", config) + + vm.iou_path = fake_iou_bin + vm.iourc = fake_file + return vm + + +@pytest.fixture +def fake_iou_bin(tmpdir): + """Create a fake IOU image on disk""" + + path = str(tmpdir / "iou.bin") + with open(path, "w+") as f: + f.write('\x7fELF\x01\x01\x01') + os.chmod(path, stat.S_IREAD | stat.S_IEXEC) + return path + + +def test_vm(project, manager): + vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager) + assert vm.name == "test" + assert vm.id == "00010203-0405-0607-0809-0a0b0c0d0e0f" + + +@patch("gns3server.config.Config.get_section_config", return_value={"iouyap_path": "/bin/test_fake"}) +def test_vm_invalid_iouyap_path(project, manager, loop): + with pytest.raises(IOUError): + vm = IOUVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager) + loop.run_until_complete(asyncio.async(vm.start())) + + +def test_start(loop, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + + +def test_stop(loop, vm): + process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() is False + process.terminate.assert_called_with() + + +def test_reload(loop, vm, fake_iou_bin): + process = MagicMock() + + # Wait process kill success + future = asyncio.Future() + future.set_result(True) + process.wait.return_value = future + + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.reload())) + assert vm.is_running() is True + process.terminate.assert_called_with() + + +def test_close(vm, port_manager): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + vm.start() + port = vm.console + vm.close() + # Raise an exception if the port is not free + port_manager.reserve_console_port(port) + assert vm.is_running() is False + + +def test_iou_path(vm, fake_iou_bin): + + vm.iou_path = fake_iou_bin + assert vm.iou_path == fake_iou_bin + + +def test_path_invalid_bin(vm, tmpdir): + + iou_path = str(tmpdir / "test.bin") + with pytest.raises(IOUError): + vm.iou_path = iou_path + + with open(iou_path, "w+") as f: + f.write("BUG") + + with pytest.raises(IOUError): + vm.iou_path = iou_path + + +def test_create_netmap_config(vm): + + vm._create_netmap_config() + netmap_path = os.path.join(vm.working_dir, "NETMAP") + + with open(netmap_path) as f: + content = f.read() + + assert "513:0/0 1:0/0" in content + assert "513:15/3 1:15/3" in content + + +def test_build_command(vm): + + assert vm._build_command() == [vm.iou_path, '-L', str(vm.application_id)] From 986c63f3446dae84802729996fc0e8ef006f18d5 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 15:37:05 +0100 Subject: [PATCH 02/11] HTTP api start iou process Now we need to start ioucon --- gns3server/handlers/__init__.py | 3 +- gns3server/handlers/iou_handler.py | 180 +++++++++++++++++++++++++++++ gns3server/modules/iou/iou_vm.py | 54 ++++----- gns3server/schemas/iou.py | 127 ++++++++++++++++++++ tests/api/test_iou.py | 97 ++++++++++++++++ 5 files changed, 433 insertions(+), 28 deletions(-) create mode 100644 gns3server/handlers/iou_handler.py create mode 100644 gns3server/schemas/iou.py create mode 100644 tests/api/test_iou.py diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 727b4053..78bb3ca6 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -3,4 +3,5 @@ __all__ = ["version_handler", "vpcs_handler", "project_handler", "virtualbox_handler", - "dynamips_handler"] + "dynamips_handler", + "iou_handler"] diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py new file mode 100644 index 00000000..70ccc6b6 --- /dev/null +++ b/gns3server/handlers/iou_handler.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 ..web.route import Route +from ..schemas.iou import IOU_CREATE_SCHEMA +from ..schemas.iou import IOU_UPDATE_SCHEMA +from ..schemas.iou import IOU_OBJECT_SCHEMA +from ..modules.iou import IOU + + +class IOUHandler: + + """ + API entry points for IOU. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new IOU instance", + input=IOU_CREATE_SCHEMA, + output=IOU_OBJECT_SCHEMA) + def create(request, response): + + iou = IOU.instance() + vm = yield from iou.create_vm(request.json["name"], + request.match_info["project_id"], + request.json.get("vm_id"), + console=request.json.get("console"), + ) + vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a IOU instance", + output=IOU_OBJECT_SCHEMA) + def show(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @classmethod + @Route.put( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a IOU instance", + input=IOU_UPDATE_SCHEMA, + output=IOU_OBJECT_SCHEMA) + def update(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.name = request.json.get("name", vm.name) + vm.console = request.json.get("console", vm.console) + vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/iou/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a IOU instance") + def delete(request, response): + + yield from IOU.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/start", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a IOU instance") + def start(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/stop", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a IOU instance") + def stop(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/reload", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a IOU instance") + def reload(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 5abf0b44..9651e210 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -63,7 +63,7 @@ class IOUVM(BaseVM): self._iou_stdout_file = "" self._started = False self._iou_path = None - self._iourc = None + self._iourc_path = None self._ioucon_thread = None # IOU settings @@ -124,13 +124,24 @@ class IOUVM(BaseVM): raise IOUError("IOU image '{}' is not executable".format(self._iou_path)) @property - def iourc(self): + def iourc_path(self): """ Returns the path to the iourc file. :returns: path to the iourc file """ - return self._iourc + return self._iourc_path + + @iourc_path.setter + def iourc_path(self, path): + """ + Set path to IOURC file + """ + + self._iourc_path = path + log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, + id=self._id, + path=self._iourc_path)) @property def use_default_iou_values(self): @@ -154,18 +165,6 @@ class IOUVM(BaseVM): else: log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) - @iourc.setter - def iourc(self, iourc): - """ - Sets the path to the iourc file. - :param iourc: path to the iourc file. - """ - - self._iourc = iourc - log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, - id=self._id, - path=self._iourc)) - def _check_requirements(self): """ Check if IOUYAP is available @@ -186,6 +185,8 @@ class IOUVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, + "iourc_path": self._iourc_path, + "iou_path": self.iou_path } @property @@ -229,7 +230,7 @@ class IOUVM(BaseVM): def application_id(self): return self._manager.get_application_id(self.id) - #TODO: ASYNCIO + # TODO: ASYNCIO def _library_check(self): """ Checks for missing shared library dependencies in the IOU image. @@ -257,9 +258,9 @@ class IOUVM(BaseVM): if not self.is_running(): # TODO: ASYNC - #self._library_check() + # self._library_check() - if not self._iourc or not os.path.isfile(self._iourc): + if not self._iourc_path or not os.path.isfile(self._iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") iouyap_path = self.iouyap_path @@ -269,18 +270,18 @@ class IOUVM(BaseVM): self._create_netmap_config() # created a environment variable pointing to the iourc file. env = os.environ.copy() - env["IOURC"] = self._iourc + env["IOURC"] = self._iourc_path self._command = self._build_command() try: log.info("Starting IOU: {}".format(self._command)) self._iou_stdout_file = os.path.join(self.working_dir, "iou.log") log.info("Logging to {}".format(self._iou_stdout_file)) with open(self._iou_stdout_file, "w") as fd: - self._iou_process = yield from asyncio.create_subprocess_exec(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self.working_dir, - env=env) + self._iou_process = yield from asyncio.create_subprocess_exec(*self._command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir, + env=env) log.info("IOU instance {} started PID={}".format(self._id, self._iou_process.pid)) self._started = True except FileNotFoundError as e: @@ -291,10 +292,9 @@ class IOUVM(BaseVM): raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) # start console support - #self._start_ioucon() + # self._start_ioucon() # connections support - #self._start_iouyap() - + # self._start_iouyap() @asyncio.coroutine def stop(self): diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py new file mode 100644 index 00000000..562ac0f7 --- /dev/null +++ b/gns3server/schemas/iou.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 . + + +IOU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "IOU VM identifier", + "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}$"}, + {"type": "integer"} # for legacy projects + ] + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, + "required": ["name", "iou_path", "iourc_path"] +} + +IOU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": ["string", "null"], + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": ["integer", "null"] + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, +} + +IOU_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "IOU instance", + "type": "object", + "properties": { + "name": { + "description": "IOU VM name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "IOU VM 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}$" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "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}$" + }, + "iou_path": { + "description": "Path of iou binary", + "type": "string" + }, + "iourc_path": { + "description": "Path of iourc", + "type": "string" + }, + }, + "additionalProperties": False, + "required": ["name", "vm_id", "console", "project_id", "iou_path", "iourc_path"] +} diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py new file mode 100644 index 00000000..5dd3268c --- /dev/null +++ b/tests/api/test_iou.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015 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 os +import stat +from tests.utils import asyncio_patch +from unittest.mock import patch + + +@pytest.fixture +def fake_iou_bin(tmpdir): + """Create a fake IOU image on disk""" + + path = str(tmpdir / "iou.bin") + with open(path, "w+") as f: + f.write('\x7fELF\x01\x01\x01') + os.chmod(path, stat.S_IREAD | stat.S_IEXEC) + return path + +@pytest.fixture +def base_params(tmpdir, fake_iou_bin): + """Return standard parameters""" + return {"name": "PC TEST 1", "iou_path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} + + +@pytest.fixture +def vm(server, project, base_params): + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params) + assert response.status == 201 + return response.json + + +def test_iou_create(server, project, base_params): + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params, example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_iou_get(server, project, vm): + response = server.get("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 200 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_iou_start(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.start", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_stop(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.stop", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_reload(server, vm): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM.reload", return_value=True) as mock: + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_delete(server, vm): + with asyncio_patch("gns3server.modules.iou.IOU.delete_vm", return_value=True) as mock: + response = server.delete("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_iou_update(server, vm, tmpdir, free_console_port): + response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", + "console": free_console_port}) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port From faa7472670c62e5e314047025f4b2943dd854094 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 15:57:02 +0100 Subject: [PATCH 03/11] IOUCON start when vm start --- gns3server/handlers/iou_handler.py | 8 ++++---- gns3server/modules/iou/iou_vm.py | 11 +++++++++-- gns3server/{old_modules => modules}/iou/ioucon.py | 4 ++-- tests/api/test_iou.py | 3 ++- 4 files changed, 17 insertions(+), 9 deletions(-) rename gns3server/{old_modules => modules}/iou/ioucon.py (99%) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index 70ccc6b6..a606efff 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -46,10 +46,10 @@ class IOUHandler: iou = IOU.instance() vm = yield from iou.create_vm(request.json["name"], - request.match_info["project_id"], - request.json.get("vm_id"), - console=request.json.get("console"), - ) + request.match_info["project_id"], + request.json.get("vm_id"), + console=request.json.get("console"), + ) vm.iou_path = request.json.get("iou_path", vm.iou_path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) response.set_status(201) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 9651e210..d173f973 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -27,12 +27,15 @@ import signal import re import asyncio import shutil +import argparse +import threading from pkg_resources import parse_version from .iou_error import IOUError from ..adapters.ethernet_adapter import EthernetAdapter from ..adapters.serial_adapter import SerialAdapter from ..base_vm import BaseVM +from .ioucon import start_ioucon import logging @@ -50,9 +53,12 @@ class IOUVM(BaseVM): :param project: Project instance :param manager: parent VM Manager :param console: TCP console port + :params console_host: TCP console host IP """ - def __init__(self, name, vm_id, project, manager, console=None): + def __init__(self, name, vm_id, project, manager, + console=None, + console_host="0.0.0.0"): super().__init__(name, vm_id, project, manager) @@ -65,6 +71,7 @@ class IOUVM(BaseVM): self._iou_path = None self._iourc_path = None self._ioucon_thread = None + self._console_host = console_host # IOU settings self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces @@ -292,7 +299,7 @@ class IOUVM(BaseVM): raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) # start console support - # self._start_ioucon() + self._start_ioucon() # connections support # self._start_iouyap() diff --git a/gns3server/old_modules/iou/ioucon.py b/gns3server/modules/iou/ioucon.py similarity index 99% rename from gns3server/old_modules/iou/ioucon.py rename to gns3server/modules/iou/ioucon.py index 9a0e980e..6dbd782d 100644 --- a/gns3server/old_modules/iou/ioucon.py +++ b/gns3server/modules/iou/ioucon.py @@ -55,7 +55,7 @@ EXIT_ABORT = 2 # Mostly from: # https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py -#--[ Telnet Commands ]--------------------------------------------------------- +# --[ Telnet Commands ]--------------------------------------------------------- SE = 240 # End of sub-negotiation parameters NOP = 241 # No operation DATMK = 242 # Data stream portion of a sync. @@ -74,7 +74,7 @@ DONT = 254 # Don't = Demand or confirm option halt IAC = 255 # Interpret as Command SEND = 1 # Sub-process negotiation SEND command IS = 0 # Sub-process negotiation IS command -#--[ Telnet Options ]---------------------------------------------------------- +# --[ Telnet Options ]---------------------------------------------------------- BINARY = 0 # Transmit Binary ECHO = 1 # Echo characters back to sender RECON = 2 # Reconnection diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 5dd3268c..2e0a6fb7 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -32,6 +32,7 @@ def fake_iou_bin(tmpdir): os.chmod(path, stat.S_IREAD | stat.S_IEXEC) return path + @pytest.fixture def base_params(tmpdir, fake_iou_bin): """Return standard parameters""" @@ -91,7 +92,7 @@ def test_iou_delete(server, vm): def test_iou_update(server, vm, tmpdir, free_console_port): response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", - "console": free_console_port}) + "console": free_console_port}) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port From fb69c693f6668055242633e68bdd24041e5c19a0 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Wed, 11 Feb 2015 17:11:18 +0100 Subject: [PATCH 04/11] Start iouyap --- gns3server/modules/iou/iou_vm.py | 111 ++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index d173f973..f243f418 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -29,6 +29,7 @@ import asyncio import shutil import argparse import threading +import configparser from pkg_resources import parse_version from .iou_error import IOUError @@ -301,7 +302,91 @@ class IOUVM(BaseVM): # start console support self._start_ioucon() # connections support - # self._start_iouyap() + self._start_iouyap() + + def _start_iouyap(self): + """ + Starts iouyap (handles connections to and from this IOU device). + """ + + try: + self._update_iouyap_config() + command = [self.iouyap_path, "-q", str(self.application_id + 512)] # iouyap has always IOU ID + 512 + log.info("starting iouyap: {}".format(command)) + self._iouyap_stdout_file = os.path.join(self.working_dir, "iouyap.log") + log.info("logging to {}".format(self._iouyap_stdout_file)) + with open(self._iouyap_stdout_file, "w") as fd: + self._iouyap_process = subprocess.Popen(command, + stdout=fd, + stderr=subprocess.STDOUT, + cwd=self.working_dir) + + log.info("iouyap started PID={}".format(self._iouyap_process.pid)) + except (OSError, subprocess.SubprocessError) as e: + iouyap_stdout = self.read_iouyap_stdout() + log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout)) + raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout)) + + def _update_iouyap_config(self): + """ + Updates the iouyap.ini file. + """ + + iouyap_ini = os.path.join(self.working_dir, "iouyap.ini") + + config = configparser.ConfigParser() + config["default"] = {"netmap": "NETMAP", + "base_port": "49000"} + + bay_id = 0 + for adapter in self._slots: + unit_id = 0 + for unit in adapter.ports.keys(): + nio = adapter.get_nio(unit) + if nio: + connection = None + if isinstance(nio, NIO_UDP): + # UDP tunnel + connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport, + rhost=nio.rhost, + rport=nio.rport)} + elif isinstance(nio, NIO_TAP): + # TAP interface + connection = {"tap_dev": "{tap_device}".format(tap_device=nio.tap_device)} + + elif isinstance(nio, NIO_GenericEthernet): + # Ethernet interface + connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)} + + if connection: + interface = "{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self.application_id + 512), bay=bay_id, unit=unit_id) + config[interface] = connection + + if nio.capturing: + pcap_data_link_type = nio.pcap_data_link_type.upper() + if pcap_data_link_type == "DLT_PPP_SERIAL": + pcap_protocol = "ppp" + elif pcap_data_link_type == "DLT_C_HDLC": + pcap_protocol = "hdlc" + elif pcap_data_link_type == "DLT_FRELAY": + pcap_protocol = "fr" + else: + pcap_protocol = "ethernet" + capture_info = {"pcap_file": "{pcap_file}".format(pcap_file=nio.pcap_output_file), + "pcap_protocol": pcap_protocol, + "pcap_overwrite": "y"} + config[interface].update(capture_info) + + unit_id += 1 + bay_id += 1 + + try: + with open(iouyap_ini, "w") as config_file: + config.write(config_file) + log.info("IOU {name} [id={id}]: iouyap.ini updated".format(name=self._name, + id=self._id)) + except OSError as e: + raise IOUError("Could not create {}: {}".format(iouyap_ini, e)) @asyncio.coroutine def stop(self): @@ -317,7 +402,7 @@ class IOUVM(BaseVM): self._ioucon_thread = None if self.is_running(): - self._terminate_process() + self._terminate_process_iou() try: yield from asyncio.wait_for(self._iou_process.wait(), timeout=3) except asyncio.TimeoutError: @@ -326,9 +411,29 @@ class IOUVM(BaseVM): log.warn("IOU process {} is still running".format(self._iou_process.pid)) self._iou_process = None + + self._terminate_process_iouyap() + try: + yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) + except asyncio.TimeoutError: + self._iou_process.kill() + if self._iouyap_process.returncode is None: + log.warn("IOUYAP process {} is still running".format(self._iou_process.pid)) + self._started = False - def _terminate_process(self): + def _terminate_process_iouyap(self): + """Terminate the process if running""" + + if self._iou_process: + log.info("Stopping IOUYAP instance {} PID={}".format(self.name, self._iouyap_process.pid)) + try: + self._iouyap_process.terminate() + # Sometime the process can already be dead when we garbage collect + except ProcessLookupError: + pass + + def _terminate_process_iou(self): """Terminate the process if running""" if self._iou_process: From ebc214d6fa8d4cf7f5d13d799851f62b8787a426 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 15:20:47 +0100 Subject: [PATCH 05/11] Fix tests and rename path to iou_path --- gns3server/handlers/iou_handler.py | 4 +-- gns3server/modules/iou/iou_vm.py | 55 ++++++++++++++-------------- gns3server/schemas/iou.py | 10 +++--- tests/api/test_iou.py | 2 +- tests/modules/iou/test_iou_vm.py | 58 ++++++++++++++++-------------- 5 files changed, 68 insertions(+), 61 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index a606efff..ab39622f 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -50,7 +50,7 @@ class IOUHandler: request.json.get("vm_id"), console=request.json.get("console"), ) - vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) response.set_status(201) response.json(vm) @@ -97,7 +97,7 @@ class IOUHandler: vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) vm.name = request.json.get("name", vm.name) vm.console = request.json.get("console", vm.console) - vm.iou_path = request.json.get("iou_path", vm.iou_path) + vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) response.json(vm) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index f243f418..8beb127c 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -69,7 +69,7 @@ class IOUVM(BaseVM): self._iou_process = None self._iou_stdout_file = "" self._started = False - self._iou_path = None + self._path = None self._iourc_path = None self._ioucon_thread = None self._console_host = console_host @@ -96,40 +96,40 @@ class IOUVM(BaseVM): self._console = None @property - def iou_path(self): + def path(self): """Path of the iou binary""" - return self._iou_path + return self._path - @iou_path.setter - def iou_path(self, path): + @path.setter + def path(self, path): """ Path of the iou binary :params path: Path to the binary """ - self._iou_path = path - if not os.path.isfile(self._iou_path) or not os.path.exists(self._iou_path): - if os.path.islink(self._iou_path): - raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._iou_path, os.path.realpath(self._iou_path))) + self._path = path + if not os.path.isfile(self._path) or not os.path.exists(self._path): + if os.path.islink(self._path): + raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path))) else: - raise IOUError("IOU image '{}' is not accessible".format(self._iou_path)) + raise IOUError("IOU image '{}' is not accessible".format(self._path)) try: - with open(self._iou_path, "rb") as f: + with open(self._path, "rb") as f: # read the first 7 bytes of the file. elf_header_start = f.read(7) except OSError as e: - raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._iou_path, e)) + raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e)) # IOU images must start with the ELF magic number, be 32-bit, little endian # and have an ELF version of 1 normal IOS image are big endian! if elf_header_start != b'\x7fELF\x01\x01\x01': - raise IOUError("'{}' is not a valid IOU image".format(self._iou_path)) + raise IOUError("'{}' is not a valid IOU image".format(self._path)) - if not os.access(self._iou_path, os.X_OK): - raise IOUError("IOU image '{}' is not executable".format(self._iou_path)) + if not os.access(self._path, os.X_OK): + raise IOUError("IOU image '{}' is not executable".format(self._path)) @property def iourc_path(self): @@ -194,7 +194,7 @@ class IOUVM(BaseVM): "console": self._console, "project_id": self.project.id, "iourc_path": self._iourc_path, - "iou_path": self.iou_path + "path": self.path } @property @@ -245,7 +245,7 @@ class IOUVM(BaseVM): """ try: - output = subprocess.check_output(["ldd", self._iou_path]) + output = subprocess.check_output(["ldd", self._path]) except (FileNotFoundError, subprocess.SubprocessError) as e: log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) return @@ -296,8 +296,8 @@ class IOUVM(BaseVM): raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e)) except (OSError, subprocess.SubprocessError) as e: iou_stdout = self.read_iou_stdout() - log.error("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) - raise IOUError("could not start IOU {}: {}\n{}".format(self._iou_path, e, iou_stdout)) + log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) + raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) # start console support self._start_ioucon() @@ -412,13 +412,14 @@ class IOUVM(BaseVM): self._iou_process = None - self._terminate_process_iouyap() - try: - yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) - except asyncio.TimeoutError: - self._iou_process.kill() - if self._iouyap_process.returncode is None: - log.warn("IOUYAP process {} is still running".format(self._iou_process.pid)) + if self._iouyap_process is not None: + self._terminate_process_iouyap() + try: + yield from asyncio.wait_for(self._iouyap_process.wait(), timeout=3) + except asyncio.TimeoutError: + self._iou_process.kill() + if self._iouyap_process.returncode is None: + log.warn("IOUYAP process {} is still running".format(self._iou_process.pid)) self._started = False @@ -512,7 +513,7 @@ class IOUVM(BaseVM): -N Ignore the NETMAP file """ - command = [self._iou_path] + command = [self._path] if len(self._ethernet_adapters) != 2: command.extend(["-e", str(len(self._ethernet_adapters))]) if len(self._serial_adapters) != 2: diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 562ac0f7..5c3434b6 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -42,7 +42,7 @@ IOU_CREATE_SCHEMA = { "maximum": 65535, "type": ["integer", "null"] }, - "iou_path": { + "path": { "description": "Path of iou binary", "type": "string" }, @@ -52,7 +52,7 @@ IOU_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "iou_path", "iourc_path"] + "required": ["name", "path", "iourc_path"] } IOU_UPDATE_SCHEMA = { @@ -71,7 +71,7 @@ IOU_UPDATE_SCHEMA = { "maximum": 65535, "type": ["integer", "null"] }, - "iou_path": { + "path": { "description": "Path of iou binary", "type": "string" }, @@ -113,7 +113,7 @@ IOU_OBJECT_SCHEMA = { "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}$" }, - "iou_path": { + "path": { "description": "Path of iou binary", "type": "string" }, @@ -123,5 +123,5 @@ IOU_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "iou_path", "iourc_path"] + "required": ["name", "vm_id", "console", "project_id", "path", "iourc_path"] } diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 2e0a6fb7..75188cb3 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -36,7 +36,7 @@ def fake_iou_bin(tmpdir): @pytest.fixture def base_params(tmpdir, fake_iou_bin): """Return standard parameters""" - return {"name": "PC TEST 1", "iou_path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} + return {"name": "PC TEST 1", "path": fake_iou_bin, "iourc_path": str(tmpdir / "iourc")} @pytest.fixture diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 5f5d4093..8f8a0181 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -47,8 +47,8 @@ def vm(project, manager, tmpdir, fake_iou_bin): config["iouyap_path"] = fake_file manager.config.set_section_config("IOU", config) - vm.iou_path = fake_iou_bin - vm.iourc = fake_file + vm.path = fake_iou_bin + vm.iourc_path = fake_file return vm @@ -76,11 +76,13 @@ def test_vm_invalid_iouyap_path(project, manager, loop): loop.run_until_complete(asyncio.async(vm.start())) -def test_start(loop, vm): +def test_start(loop, vm, monkeypatch): with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() def test_stop(loop, vm): @@ -92,12 +94,14 @@ def test_stop(loop, vm): process.wait.return_value = future with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() - loop.run_until_complete(asyncio.async(vm.stop())) - assert vm.is_running() is False - process.terminate.assert_called_with() + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.stop())) + assert vm.is_running() is False + process.terminate.assert_called_with() def test_reload(loop, vm, fake_iou_bin): @@ -109,12 +113,14 @@ def test_reload(loop, vm, fake_iou_bin): process.wait.return_value = future with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._check_requirements", return_value=True): - with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): - loop.run_until_complete(asyncio.async(vm.start())) - assert vm.is_running() - loop.run_until_complete(asyncio.async(vm.reload())) - assert vm.is_running() is True - process.terminate.assert_called_with() + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_ioucon", return_value=True): + with asyncio_patch("gns3server.modules.iou.iou_vm.IOUVM._start_iouyap", return_value=True): + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + loop.run_until_complete(asyncio.async(vm.start())) + assert vm.is_running() + loop.run_until_complete(asyncio.async(vm.reload())) + assert vm.is_running() is True + process.terminate.assert_called_with() def test_close(vm, port_manager): @@ -128,23 +134,23 @@ def test_close(vm, port_manager): assert vm.is_running() is False -def test_iou_path(vm, fake_iou_bin): +def test_path(vm, fake_iou_bin): - vm.iou_path = fake_iou_bin - assert vm.iou_path == fake_iou_bin + vm.path = fake_iou_bin + assert vm.path == fake_iou_bin def test_path_invalid_bin(vm, tmpdir): - iou_path = str(tmpdir / "test.bin") + path = str(tmpdir / "test.bin") with pytest.raises(IOUError): - vm.iou_path = iou_path + vm.path = path - with open(iou_path, "w+") as f: + with open(path, "w+") as f: f.write("BUG") with pytest.raises(IOUError): - vm.iou_path = iou_path + vm.path = path def test_create_netmap_config(vm): @@ -161,4 +167,4 @@ def test_create_netmap_config(vm): def test_build_command(vm): - assert vm._build_command() == [vm.iou_path, '-L', str(vm.application_id)] + assert vm._build_command() == [vm.path, '-L', str(vm.application_id)] From 4689024b50c98e98a31998c43ac765c5813af938 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 16:25:55 +0100 Subject: [PATCH 06/11] Add a --live options to control livereload Because the livereload bug due to timezone issues with Vagrant --- gns3server/main.py | 5 ++++- gns3server/server.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 4810fd68..991f8cce 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -91,6 +91,7 @@ def parse_arguments(argv, config): "allow": config.getboolean("allow_remote_console", False), "quiet": config.getboolean("quiet", False), "debug": config.getboolean("debug", False), + "live": config.getboolean("live", False), } parser = argparse.ArgumentParser(description="GNS3 server version {}".format(__version__)) @@ -104,7 +105,9 @@ def parse_arguments(argv, config): parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)") parser.add_argument("-A", "--allow", action="store_true", help="allow remote connections to local console ports") parser.add_argument("-q", "--quiet", action="store_true", help="do not show logs on stdout") - parser.add_argument("-d", "--debug", action="store_true", help="show debug logs and enable code live reload") + parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") + parser.add_argument("--live", action="store_true", help="enable code live reload") + return parser.parse_args(argv) diff --git a/gns3server/server.py b/gns3server/server.py index 349880e5..baf7e1a9 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -173,7 +173,7 @@ class Server: self._loop.run_until_complete(self._run_application(app, ssl_context)) self._signal_handling() - if server_config.getboolean("debug"): + if server_config.getboolean("live"): log.info("Code live reload is enabled, watching for file changes") self._loop.call_later(1, self._reload_hook) self._loop.run_forever() From 05df7001a393327d8bfc7345f2011ff46f32a1b2 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 17:03:01 +0100 Subject: [PATCH 07/11] Successfully create an iou device from the GUI via HTTP --- gns3server/main.py | 1 - gns3server/modules/iou/iou_vm.py | 6 +++--- gns3server/schemas/iou.py | 24 ++++++++++++++++-------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/gns3server/main.py b/gns3server/main.py index 991f8cce..f25ce6d9 100644 --- a/gns3server/main.py +++ b/gns3server/main.py @@ -108,7 +108,6 @@ def parse_arguments(argv, config): parser.add_argument("-d", "--debug", action="store_true", help="show debug logs") parser.add_argument("--live", action="store_true", help="enable code live reload") - return parser.parse_args(argv) diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 8beb127c..11481c7a 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -193,7 +193,6 @@ class IOUVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, - "iourc_path": self._iourc_path, "path": self.path } @@ -268,7 +267,7 @@ class IOUVM(BaseVM): # TODO: ASYNC # self._library_check() - if not self._iourc_path or not os.path.isfile(self._iourc_path): + if self._iourc_path and not os.path.isfile(self._iourc_path): raise IOUError("A valid iourc file is necessary to start IOU") iouyap_path = self.iouyap_path @@ -278,7 +277,8 @@ class IOUVM(BaseVM): self._create_netmap_config() # created a environment variable pointing to the iourc file. env = os.environ.copy() - env["IOURC"] = self._iourc_path + if self._iourc_path: + env["IOURC"] = self._iourc_path self._command = self._build_command() try: log.info("Starting IOU: {}".format(self._command)) diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 5c3434b6..bc4f1ad4 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -52,7 +52,7 @@ IOU_CREATE_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "path", "iourc_path"] + "required": ["name", "path"] } IOU_UPDATE_SCHEMA = { @@ -73,12 +73,24 @@ IOU_UPDATE_SCHEMA = { }, "path": { "description": "Path of iou binary", - "type": "string" + "type": ["string", "null"] }, "iourc_path": { "description": "Path of iourc", - "type": "string" + "type": ["string", "null"] }, + "initial_config": { + "description": "Initial configuration path", + "type": ["string", "null"] + }, + "serial_adapters": { + "description": "How many serial adapters are connected to the IOU", + "type": ["integer", "null"] + }, + "ethernet_adapters": { + "description": "How many ethernet adapters are connected to the IOU", + "type": ["integer", "null"] + } }, "additionalProperties": False, } @@ -117,11 +129,7 @@ IOU_OBJECT_SCHEMA = { "description": "Path of iou binary", "type": "string" }, - "iourc_path": { - "description": "Path of iourc", - "type": "string" - }, }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path", "iourc_path"] + "required": ["name", "vm_id", "console", "project_id", "path"] } From 8b61aa9ae7ca3a078a04b19c5dc0a35d495e4d75 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 21:02:52 +0100 Subject: [PATCH 08/11] Set ram, ethernet adapters, serial adapters --- gns3server/handlers/iou_handler.py | 9 ++ gns3server/modules/iou/iou_vm.py | 130 +++++++++++++++++++++++++++-- gns3server/schemas/iou.py | 42 +++++++++- tests/api/test_iou.py | 43 +++++++++- 4 files changed, 213 insertions(+), 11 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index ab39622f..d73a0fb1 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -49,6 +49,10 @@ class IOUHandler: request.match_info["project_id"], request.json.get("vm_id"), console=request.json.get("console"), + serial_adapters=request.json.get("serial_adapters"), + ethernet_adapters=request.json.get("ethernet_adapters"), + ram=request.json.get("ram"), + nvram=request.json.get("nvram") ) vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) @@ -99,6 +103,11 @@ class IOUHandler: vm.console = request.json.get("console", vm.console) vm.path = request.json.get("path", vm.path) vm.iourc_path = request.json.get("iourc_path", vm.iourc_path) + vm.ethernet_adapters = request.json.get("ethernet_adapters", vm.ethernet_adapters) + vm.serial_adapters = request.json.get("serial_adapters", vm.serial_adapters) + vm.ram = request.json.get("ram", vm.ram) + vm.nvram = request.json.get("nvram", vm.nvram) + response.json(vm) @classmethod diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 11481c7a..6fb7db94 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -55,11 +55,19 @@ class IOUVM(BaseVM): :param manager: parent VM Manager :param console: TCP console port :params console_host: TCP console host IP + :params ethernet_adapters: Number of ethernet adapters + :params serial_adapters: Number of serial adapters + :params ram: Ram MB + :params nvram: Nvram KB """ def __init__(self, name, vm_id, project, manager, console=None, - console_host="0.0.0.0"): + console_host="0.0.0.0", + ram=None, + nvram=None, + ethernet_adapters=None, + serial_adapters=None): super().__init__(name, vm_id, project, manager) @@ -75,13 +83,14 @@ class IOUVM(BaseVM): self._console_host = console_host # IOU settings - self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces - self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces - self._slots = self._ethernet_adapters + self._serial_adapters + self._ethernet_adapters = [] + self._serial_adapters = [] + self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces + self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces self._use_default_iou_values = True # for RAM & NVRAM values - self._nvram = 128 # Kilobytes + self._nvram = 128 if nvram is None else nvram # Kilobytes self._initial_config = "" - self._ram = 256 # Megabytes + self._ram = 256 if ram is None else ram # Megabytes self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). if self._console is not None: @@ -193,7 +202,11 @@ class IOUVM(BaseVM): "vm_id": self.id, "console": self._console, "project_id": self.project.id, - "path": self.path + "path": self.path, + "ethernet_adapters": len(self._ethernet_adapters), + "serial_adapters": len(self._serial_adapters), + "ram": self._ram, + "nvram": self._nvram } @property @@ -233,6 +246,57 @@ class IOUVM(BaseVM): self._manager.port_manager.release_console_port(self._console) self._console = self._manager.port_manager.reserve_console_port(console) + @property + def ram(self): + """ + Returns the amount of RAM allocated to this IOU instance. + :returns: amount of RAM in Mbytes (integer) + """ + + return self._ram + + @ram.setter + def ram(self, ram): + """ + Sets amount of RAM allocated to this IOU instance. + :param ram: amount of RAM in Mbytes (integer) + """ + + if self._ram == ram: + return + + log.info("IOU {name} [id={id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, + id=self._id, + old_ram=self._ram, + new_ram=ram)) + + self._ram = ram + + @property + def nvram(self): + """ + Returns the mount of NVRAM allocated to this IOU instance. + :returns: amount of NVRAM in Kbytes (integer) + """ + + return self._nvram + + @nvram.setter + def nvram(self, nvram): + """ + Sets amount of NVRAM allocated to this IOU instance. + :param nvram: amount of NVRAM in Kbytes (integer) + """ + + if self._nvram == nvram: + return + + log.info("IOU {name} [id={id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, + id=self._id, + old_nvram=self._nvram, + new_nvram=nvram)) + self._nvram = nvram + @property def application_id(self): return self._manager.get_application_id(self.id) @@ -571,3 +635,55 @@ class IOUVM(BaseVM): self._ioucon_thread_stop_event = threading.Event() self._ioucon_thread = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event)) self._ioucon_thread.start() + + @property + def ethernet_adapters(self): + """ + Returns the number of Ethernet adapters for this IOU instance. + :returns: number of adapters + """ + + return len(self._ethernet_adapters) + + @ethernet_adapters.setter + def ethernet_adapters(self, ethernet_adapters): + """ + Sets the number of Ethernet adapters for this IOU instance. + :param ethernet_adapters: number of adapters + """ + + self._ethernet_adapters.clear() + for _ in range(0, ethernet_adapters): + self._ethernet_adapters.append(EthernetAdapter()) + + log.info("IOU {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=len(self._ethernet_adapters))) + + self._slots = self._ethernet_adapters + self._serial_adapters + + @property + def serial_adapters(self): + """ + Returns the number of Serial adapters for this IOU instance. + :returns: number of adapters + """ + + return len(self._serial_adapters) + + @serial_adapters.setter + def serial_adapters(self, serial_adapters): + """ + Sets the number of Serial adapters for this IOU instance. + :param serial_adapters: number of adapters + """ + + self._serial_adapters.clear() + for _ in range(0, serial_adapters): + self._serial_adapters.append(SerialAdapter()) + + log.info("IOU {name} [id={id}]: number of Serial adapters changed to {adapters}".format(name=self._name, + id=self._id, + adapters=len(self._serial_adapters))) + + self._slots = self._ethernet_adapters + self._serial_adapters diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index bc4f1ad4..387874cf 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -50,6 +50,22 @@ IOU_CREATE_SCHEMA = { "description": "Path of iourc", "type": "string" }, + "serial_adapters": { + "description": "How many serial adapters are connected to the IOU", + "type": "integer" + }, + "ethernet_adapters": { + "description": "How many ethernet adapters are connected to the IOU", + "type": "integer" + }, + "ram": { + "description": "Allocated RAM MB", + "type": ["integer", "null"] + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": ["integer", "null"] + } }, "additionalProperties": False, "required": ["name", "path"] @@ -90,6 +106,14 @@ IOU_UPDATE_SCHEMA = { "ethernet_adapters": { "description": "How many ethernet adapters are connected to the IOU", "type": ["integer", "null"] + }, + "ram": { + "description": "Allocated RAM MB", + "type": ["integer", "null"] + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": ["integer", "null"] } }, "additionalProperties": False, @@ -129,7 +153,23 @@ IOU_OBJECT_SCHEMA = { "description": "Path of iou binary", "type": "string" }, + "serial_adapters": { + "description": "How many serial adapters are connected to the IOU", + "type": "integer" + }, + "ethernet_adapters": { + "description": "How many ethernet adapters are connected to the IOU", + "type": "integer" + }, + "ram": { + "description": "Allocated RAM MB", + "type": "integer" + }, + "nvram": { + "description": "Allocated NVRAM KB", + "type": "integer" + } }, "additionalProperties": False, - "required": ["name", "vm_id", "console", "project_id", "path"] + "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram"] } diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 75188cb3..6ae3f715 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -47,11 +47,33 @@ def vm(server, project, base_params): def test_iou_create(server, project, base_params): - response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params, example=True) + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), base_params) assert response.status == 201 assert response.route == "/projects/{project_id}/iou/vms" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id + assert response.json["serial_adapters"] == 2 + assert response.json["ethernet_adapters"] == 2 + assert response.json["ram"] == 256 + assert response.json["nvram"] == 128 + + +def test_iou_create_with_params(server, project, base_params): + params = base_params + params["ram"] = 1024 + params["nvram"] = 512 + params["serial_adapters"] = 4 + params["ethernet_adapters"] = 0 + + response = server.post("/projects/{project_id}/iou/vms".format(project_id=project.id), params, example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + assert response.json["serial_adapters"] == 4 + assert response.json["ethernet_adapters"] == 0 + assert response.json["ram"] == 1024 + assert response.json["nvram"] == 512 def test_iou_get(server, project, vm): @@ -60,6 +82,10 @@ def test_iou_get(server, project, vm): assert response.route == "/projects/{project_id}/iou/vms/{vm_id}" assert response.json["name"] == "PC TEST 1" assert response.json["project_id"] == project.id + assert response.json["serial_adapters"] == 2 + assert response.json["ethernet_adapters"] == 2 + assert response.json["ram"] == 256 + assert response.json["nvram"] == 128 def test_iou_start(server, vm): @@ -91,8 +117,19 @@ def test_iou_delete(server, vm): def test_iou_update(server, vm, tmpdir, free_console_port): - response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"name": "test", - "console": free_console_port}) + params = { + "name": "test", + "console": free_console_port, + "ram": 512, + "nvram": 2048, + "ethernet_adapters": 4, + "serial_adapters": 0 + } + response = server.put("/projects/{project_id}/iou/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) assert response.status == 200 assert response.json["name"] == "test" assert response.json["console"] == free_console_port + assert response.json["ethernet_adapters"] == 4 + assert response.json["serial_adapters"] == 0 + assert response.json["ram"] == 512 + assert response.json["nvram"] == 2048 From 3471b03ef92ef1f7ac84ba9631ccc603c5838c26 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 21:39:24 +0100 Subject: [PATCH 09/11] Clarify JSON schema validation errors --- gns3server/web/response.py | 4 +--- gns3server/web/route.py | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/gns3server/web/response.py b/gns3server/web/response.py index 9ae3c1a6..9241d0c9 100644 --- a/gns3server/web/response.py +++ b/gns3server/web/response.py @@ -62,8 +62,6 @@ class Response(aiohttp.web.Response): try: jsonschema.validate(answer, self._output_schema) except jsonschema.ValidationError as e: - log.error("Invalid output schema {} '{}' in schema: {}".format(e.validator, - e.validator_value, - json.dumps(e.schema))) + log.error("Invalid output query. JSON schema error: {}".format(e.message)) raise aiohttp.web.HTTPBadRequest(text="{}".format(e)) self.body = json.dumps(answer, indent=4, sort_keys=True).encode('utf-8') diff --git a/gns3server/web/route.py b/gns3server/web/route.py index 10c28205..d63701fc 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -28,7 +28,6 @@ log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response - @asyncio.coroutine def parse_request(request, input_schema): """Parse body of request and raise HTTP errors in case of problems""" @@ -42,9 +41,7 @@ def parse_request(request, input_schema): try: jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: - log.error("Invalid input schema {} '{}' in schema: {}".format(e.validator, - e.validator_value, - json.dumps(e.schema))) + log.error("Invalid input query. JSON schema error: {}".format(e.message)) raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( e.validator, e.validator_value, From 9160d3caf46722d6449e832274aedcdb5852aed9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 21:44:43 +0100 Subject: [PATCH 10/11] Remove old directories to avoid editing them by mistake... --- gns3server/old_modules/iou/__init__.py | 843 ----------- .../old_modules/iou/adapters/__init__.py | 0 .../old_modules/iou/adapters/adapter.py | 105 -- .../iou/adapters/ethernet_adapter.py | 32 - .../iou/adapters/serial_adapter.py | 32 - gns3server/old_modules/iou/iou_device.py | 1069 -------------- gns3server/old_modules/iou/iou_error.py | 39 - gns3server/old_modules/iou/nios/__init__.py | 0 gns3server/old_modules/iou/nios/nio.py | 80 -- .../iou/nios/nio_generic_ethernet.py | 50 - gns3server/old_modules/iou/nios/nio_tap.py | 50 - gns3server/old_modules/iou/nios/nio_udp.py | 76 - gns3server/old_modules/iou/schemas.py | 472 ------- gns3server/old_modules/qemu/__init__.py | 687 --------- .../old_modules/qemu/adapters/__init__.py | 0 .../old_modules/qemu/adapters/adapter.py | 105 -- .../qemu/adapters/ethernet_adapter.py | 32 - gns3server/old_modules/qemu/nios/__init__.py | 0 gns3server/old_modules/qemu/nios/nio.py | 66 - gns3server/old_modules/qemu/nios/nio_udp.py | 76 - gns3server/old_modules/qemu/qemu_error.py | 39 - gns3server/old_modules/qemu/qemu_vm.py | 1244 ----------------- gns3server/old_modules/qemu/schemas.py | 423 ------ 23 files changed, 5520 deletions(-) delete mode 100644 gns3server/old_modules/iou/__init__.py delete mode 100644 gns3server/old_modules/iou/adapters/__init__.py delete mode 100644 gns3server/old_modules/iou/adapters/adapter.py delete mode 100644 gns3server/old_modules/iou/adapters/ethernet_adapter.py delete mode 100644 gns3server/old_modules/iou/adapters/serial_adapter.py delete mode 100644 gns3server/old_modules/iou/iou_device.py delete mode 100644 gns3server/old_modules/iou/iou_error.py delete mode 100644 gns3server/old_modules/iou/nios/__init__.py delete mode 100644 gns3server/old_modules/iou/nios/nio.py delete mode 100644 gns3server/old_modules/iou/nios/nio_generic_ethernet.py delete mode 100644 gns3server/old_modules/iou/nios/nio_tap.py delete mode 100644 gns3server/old_modules/iou/nios/nio_udp.py delete mode 100644 gns3server/old_modules/iou/schemas.py delete mode 100644 gns3server/old_modules/qemu/__init__.py delete mode 100644 gns3server/old_modules/qemu/adapters/__init__.py delete mode 100644 gns3server/old_modules/qemu/adapters/adapter.py delete mode 100644 gns3server/old_modules/qemu/adapters/ethernet_adapter.py delete mode 100644 gns3server/old_modules/qemu/nios/__init__.py delete mode 100644 gns3server/old_modules/qemu/nios/nio.py delete mode 100644 gns3server/old_modules/qemu/nios/nio_udp.py delete mode 100644 gns3server/old_modules/qemu/qemu_error.py delete mode 100644 gns3server/old_modules/qemu/qemu_vm.py delete mode 100644 gns3server/old_modules/qemu/schemas.py diff --git a/gns3server/old_modules/iou/__init__.py b/gns3server/old_modules/iou/__init__.py deleted file mode 100644 index 04c7e4c0..00000000 --- a/gns3server/old_modules/iou/__init__.py +++ /dev/null @@ -1,843 +0,0 @@ -# -*- 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 . - -""" -IOU server module. -""" - -import os -import base64 -import ntpath -import stat -import tempfile -import socket -import shutil - -from gns3server.modules import IModule -from gns3server.config import Config -from gns3dms.cloud.rackspace_ctrl import get_provider -from .iou_device import IOUDevice -from .iou_error import IOUError -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from .nios.nio_generic_ethernet import NIO_GenericEthernet -from ..attic import find_unused_port -from ..attic import has_privileged_access - -from .schemas import IOU_CREATE_SCHEMA -from .schemas import IOU_DELETE_SCHEMA -from .schemas import IOU_UPDATE_SCHEMA -from .schemas import IOU_START_SCHEMA -from .schemas import IOU_STOP_SCHEMA -from .schemas import IOU_RELOAD_SCHEMA -from .schemas import IOU_ALLOCATE_UDP_PORT_SCHEMA -from .schemas import IOU_ADD_NIO_SCHEMA -from .schemas import IOU_DELETE_NIO_SCHEMA -from .schemas import IOU_START_CAPTURE_SCHEMA -from .schemas import IOU_STOP_CAPTURE_SCHEMA -from .schemas import IOU_EXPORT_CONFIG_SCHEMA - -import logging -log = logging.getLogger(__name__) - - -class IOU(IModule): - - """ - IOU 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 iouyap location - config = Config.instance() - iou_config = config.get_section_config(name.upper()) - self._iouyap = iou_config.get("iouyap_path") - if not self._iouyap or not os.path.isfile(self._iouyap): - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for iouyap in the current working directory and $PATH - for path in paths: - try: - if "iouyap" in os.listdir(path) and os.access(os.path.join(path, "iouyap"), os.X_OK): - self._iouyap = os.path.join(path, "iouyap") - break - except OSError: - continue - - if not self._iouyap: - log.warning("iouyap binary couldn't be found!") - elif not os.access(self._iouyap, os.X_OK): - log.warning("iouyap is not executable") - - # a new process start when calling IModule - IModule.__init__(self, name, *args, **kwargs) - self._iou_instances = {} - self._console_start_port_range = iou_config.get("console_start_port_range", 4001) - self._console_end_port_range = iou_config.get("console_end_port_range", 4500) - self._allocated_udp_ports = [] - self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001) - self._udp_end_port_range = iou_config.get("udp_end_port_range", 35000) - self._host = iou_config.get("host", kwargs["host"]) - self._console_host = iou_config.get("console_host", kwargs["console_host"]) - self._projects_dir = kwargs["projects_dir"] - self._tempdir = kwargs["temp_dir"] - self._working_dir = self._projects_dir - self._server_iourc_path = iou_config.get("iourc", "") - self._iourc = "" - - # check every 5 seconds - self._iou_callback = self.add_periodic_callback(self._check_iou_is_alive, 5000) - self._iou_callback.start() - - def stop(self, signum=None): - """ - Properly stops the module. - - :param signum: signal number (if called by the signal handler) - """ - - self._iou_callback.stop() - - # delete all IOU instances - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.delete() - - self.delete_iourc_file() - - IModule.stop(self, signum) # this will stop the I/O loop - - def _check_iou_is_alive(self): - """ - Periodic callback to check if IOU and iouyap are alive - for each IOU instance. - - Sends a notification to the client if not. - """ - - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - if iou_instance.started and (not iou_instance.is_running() or not iou_instance.is_iouyap_running()): - notification = {"module": self.name, - "id": iou_id, - "name": iou_instance.name} - if not iou_instance.is_running(): - stdout = iou_instance.read_iou_stdout() - notification["message"] = "IOU has stopped running" - notification["details"] = stdout - self.send_notification("{}.iou_stopped".format(self.name), notification) - elif not iou_instance.is_iouyap_running(): - stdout = iou_instance.read_iouyap_stdout() - notification["message"] = "iouyap has stopped running" - notification["details"] = stdout - self.send_notification("{}.iouyap_stopped".format(self.name), notification) - iou_instance.stop() - - def get_iou_instance(self, iou_id): - """ - Returns an IOU device instance. - - :param iou_id: IOU device identifier - - :returns: IOUDevice instance - """ - - if iou_id not in self._iou_instances: - log.debug("IOU device ID {} doesn't exist".format(iou_id), exc_info=1) - self.send_custom_error("IOU device ID {} doesn't exist".format(iou_id)) - return None - return self._iou_instances[iou_id] - - def delete_iourc_file(self): - """ - Deletes the IOURC file. - """ - - if self._iourc and os.path.isfile(self._iourc): - try: - log.info("deleting iourc file {}".format(self._iourc)) - os.remove(self._iourc) - except OSError as e: - log.warn("could not delete iourc file {}: {}".format(self._iourc, e)) - - @IModule.route("iou.reset") - def reset(self, request=None): - """ - Resets the module (JSON-RPC notification). - - :param request: JSON request (not used) - """ - - # delete all IOU instances - for iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.delete() - - # resets the instance IDs - IOUDevice.reset() - - self._iou_instances.clear() - self._allocated_udp_ports.clear() - self.delete_iourc_file() - - self._working_dir = self._projects_dir - log.info("IOU module has been reset") - - @IModule.route("iou.settings") - def settings(self, request): - """ - Set or update settings. - - Mandatory request parameters: - - iourc (base64 encoded iourc file) - - Optional request parameters: - - iouyap (path to iouyap) - - 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 "iourc" in request: - iourc_content = base64.decodebytes(request["iourc"].encode("utf-8")).decode("utf-8") - iourc_content = iourc_content.replace("\r\n", "\n") # dos2unix - try: - with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: - log.info("saving iourc file content to {}".format(f.name)) - f.write(iourc_content) - self._iourc = f.name - except OSError as e: - raise IOUError("Could not create the iourc file: {}".format(e)) - - if "iouyap" in request and request["iouyap"]: - self._iouyap = request["iouyap"] - log.info("iouyap path set to {}".format(self._iouyap)) - - 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 iou_id in self._iou_instances: - iou_instance = self._iou_instances[iou_id] - iou_instance.working_dir = os.path.join(self._working_dir, "iou", "device-{}".format(iou_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("iou.create") - def iou_create(self, request): - """ - Creates a new IOU instance. - - Mandatory request parameters: - - path (path to the IOU executable) - - Optional request parameters: - - name (IOU name) - - console (IOU console port) - - Response parameters: - - id (IOU instance identifier) - - name (IOU name) - - default settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_CREATE_SCHEMA): - return - - name = request["name"] - iou_path = request["path"] - console = request.get("console") - iou_id = request.get("iou_id") - - updated_iou_path = os.path.join(self.images_directory, iou_path) - if os.path.isfile(updated_iou_path): - iou_path = updated_iou_path - else: - if not os.path.exists(self.images_directory): - os.mkdir(self.images_directory) - cloud_path = request.get("cloud_path", None) - if cloud_path is not None: - # Download the image from cloud files - _, filename = ntpath.split(iou_path) - src = '{}/{}'.format(cloud_path, filename) - provider = get_provider(self._cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, updated_iou_path)) - provider.download_file(src, updated_iou_path) - log.debug("Download of {} complete.".format(src)) - # Make file executable - st = os.stat(updated_iou_path) - os.chmod(updated_iou_path, st.st_mode | stat.S_IEXEC) - iou_path = updated_iou_path - - try: - iou_instance = IOUDevice(name, - iou_path, - self._working_dir, - iou_id, - console, - self._console_host, - self._console_start_port_range, - self._console_end_port_range) - - except IOUError as e: - self.send_custom_error(str(e)) - return - - response = {"name": iou_instance.name, - "id": iou_instance.id} - - defaults = iou_instance.defaults() - response.update(defaults) - self._iou_instances[iou_instance.id] = iou_instance - self.send_response(response) - - @IModule.route("iou.delete") - def iou_delete(self, request): - """ - Deletes an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - Response parameter: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_DELETE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - iou_instance.clean_delete() - del self._iou_instances[request["id"]] - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("iou.update") - def iou_update(self, request): - """ - Updates an IOU instance - - Mandatory request parameters: - - id (IOU instance identifier) - - Optional request parameters: - - any setting to update - - initial_config_base64 (initial-config base64 encoded) - - Response parameters: - - updated settings - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_UPDATE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - config_path = os.path.join(iou_instance.working_dir, "initial-config.cfg") - try: - if "initial_config_base64" in request: - # a new initial-config has been pushed - config = base64.decodebytes(request["initial_config_base64"].encode("utf-8")).decode("utf-8") - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', iou_instance.name) - try: - with open(config_path, "w") as f: - log.info("saving initial-config to {}".format(config_path)) - f.write(config) - except OSError as e: - raise IOUError("Could not save the configuration {}: {}".format(config_path, e)) - # update the request with the new local initial-config path - request["initial_config"] = os.path.basename(config_path) - elif "initial_config" in request: - if os.path.isfile(request["initial_config"]) and request["initial_config"] != config_path: - # this is a local file set in the GUI - try: - with open(request["initial_config"], "r", errors="replace") as f: - config = f.read() - with open(config_path, "w") as f: - config = "!\n" + config.replace("\r", "") - config = config.replace('%h', iou_instance.name) - f.write(config) - request["initial_config"] = os.path.basename(config_path) - except OSError as e: - raise IOUError("Could not save the configuration from {} to {}: {}".format(request["initial_config"], config_path, e)) - elif not os.path.isfile(config_path): - raise IOUError("Startup-config {} could not be found on this server".format(request["initial_config"])) - except IOUError as e: - self.send_custom_error(str(e)) - return - - # update the IOU settings - response = {} - for name, value in request.items(): - if hasattr(iou_instance, name) and getattr(iou_instance, name) != value: - try: - setattr(iou_instance, name, value) - response[name] = value - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response(response) - - @IModule.route("iou.start") - def vm_start(self, request): - """ - Starts an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_START_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - iou_instance.iouyap = self._iouyap - if self._iourc: - iou_instance.iourc = self._iourc - else: - # if there is no IOURC file pushed by the client then use the server IOURC file - iou_instance.iourc = self._server_iourc_path - iou_instance.start() - except IOUError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("iou.stop") - def vm_stop(self, request): - """ - Stops an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_STOP_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - iou_instance.stop() - except IOUError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("iou.reload") - def vm_reload(self, request): - """ - Reloads an IOU instance. - - Mandatory request parameters: - - id (IOU identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_RELOAD_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - try: - if iou_instance.is_running(): - iou_instance.stop() - iou_instance.start() - except IOUError as e: - self.send_custom_error(str(e)) - return - self.send_response(True) - - @IModule.route("iou.allocate_udp_port") - def allocate_udp_port(self, request): - """ - Allocates a UDP port in order to create an UDP NIO. - - Mandatory request parameters: - - id (IOU 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, IOU_ALLOCATE_UDP_PORT_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_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(iou_instance.name, - iou_instance.id, - port, - self._host)) - response = {"lport": port, - "port_id": request["port_id"]} - self.send_response(response) - - @IModule.route("iou.add_nio") - def add_nio(self, request): - """ - Adds an NIO (Network Input/Output) for an IOU instance. - - Mandatory request parameters: - - id (IOU instance identifier) - - slot (slot number) - - 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) - - type "nio_generic_ethernet" - - ethernet_device (Ethernet device name e.g. eth0) - - type "nio_tap" - - tap_device (TAP device name e.g. tap0) - - Response parameters: - - port_id (unique port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_ADD_NIO_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - 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 IOUError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIO_UDP(lport, rhost, rport) - elif request["nio"]["type"] == "nio_tap": - tap_device = request["nio"]["tap_device"] - if not has_privileged_access(self._iouyap): - raise IOUError("{} has no privileged access to {}.".format(self._iouyap, tap_device)) - nio = NIO_TAP(tap_device) - elif request["nio"]["type"] == "nio_generic_ethernet": - ethernet_device = request["nio"]["ethernet_device"] - if not has_privileged_access(self._iouyap): - raise IOUError("{} has no privileged access to {}.".format(self._iouyap, ethernet_device)) - nio = NIO_GenericEthernet(ethernet_device) - if not nio: - raise IOUError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"])) - except IOUError as e: - self.send_custom_error(str(e)) - return - - try: - iou_instance.slot_add_nio_binding(slot, port, nio) - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response({"port_id": request["port_id"]}) - - @IModule.route("iou.delete_nio") - def delete_nio(self, request): - """ - Deletes an NIO (Network Input/Output). - - Mandatory request parameters: - - id (IOU instance identifier) - - slot (slot identifier) - - port (port identifier) - - Response parameters: - - True on success - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_DELETE_NIO_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - port = request["port"] - try: - nio = iou_instance.slot_remove_nio_binding(slot, port) - if isinstance(nio, NIO_UDP) and nio.lport in self._allocated_udp_ports: - self._allocated_udp_ports.remove(nio.lport) - except IOUError as e: - self.send_custom_error(str(e)) - return - - self.send_response(True) - - @IModule.route("iou.start_capture") - def start_capture(self, request): - """ - Starts a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - slot (slot number) - - port (port number) - - port_id (port identifier) - - capture_file_name - - Optional request parameters: - - data_link_type (PCAP DLT_* value) - - Response parameters: - - port_id (port identifier) - - capture_file_path (path to the capture file) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_START_CAPTURE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - port = request["port"] - capture_file_name = request["capture_file_name"] - data_link_type = request.get("data_link_type") - - try: - capture_file_path = os.path.join(self._working_dir, "captures", capture_file_name) - iou_instance.start_capture(slot, port, capture_file_path, data_link_type) - except IOUError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"], - "capture_file_path": capture_file_path} - self.send_response(response) - - @IModule.route("iou.stop_capture") - def stop_capture(self, request): - """ - Stops a packet capture. - - Mandatory request parameters: - - id (vm identifier) - - slot (slot number) - - port (port number) - - port_id (port identifier) - - Response parameters: - - port_id (port identifier) - - :param request: JSON request - """ - - # validate the request - if not self.validate_request(request, IOU_STOP_CAPTURE_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - slot = request["slot"] - port = request["port"] - try: - iou_instance.stop_capture(slot, port) - except IOUError as e: - self.send_custom_error(str(e)) - return - - response = {"port_id": request["port_id"]} - self.send_response(response) - - @IModule.route("iou.export_config") - def export_config(self, request): - """ - Exports the initial-config from an IOU instance. - - Mandatory request parameters: - - id (vm identifier) - - Response parameters: - - initial_config_base64 (initial-config base64 encoded) - - False if no configuration can be exported - """ - - # validate the request - if not self.validate_request(request, IOU_EXPORT_CONFIG_SCHEMA): - return - - # get the instance - iou_instance = self.get_iou_instance(request["id"]) - if not iou_instance: - return - - if not iou_instance.initial_config: - self.send_custom_error("unable to export the initial-config because it doesn't exist") - return - - response = {} - initial_config_path = os.path.join(iou_instance.working_dir, iou_instance.initial_config) - try: - with open(initial_config_path, "rb") as f: - config = f.read() - response["initial_config_base64"] = base64.encodebytes(config).decode("utf-8") - except OSError as e: - self.send_custom_error("unable to export the initial-config: {}".format(e)) - return - - if not response: - self.send_response(False) - else: - self.send_response(response) - - @IModule.route("iou.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/old_modules/iou/adapters/__init__.py b/gns3server/old_modules/iou/adapters/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/iou/adapters/adapter.py b/gns3server/old_modules/iou/adapters/adapter.py deleted file mode 100644 index 06645e56..00000000 --- a/gns3server/old_modules/iou/adapters/adapter.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- 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=4): - - 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/old_modules/iou/adapters/ethernet_adapter.py b/gns3server/old_modules/iou/adapters/ethernet_adapter.py deleted file mode 100644 index bf96362f..00000000 --- a/gns3server/old_modules/iou/adapters/ethernet_adapter.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- 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): - - """ - IOU Ethernet adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=4) - - def __str__(self): - - return "IOU Ethernet adapter" diff --git a/gns3server/old_modules/iou/adapters/serial_adapter.py b/gns3server/old_modules/iou/adapters/serial_adapter.py deleted file mode 100644 index ca7d3200..00000000 --- a/gns3server/old_modules/iou/adapters/serial_adapter.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- 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 SerialAdapter(Adapter): - - """ - IOU Serial adapter. - """ - - def __init__(self): - Adapter.__init__(self, interfaces=4) - - def __str__(self): - - return "IOU Serial adapter" diff --git a/gns3server/old_modules/iou/iou_device.py b/gns3server/old_modules/iou/iou_device.py deleted file mode 100644 index ff8ff2c3..00000000 --- a/gns3server/old_modules/iou/iou_device.py +++ /dev/null @@ -1,1069 +0,0 @@ -# -*- 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 . - -""" -IOU device management (creates command line, processes, files etc.) in -order to run an IOU instance. -""" - -import os -import re -import signal -import subprocess -import argparse -import threading -import configparser -import shutil - -from .ioucon import start_ioucon -from .iou_error import IOUError -from .adapters.ethernet_adapter import EthernetAdapter -from .adapters.serial_adapter import SerialAdapter -from .nios.nio_udp import NIO_UDP -from .nios.nio_tap import NIO_TAP -from .nios.nio_generic_ethernet import NIO_GenericEthernet -from ..attic import find_unused_port - -import logging -log = logging.getLogger(__name__) - - -class IOUDevice(object): - - """ - IOU device implementation. - - :param name: name of this IOU device - :param path: path to IOU executable - :param working_dir: path to a working directory - :param iou_id: IOU instance ID - :param console: TCP console port - :param console_host: IP address to bind for console connections - :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, - path, - working_dir, - iou_id=None, - console=None, - console_host="0.0.0.0", - console_start_port_range=4001, - console_end_port_range=4512): - - if not iou_id: - # find an instance identifier if none is provided (0 < id <= 512) - self._id = 0 - for identifier in range(1, 513): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise IOUError("Maximum number of IOU instances reached") - else: - if iou_id in self._instances: - raise IOUError("IOU identifier {} is already used by another IOU device".format(iou_id)) - self._id = iou_id - self._instances.append(self._id) - - self._name = name - self._path = path - self._iourc = "" - self._iouyap = "" - self._console = console - self._working_dir = None - self._command = [] - self._process = None - self._iouyap_process = None - self._iou_stdout_file = "" - self._iouyap_stdout_file = "" - self._ioucon_thead = None - self._ioucon_thread_stop_event = None - self._started = False - self._console_host = console_host - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range - - # IOU settings - self._ethernet_adapters = [EthernetAdapter(), EthernetAdapter()] # one adapter = 4 interfaces - self._serial_adapters = [SerialAdapter(), SerialAdapter()] # one adapter = 4 interfaces - self._slots = self._ethernet_adapters + self._serial_adapters - self._use_default_iou_values = True # for RAM & NVRAM values - self._nvram = 128 # Kilobytes - self._initial_config = "" - self._ram = 256 # Megabytes - self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). - - working_dir_path = os.path.join(working_dir, "iou", "device-{}".format(self._id)) - - if iou_id and not os.path.isdir(working_dir_path): - raise IOUError("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._console_host, - ignore_ports=self._allocated_console_ports) - except Exception as e: - raise IOUError(e) - - if self._console in self._allocated_console_ports: - raise IOUError("Console port {} is already in used another IOU device".format(console)) - self._allocated_console_ports.append(self._console) - - log.info("IOU device {name} [id={id}] has been created".format(name=self._name, - id=self._id)) - - def defaults(self): - """ - Returns all the default attribute values for IOU. - - :returns: default values (dictionary) - """ - - iou_defaults = {"name": self._name, - "path": self._path, - "intial_config": self._initial_config, - "use_default_iou_values": self._use_default_iou_values, - "ram": self._ram, - "nvram": self._nvram, - "ethernet_adapters": len(self._ethernet_adapters), - "serial_adapters": len(self._serial_adapters), - "console": self._console, - "l1_keepalives": self._l1_keepalives} - - return iou_defaults - - @property - def id(self): - """ - Returns the unique ID for this IOU device. - - :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 IOU device. - - :returns: name - """ - - return self._name - - @name.setter - def name(self, new_name): - """ - Sets the name of this IOU device. - - :param new_name: name - """ - - if self._initial_config: - # update the initial-config - config_path = os.path.join(self._working_dir, "initial-config.cfg") - if os.path.isfile(config_path): - try: - with open(config_path, "r+", errors="replace") as f: - old_config = f.read() - new_config = old_config.replace(self._name, new_name) - f.seek(0) - f.write(new_config) - except OSError as e: - raise IOUError("Could not amend the configuration {}: {}".format(config_path, e)) - - log.info("IOU {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - self._name = new_name - - @property - def path(self): - """ - Returns the path to the IOU executable. - - :returns: path to IOU - """ - - return self._path - - @path.setter - def path(self, path): - """ - Sets the path to the IOU executable. - - :param path: path to IOU - """ - - self._path = path - log.info("IOU {name} [id={id}]: path changed to {path}".format(name=self._name, - id=self._id, - path=path)) - - @property - def iourc(self): - """ - Returns the path to the iourc file. - - :returns: path to the iourc file - """ - - return self._iourc - - @iourc.setter - def iourc(self, iourc): - """ - Sets the path to the iourc file. - - :param iourc: path to the iourc file. - """ - - self._iourc = iourc - log.info("IOU {name} [id={id}]: iourc file path set to {path}".format(name=self._name, - id=self._id, - path=self._iourc)) - - @property - def iouyap(self): - """ - Returns the path to iouyap - - :returns: path to iouyap - """ - - return self._iouyap - - @iouyap.setter - def iouyap(self, iouyap): - """ - Sets the path to iouyap. - - :param iouyap: path to iouyap - """ - - self._iouyap = iouyap - log.info("IOU {name} [id={id}]: iouyap path set to {path}".format(name=self._name, - id=self._id, - path=self._iouyap)) - - @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 for IOU. - - :param working_dir: path to the working directory - """ - - try: - os.makedirs(working_dir) - except FileExistsError: - pass - except OSError as e: - raise IOUError("Could not create working directory {}: {}".format(working_dir, e)) - - self._working_dir = working_dir - log.info("IOU {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 IOUError("Console port {} is already used by another IOU device".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) - log.info("IOU {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) - - def command(self): - """ - Returns the IOU command line. - - :returns: IOU command line (string) - """ - - return " ".join(self._build_command()) - - def delete(self): - """ - Deletes this IOU device. - """ - - 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("IOU device {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) - - def clean_delete(self): - """ - Deletes this IOU device & all files (nvram, initial-config etc.) - """ - - 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 IOU device {name} [id={id}]: {error}".format(name=self._name, - id=self._id, - error=e)) - return - - log.info("IOU device {name} [id={id}] has been deleted (including associated files)".format(name=self._name, - id=self._id)) - - @property - def started(self): - """ - Returns either this IOU device has been started or not. - - :returns: boolean - """ - - return self._started - - def _update_iouyap_config(self): - """ - Updates the iouyap.ini file. - """ - - iouyap_ini = os.path.join(self._working_dir, "iouyap.ini") - - config = configparser.ConfigParser() - config["default"] = {"netmap": "NETMAP", - "base_port": "49000"} - - bay_id = 0 - for adapter in self._slots: - unit_id = 0 - for unit in adapter.ports.keys(): - nio = adapter.get_nio(unit) - if nio: - connection = None - if isinstance(nio, NIO_UDP): - # UDP tunnel - connection = {"tunnel_udp": "{lport}:{rhost}:{rport}".format(lport=nio.lport, - rhost=nio.rhost, - rport=nio.rport)} - elif isinstance(nio, NIO_TAP): - # TAP interface - connection = {"tap_dev": "{tap_device}".format(tap_device=nio.tap_device)} - - elif isinstance(nio, NIO_GenericEthernet): - # Ethernet interface - connection = {"eth_dev": "{ethernet_device}".format(ethernet_device=nio.ethernet_device)} - - if connection: - interface = "{iouyap_id}:{bay}/{unit}".format(iouyap_id=str(self._id + 512), bay=bay_id, unit=unit_id) - config[interface] = connection - - if nio.capturing: - pcap_data_link_type = nio.pcap_data_link_type.upper() - if pcap_data_link_type == "DLT_PPP_SERIAL": - pcap_protocol = "ppp" - elif pcap_data_link_type == "DLT_C_HDLC": - pcap_protocol = "hdlc" - elif pcap_data_link_type == "DLT_FRELAY": - pcap_protocol = "fr" - else: - pcap_protocol = "ethernet" - capture_info = {"pcap_file": "{pcap_file}".format(pcap_file=nio.pcap_output_file), - "pcap_protocol": pcap_protocol, - "pcap_overwrite": "y"} - config[interface].update(capture_info) - - unit_id += 1 - bay_id += 1 - - try: - with open(iouyap_ini, "w") as config_file: - config.write(config_file) - log.info("IOU {name} [id={id}]: iouyap.ini updated".format(name=self._name, - id=self._id)) - except OSError as e: - raise IOUError("Could not create {}: {}".format(iouyap_ini, e)) - - def _create_netmap_config(self): - """ - Creates the NETMAP file. - """ - - netmap_path = os.path.join(self._working_dir, "NETMAP") - try: - with open(netmap_path, "w") as f: - for bay in range(0, 16): - for unit in range(0, 4): - f.write("{iouyap_id}:{bay}/{unit}{iou_id:>5d}:{bay}/{unit}\n".format(iouyap_id=str(self._id + 512), - bay=bay, - unit=unit, - iou_id=self._id)) - log.info("IOU {name} [id={id}]: NETMAP file created".format(name=self._name, - id=self._id)) - except OSError as e: - raise IOUError("Could not create {}: {}".format(netmap_path, e)) - - def _start_ioucon(self): - """ - Starts ioucon thread (for console connections). - """ - - if not self._ioucon_thead: - telnet_server = "{}:{}".format(self._console_host, self.console) - log.info("starting ioucon for IOU instance {} to accept Telnet connections on {}".format(self._name, telnet_server)) - args = argparse.Namespace(appl_id=str(self._id), debug=False, escape='^^', telnet_limit=0, telnet_server=telnet_server) - self._ioucon_thread_stop_event = threading.Event() - self._ioucon_thead = threading.Thread(target=start_ioucon, args=(args, self._ioucon_thread_stop_event)) - self._ioucon_thead.start() - - def _start_iouyap(self): - """ - Starts iouyap (handles connections to and from this IOU device). - """ - - try: - self._update_iouyap_config() - command = [self._iouyap, "-q", str(self._id + 512)] # iouyap has always IOU ID + 512 - log.info("starting iouyap: {}".format(command)) - self._iouyap_stdout_file = os.path.join(self._working_dir, "iouyap.log") - log.info("logging to {}".format(self._iouyap_stdout_file)) - with open(self._iouyap_stdout_file, "w") as fd: - self._iouyap_process = subprocess.Popen(command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir) - - log.info("iouyap started PID={}".format(self._iouyap_process.pid)) - except (OSError, subprocess.SubprocessError) as e: - iouyap_stdout = self.read_iouyap_stdout() - log.error("could not start iouyap: {}\n{}".format(e, iouyap_stdout)) - raise IOUError("Could not start iouyap: {}\n{}".format(e, iouyap_stdout)) - - def _library_check(self): - """ - Checks for missing shared library dependencies in the IOU image. - """ - - try: - output = subprocess.check_output(["ldd", self._path]) - except (FileNotFoundError, subprocess.SubprocessError) as e: - log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) - return - - p = re.compile("([\.\w]+)\s=>\s+not found") - missing_libs = p.findall(output.decode("utf-8")) - if missing_libs: - raise IOUError("The following shared library dependencies cannot be found for IOU image {}: {}".format(self._path, - ", ".join(missing_libs))) - - def start(self): - """ - Starts the IOU process. - """ - - if not self.is_running(): - - if not os.path.isfile(self._path) or not os.path.exists(self._path): - if os.path.islink(self._path): - raise IOUError("IOU image '{}' linked to '{}' is not accessible".format(self._path, os.path.realpath(self._path))) - else: - raise IOUError("IOU image '{}' is not accessible".format(self._path)) - - try: - with open(self._path, "rb") as f: - # read the first 7 bytes of the file. - elf_header_start = f.read(7) - except OSError as e: - raise IOUError("Cannot read ELF header for IOU image '{}': {}".format(self._path, e)) - - # IOU images must start with the ELF magic number, be 32-bit, little endian - # and have an ELF version of 1 normal IOS image are big endian! - if elf_header_start != b'\x7fELF\x01\x01\x01': - raise IOUError("'{}' is not a valid IOU image".format(self._path)) - - if not os.access(self._path, os.X_OK): - raise IOUError("IOU image '{}' is not executable".format(self._path)) - - self._library_check() - - if not self._iourc or not os.path.isfile(self._iourc): - raise IOUError("A valid iourc file is necessary to start IOU") - - if not self._iouyap or not os.path.isfile(self._iouyap): - raise IOUError("iouyap is necessary to start IOU") - - self._create_netmap_config() - # created a environment variable pointing to the iourc file. - env = os.environ.copy() - env["IOURC"] = self._iourc - self._command = self._build_command() - try: - log.info("starting IOU: {}".format(self._command)) - self._iou_stdout_file = os.path.join(self._working_dir, "iou.log") - log.info("logging to {}".format(self._iou_stdout_file)) - with open(self._iou_stdout_file, "w") as fd: - self._process = subprocess.Popen(self._command, - stdout=fd, - stderr=subprocess.STDOUT, - cwd=self._working_dir, - env=env) - log.info("IOU instance {} started PID={}".format(self._id, self._process.pid)) - self._started = True - except FileNotFoundError as e: - raise IOUError("could not start IOU: {}: 32-bit binary support is probably not installed".format(e)) - except (OSError, subprocess.SubprocessError) as e: - iou_stdout = self.read_iou_stdout() - log.error("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) - raise IOUError("could not start IOU {}: {}\n{}".format(self._path, e, iou_stdout)) - - # start console support - self._start_ioucon() - # connections support - self._start_iouyap() - - def stop(self): - """ - Stops the IOU process. - """ - - # stop console support - if self._ioucon_thead: - self._ioucon_thread_stop_event.set() - if self._ioucon_thead.is_alive(): - self._ioucon_thead.join(timeout=3.0) # wait for the thread to free the console port - self._ioucon_thead = None - - # stop iouyap - if self.is_iouyap_running(): - log.info("stopping iouyap PID={} for IOU instance {}".format(self._iouyap_process.pid, self._id)) - try: - self._iouyap_process.terminate() - self._iouyap_process.wait(1) - except subprocess.TimeoutExpired: - self._iouyap_process.kill() - if self._iouyap_process.poll() is None: - log.warn("iouyap PID={} for IOU instance {} is still running".format(self._iouyap_process.pid, - self._id)) - self._iouyap_process = None - - # stop the IOU process - if self.is_running(): - log.info("stopping IOU 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("IOU instance {} PID={} is still running".format(self._id, - self._process.pid)) - self._process = None - self._started = False - - def read_iou_stdout(self): - """ - Reads the standard output of the IOU process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._iou_stdout_file: - try: - with open(self._iou_stdout_file, errors="replace") as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._iou_stdout_file, e)) - return output - - def read_iouyap_stdout(self): - """ - Reads the standard output of the iouyap process. - Only use when the process has been stopped or has crashed. - """ - - output = "" - if self._iouyap_stdout_file: - try: - with open(self._iouyap_stdout_file, errors="replace") as file: - output = file.read() - except OSError as e: - log.warn("could not read {}: {}".format(self._iouyap_stdout_file, e)) - return output - - def is_running(self): - """ - Checks if the IOU process is running - - :returns: True or False - """ - - if self._process and self._process.poll() is None: - return True - return False - - def is_iouyap_running(self): - """ - Checks if the iouyap process is running - - :returns: True or False - """ - - if self._iouyap_process and self._iouyap_process.poll() is None: - return True - return False - - def slot_add_nio_binding(self, slot_id, port_id, nio): - """ - Adds a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - :param nio: NIO instance to add to the slot/port - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - adapter.add_nio(port_id, nio) - log.info("IOU {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) - - def slot_remove_nio_binding(self, slot_id, port_id): - """ - Removes a slot NIO binding. - - :param slot_id: slot ID - :param port_id: port ID - - :returns: NIO instance - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - adapter.remove_nio(port_id) - log.info("IOU {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, - id=self._id, - nio=nio, - slot_id=slot_id, - port_id=port_id)) - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) - - return nio - - def _enable_l1_keepalives(self, command): - """ - Enables L1 keepalive messages if supported. - - :param command: command line - """ - - env = os.environ.copy() - env["IOURC"] = self._iourc - try: - output = subprocess.check_output([self._path, "-h"], stderr=subprocess.STDOUT, cwd=self._working_dir, env=env) - if re.search("-l\s+Enable Layer 1 keepalive messages", output.decode("utf-8")): - command.extend(["-l"]) - else: - raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path))) - except (OSError, subprocess.SubprocessError) as e: - log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e)) - - def _build_command(self): - """ - Command to start the IOU process. - (to be passed to subprocess.Popen()) - - IOU command line: - Usage: [options] - : unix-js-m | unix-is-m | unix-i-m | ... - : instance identifier (0 < id <= 1024) - Options: - -e Number of Ethernet interfaces (default 2) - -s Number of Serial interfaces (default 2) - -n Size of nvram in Kb (default 64KB) - -b IOS debug string - -c Configuration file name - -d Generate debug information - -t Netio message trace - -q Suppress informational messages - -h Display this help - -C Turn off use of host clock - -m Megabytes of router memory (default 256MB) - -L Disable local console, use remote console - -l Enable Layer 1 keepalive messages - -u UDP port base for distributed networks - -R Ignore options from the IOURC file - -U Disable unix: file system location - -W Disable watchdog timer - -N Ignore the NETMAP file - """ - - command = [self._path] - if len(self._ethernet_adapters) != 2: - command.extend(["-e", str(len(self._ethernet_adapters))]) - if len(self._serial_adapters) != 2: - command.extend(["-s", str(len(self._serial_adapters))]) - if not self.use_default_iou_values: - command.extend(["-n", str(self._nvram)]) - command.extend(["-m", str(self._ram)]) - command.extend(["-L"]) # disable local console, use remote console - if self._initial_config: - command.extend(["-c", self._initial_config]) - if self._l1_keepalives: - self._enable_l1_keepalives(command) - command.extend([str(self._id)]) - return command - - @property - def use_default_iou_values(self): - """ - Returns if this device uses the default IOU image values. - - :returns: boolean - """ - - return self._use_default_iou_values - - @use_default_iou_values.setter - def use_default_iou_values(self, state): - """ - Sets if this device uses the default IOU image values. - - :param state: boolean - """ - - self._use_default_iou_values = state - if state: - log.info("IOU {name} [id={id}]: uses the default IOU image values".format(name=self._name, id=self._id)) - else: - log.info("IOU {name} [id={id}]: does not use the default IOU image values".format(name=self._name, id=self._id)) - - @property - def l1_keepalives(self): - """ - Returns either layer 1 keepalive messages option is enabled or disabled. - - :returns: boolean - """ - - return self._l1_keepalives - - @l1_keepalives.setter - def l1_keepalives(self, state): - """ - Enables or disables layer 1 keepalive messages. - - :param state: boolean - """ - - self._l1_keepalives = state - if state: - log.info("IOU {name} [id={id}]: has activated layer 1 keepalive messages".format(name=self._name, id=self._id)) - else: - log.info("IOU {name} [id={id}]: has deactivated layer 1 keepalive messages".format(name=self._name, id=self._id)) - - @property - def ram(self): - """ - Returns the amount of RAM allocated to this IOU instance. - - :returns: amount of RAM in Mbytes (integer) - """ - - return self._ram - - @ram.setter - def ram(self, ram): - """ - Sets amount of RAM allocated to this IOU instance. - - :param ram: amount of RAM in Mbytes (integer) - """ - - if self._ram == ram: - return - - log.info("IOU {name} [id={id}]: RAM updated from {old_ram}MB to {new_ram}MB".format(name=self._name, - id=self._id, - old_ram=self._ram, - new_ram=ram)) - - self._ram = ram - - @property - def nvram(self): - """ - Returns the mount of NVRAM allocated to this IOU instance. - - :returns: amount of NVRAM in Kbytes (integer) - """ - - return self._nvram - - @nvram.setter - def nvram(self, nvram): - """ - Sets amount of NVRAM allocated to this IOU instance. - - :param nvram: amount of NVRAM in Kbytes (integer) - """ - - if self._nvram == nvram: - return - - log.info("IOU {name} [id={id}]: NVRAM updated from {old_nvram}KB to {new_nvram}KB".format(name=self._name, - id=self._id, - old_nvram=self._nvram, - new_nvram=nvram)) - self._nvram = nvram - - @property - def initial_config(self): - """ - Returns the initial-config for this IOU instance. - - :returns: path to initial-config file - """ - - return self._initial_config - - @initial_config.setter - def initial_config(self, initial_config): - """ - Sets the initial-config for this IOU instance. - - :param initial_config: path to initial-config file - """ - - self._initial_config = initial_config - log.info("IOU {name} [id={id}]: initial_config set to {config}".format(name=self._name, - id=self._id, - config=self._initial_config)) - - @property - def ethernet_adapters(self): - """ - Returns the number of Ethernet adapters for this IOU instance. - - :returns: number of adapters - """ - - return len(self._ethernet_adapters) - - @ethernet_adapters.setter - def ethernet_adapters(self, ethernet_adapters): - """ - Sets the number of Ethernet adapters for this IOU instance. - - :param ethernet_adapters: number of adapters - """ - - self._ethernet_adapters.clear() - for _ in range(0, ethernet_adapters): - self._ethernet_adapters.append(EthernetAdapter()) - - log.info("IOU {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name, - id=self._id, - adapters=len(self._ethernet_adapters))) - - self._slots = self._ethernet_adapters + self._serial_adapters - - @property - def serial_adapters(self): - """ - Returns the number of Serial adapters for this IOU instance. - - :returns: number of adapters - """ - - return len(self._serial_adapters) - - @serial_adapters.setter - def serial_adapters(self, serial_adapters): - """ - Sets the number of Serial adapters for this IOU instance. - - :param serial_adapters: number of adapters - """ - - self._serial_adapters.clear() - for _ in range(0, serial_adapters): - self._serial_adapters.append(SerialAdapter()) - - log.info("IOU {name} [id={id}]: number of Serial adapters changed to {adapters}".format(name=self._name, - id=self._id, - adapters=len(self._serial_adapters))) - - self._slots = self._ethernet_adapters + self._serial_adapters - - def start_capture(self, slot_id, port_id, output_file, data_link_type="DLT_EN10MB"): - """ - Starts a packet capture. - - :param slot_id: slot ID - :param port_id: port ID - :param port: allocated port - :param output_file: PCAP destination file for the capture - :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - if nio.capturing: - raise IOUError("Packet capture is already activated on {slot_id}/{port_id}".format(slot_id=slot_id, - port_id=port_id)) - - try: - os.makedirs(os.path.dirname(output_file)) - except FileExistsError: - pass - except OSError as e: - raise IOUError("Could not create captures directory {}".format(e)) - - nio.startPacketCapture(output_file, data_link_type) - - log.info("IOU {name} [id={id}]: starting packet capture on {slot_id}/{port_id}".format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) - - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) - - def stop_capture(self, slot_id, port_id): - """ - Stops a packet capture. - - :param slot_id: slot ID - :param port_id: port ID - """ - - try: - adapter = self._slots[slot_id] - except IndexError: - raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, - slot_id=slot_id)) - - if not adapter.port_exists(port_id): - raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, - port_id=port_id)) - - nio = adapter.get_nio(port_id) - nio.stopPacketCapture() - log.info("IOU {name} [id={id}]: stopping packet capture on {slot_id}/{port_id}".format(name=self._name, - id=self._id, - slot_id=slot_id, - port_id=port_id)) - if self.is_iouyap_running(): - self._update_iouyap_config() - os.kill(self._iouyap_process.pid, signal.SIGHUP) diff --git a/gns3server/old_modules/iou/iou_error.py b/gns3server/old_modules/iou/iou_error.py deleted file mode 100644 index 8aac176f..00000000 --- a/gns3server/old_modules/iou/iou_error.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- 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 . - -""" -Custom exceptions for IOU module. -""" - - -class IOUError(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/old_modules/iou/nios/__init__.py b/gns3server/old_modules/iou/nios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/iou/nios/nio.py b/gns3server/old_modules/iou/nios/nio.py deleted file mode 100644 index 0c8e610e..00000000 --- a/gns3server/old_modules/iou/nios/nio.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- 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 = "" - self._pcap_data_link_type = "" - - def startPacketCapture(self, pcap_output_file, pcap_data_link_type="DLT_EN10MB"): - """ - - :param pcap_output_file: PCAP destination file for the capture - :param pcap_data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB - """ - - self._capturing = True - self._pcap_output_file = pcap_output_file - self._pcap_data_link_type = pcap_data_link_type - - def stopPacketCapture(self): - - self._capturing = False - self._pcap_output_file = "" - self._pcap_data_link_type = "" - - @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 - - @property - def pcap_data_link_type(self): - """ - Returns the PCAP data link type - - :returns: PCAP data link type (DLT_* value) - """ - - return self._pcap_data_link_type diff --git a/gns3server/old_modules/iou/nios/nio_generic_ethernet.py b/gns3server/old_modules/iou/nios/nio_generic_ethernet.py deleted file mode 100644 index 709e6474..00000000 --- a/gns3server/old_modules/iou/nios/nio_generic_ethernet.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- 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 generic Ethernet NIOs (PCAP library). -""" - -from .nio import NIO - - -class NIO_GenericEthernet(NIO): - - """ - Generic Ethernet NIO. - - :param ethernet_device: Ethernet device name (e.g. eth0) - """ - - def __init__(self, ethernet_device): - - NIO.__init__(self) - self._ethernet_device = ethernet_device - - @property - def ethernet_device(self): - """ - Returns the Ethernet device used by this NIO. - - :returns: the Ethernet device name - """ - - return self._ethernet_device - - def __str__(self): - - return "NIO Ethernet" diff --git a/gns3server/old_modules/iou/nios/nio_tap.py b/gns3server/old_modules/iou/nios/nio_tap.py deleted file mode 100644 index f6b1663f..00000000 --- a/gns3server/old_modules/iou/nios/nio_tap.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- 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 TAP NIOs (UNIX based OSes only). -""" - -from .nio import NIO - - -class NIO_TAP(NIO): - - """ - TAP NIO. - - :param tap_device: TAP device name (e.g. tap0) - """ - - def __init__(self, tap_device): - - NIO.__init__(self) - self._tap_device = tap_device - - @property - def tap_device(self): - """ - Returns the TAP device used by this NIO. - - :returns: the TAP device name - """ - - return self._tap_device - - def __str__(self): - - return "NIO TAP" diff --git a/gns3server/old_modules/iou/nios/nio_udp.py b/gns3server/old_modules/iou/nios/nio_udp.py deleted file mode 100644 index 3b25f0c4..00000000 --- a/gns3server/old_modules/iou/nios/nio_udp.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- 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/old_modules/iou/schemas.py b/gns3server/old_modules/iou/schemas.py deleted file mode 100644 index f1315ec3..00000000 --- a/gns3server/old_modules/iou/schemas.py +++ /dev/null @@ -1,472 +0,0 @@ -# -*- 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 . - - -IOU_CREATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to create a new IOU instance", - "type": "object", - "properties": { - "name": { - "description": "IOU device name", - "type": "string", - "minLength": 1, - }, - "iou_id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "path": { - "description": "path to the IOU executable", - "type": "string", - "minLength": 1, - }, - "cloud_path": { - "description": "Path to the image in the cloud object store", - "type": "string", - } - }, - "additionalProperties": False, - "required": ["name", "path"], -} - -IOU_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "name": { - "description": "IOU device name", - "type": "string", - "minLength": 1, - }, - "path": { - "description": "path to the IOU executable", - "type": "string", - "minLength": 1, - }, - "initial_config": { - "description": "path to the IOU initial configuration file", - "type": "string", - "minLength": 1, - }, - "ram": { - "description": "amount of RAM in MB", - "type": "integer" - }, - "nvram": { - "description": "amount of NVRAM in KB", - "type": "integer" - }, - "ethernet_adapters": { - "description": "number of Ethernet adapters", - "type": "integer", - "minimum": 0, - "maximum": 16, - }, - "serial_adapters": { - "description": "number of serial adapters", - "type": "integer", - "minimum": 0, - "maximum": 16, - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "use_default_iou_values": { - "description": "use the default IOU RAM & NVRAM values", - "type": "boolean" - }, - "l1_keepalives": { - "description": "enable or disable layer 1 keepalive messages", - "type": "boolean" - }, - "initial_config_base64": { - "description": "initial configuration base64 encoded", - "type": "string" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -IOU_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -IOU_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for an IOU 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": "IOU device instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - "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", "slot", "port", "nio"] -} - - -IOU_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - }, - "additionalProperties": False, - "required": ["id", "slot", "port"] -} - -IOU_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on an IOU instance port", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - "data_link_type": { - "description": "PCAP data link type", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "slot", "port", "port_id", "capture_file_name"] -} - -IOU_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on an IOU instance port", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - "slot": { - "description": "Slot number", - "type": "integer", - "minimum": 0, - "maximum": 15 - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - "port_id": { - "description": "Unique port identifier for the IOU instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "slot", "port", "port_id"] -} - -IOU_EXPORT_CONFIG_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to export an initial-config from an IOU instance", - "type": "object", - "properties": { - "id": { - "description": "IOU device instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} diff --git a/gns3server/old_modules/qemu/__init__.py b/gns3server/old_modules/qemu/__init__.py deleted file mode 100644 index 01b3c72e..00000000 --- a/gns3server/old_modules/qemu/__init__.py +++ /dev/null @@ -1,687 +0,0 @@ -# -*- 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 sys -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 - -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): - - # 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._monitor_start_port_range = qemu_config.get("monitor_start_port_range", 5501) - self._monitor_end_port_range = qemu_config.get("monitor_end_port_range", 6000) - 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._console_host = qemu_config.get("console_host", kwargs["console_host"]) - self._monitor_host = qemu_config.get("monitor_host", "127.0.0.1") - 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() - - self._working_dir = self._projects_dir - 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 - - monitor_start_port_range - - monitor_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 "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 "monitor_start_port_range" in request and "monitor_end_port_range" in request: - self._monitor_start_port_range = request["monitor_start_port_range"] - self._monitor_end_port_range = request["monitor_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) - - monitor (QEMU VM monitor 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") - monitor = request.get("monitor") - qemu_id = request.get("qemu_id") - - try: - qemu_instance = QemuVM(name, - qemu_path, - self._working_dir, - self._host, - qemu_id, - console, - self._console_host, - self._console_start_port_range, - self._console_end_port_range, - monitor, - self._monitor_host, - self._monitor_start_port_range, - self._monitor_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 - """ - - if sys.platform.startswith("win"): - return "" - try: - output = subprocess.check_output([qemu_path, "-version"]) - match = re.search("version\s+([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 subprocess.SubprocessError 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: - - List of Qemu binaries - """ - - qemus = [] - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for Qemu binaries in the current working directory and $PATH - if sys.platform.startswith("win"): - # add specific Windows paths - if hasattr(sys, "frozen"): - # add any qemu dir in the same location as gns3server.exe to the list of paths - exec_dir = os.path.dirname(os.path.abspath(sys.executable)) - for f in os.listdir(exec_dir): - if f.lower().startswith("qemu"): - paths.append(os.path.join(exec_dir, f)) - - if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]): - paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu")) - if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]): - paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu")) - elif sys.platform.startswith("darwin"): - # add specific locations on Mac OS X regardless of what's in $PATH - paths.extend(["/usr/local/bin", "/opt/local/bin"]) - if hasattr(sys, "frozen"): - paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/"))) - for path in paths: - try: - for f in os.listdir(path): - if (f.startswith("qemu-system") or f == "qemu" or f == "qemu.exe") and \ - os.access(os.path.join(path, f), os.X_OK) and \ - os.path.isfile(os.path.join(path, f)): - qemu_path = os.path.join(path, f) - version = self._get_qemu_version(qemu_path) - qemus.append({"path": qemu_path, "version": version}) - except OSError: - continue - - response = {"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/old_modules/qemu/adapters/__init__.py b/gns3server/old_modules/qemu/adapters/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/qemu/adapters/adapter.py b/gns3server/old_modules/qemu/adapters/adapter.py deleted file mode 100644 index ade660f9..00000000 --- a/gns3server/old_modules/qemu/adapters/adapter.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- 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/old_modules/qemu/adapters/ethernet_adapter.py b/gns3server/old_modules/qemu/adapters/ethernet_adapter.py deleted file mode 100644 index 2064bb68..00000000 --- a/gns3server/old_modules/qemu/adapters/ethernet_adapter.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- 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/old_modules/qemu/nios/__init__.py b/gns3server/old_modules/qemu/nios/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gns3server/old_modules/qemu/nios/nio.py b/gns3server/old_modules/qemu/nios/nio.py deleted file mode 100644 index 3c8a6b9e..00000000 --- a/gns3server/old_modules/qemu/nios/nio.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- 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/old_modules/qemu/nios/nio_udp.py b/gns3server/old_modules/qemu/nios/nio_udp.py deleted file mode 100644 index 3b25f0c4..00000000 --- a/gns3server/old_modules/qemu/nios/nio_udp.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- 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/old_modules/qemu/qemu_error.py b/gns3server/old_modules/qemu/qemu_error.py deleted file mode 100644 index 55135a34..00000000 --- a/gns3server/old_modules/qemu/qemu_error.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- 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/old_modules/qemu/qemu_vm.py b/gns3server/old_modules/qemu/qemu_vm.py deleted file mode 100644 index a5ae107d..00000000 --- a/gns3server/old_modules/qemu/qemu_vm.py +++ /dev/null @@ -1,1244 +0,0 @@ -# -*- 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 sys -import os -import shutil -import random -import subprocess -import shlex -import ntpath -import telnetlib -import time -import re - -from gns3server.config import Config -from gns3dms.cloud.rackspace_ctrl import get_provider - -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 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_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end - :param monitor: TCP monitor port - :param monitor_host: IP address to bind for monitor connections - :param monitor_start_port_range: TCP monitor port range start - :param monitor_end_port_range: TCP monitor port range end - """ - - _instances = [] - _allocated_console_ports = [] - _allocated_monitor_ports = [] - - def __init__(self, - name, - qemu_path, - working_dir, - host="127.0.0.1", - qemu_id=None, - console=None, - console_host="0.0.0.0", - console_start_port_range=5001, - console_end_port_range=5500, - monitor=None, - monitor_host="0.0.0.0", - monitor_start_port_range=5501, - monitor_end_port_range=6000): - - 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._cpulimit_process = None - self._stdout_file = "" - self._console_host = console_host - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range - self._monitor_host = monitor_host - self._monitor_start_port_range = monitor_start_port_range - self._monitor_end_port_range = monitor_end_port_range - self._cloud_path = None - - # QEMU settings - self._qemu_path = qemu_path - self._hda_disk_image = "" - self._hdb_disk_image = "" - self._options = "" - self._ram = 256 - self._console = console - self._monitor = monitor - self._ethernet_adapters = [] - self._adapter_type = "e1000" - self._initrd = "" - self._kernel_image = "" - self._kernel_command_line = "" - self._legacy_networking = False - self._cpu_throttling = 0 # means no CPU throttling - self._process_priority = "low" - - 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._console_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) - - if not self._monitor: - # allocate a monitor port - try: - self._monitor = find_unused_port(self._monitor_start_port_range, - self._monitor_end_port_range, - self._monitor_host, - ignore_ports=self._allocated_monitor_ports) - except Exception as e: - raise QemuError(e) - - if self._monitor in self._allocated_monitor_ports: - raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) - self._allocated_monitor_ports.append(self._monitor) - - 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, - "hda_disk_image": self._hda_disk_image, - "hdb_disk_image": self._hdb_disk_image, - "options": self._options, - "adapters": self.adapters, - "adapter_type": self._adapter_type, - "console": self._console, - "monitor": self._monitor, - "initrd": self._initrd, - "kernel_image": self._kernel_image, - "kernel_command_line": self._kernel_command_line, - "legacy_networking": self._legacy_networking, - "cpu_throttling": self._cpu_throttling, - "process_priority": self._process_priority - } - - 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() - cls._allocated_monitor_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)) - - @property - def monitor(self): - """ - Returns the TCP monitor port. - - :returns: monitor port (integer) - """ - - return self._monitor - - @monitor.setter - def monitor(self, monitor): - """ - Sets the TCP monitor port. - - :param monitor: monitor port (integer) - """ - - if monitor in self._allocated_monitor_ports: - raise QemuError("Monitor port {} is already used by another QEMU VM".format(monitor)) - - self._allocated_monitor_ports.remove(self._monitor) - self._monitor = monitor - self._allocated_monitor_ports.append(self._monitor) - - log.info("QEMU VM {name} [id={id}]: monitor port set to {port}".format(name=self._name, - id=self._id, - port=monitor)) - - 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) - - if self._monitor and self._monitor in self._allocated_monitor_ports: - self._allocated_monitor_ports.remove(self._monitor) - - 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) - - if self._monitor: - self._allocated_monitor_ports.remove(self._monitor) - - 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 cloud_path(self): - """ - Returns the cloud path where images can be downloaded from. - - :returns: cloud path - """ - - return self._cloud_path - - @cloud_path.setter - def cloud_path(self, cloud_path): - """ - Sets the cloud path where images can be downloaded from. - - :param cloud_path: - :return: - """ - - self._cloud_path = cloud_path - - @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 hda_disk_image(self): - """ - Returns the hda disk image path for this QEMU VM. - - :returns: QEMU hda disk image path - """ - - return self._hda_disk_image - - @hda_disk_image.setter - def hda_disk_image(self, hda_disk_image): - """ - Sets the hda disk image for this QEMU VM. - - :param hda_disk_image: QEMU hda disk image path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name, - id=self._id, - disk_image=hda_disk_image)) - self._hda_disk_image = hda_disk_image - - @property - def hdb_disk_image(self): - """ - Returns the hdb disk image path for this QEMU VM. - - :returns: QEMU hdb disk image path - """ - - return self._hdb_disk_image - - @hdb_disk_image.setter - def hdb_disk_image(self, hdb_disk_image): - """ - Sets the hdb disk image for this QEMU VM. - - :param hdb_disk_image: QEMU hdb disk image path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name, - id=self._id, - disk_image=hdb_disk_image)) - self._hdb_disk_image = hdb_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)) - - @property - def legacy_networking(self): - """ - Returns either QEMU legacy networking commands are used. - - :returns: boolean - """ - - return self._legacy_networking - - @legacy_networking.setter - def legacy_networking(self, legacy_networking): - """ - Sets either QEMU legacy networking commands are used. - - :param legacy_networking: boolean - """ - - if legacy_networking: - log.info("QEMU VM {name} [id={id}] has enabled legacy networking".format(name=self._name, id=self._id)) - else: - log.info("QEMU VM {name} [id={id}] has disabled legacy networking".format(name=self._name, id=self._id)) - self._legacy_networking = legacy_networking - - @property - def cpu_throttling(self): - """ - Returns the percentage of CPU allowed. - - :returns: integer - """ - - return self._cpu_throttling - - @cpu_throttling.setter - def cpu_throttling(self, cpu_throttling): - """ - Sets the percentage of CPU allowed. - - :param cpu_throttling: integer - """ - - log.info("QEMU VM {name} [id={id}] has set the percentage of CPU allowed to {cpu}".format(name=self._name, - id=self._id, - cpu=cpu_throttling)) - self._cpu_throttling = cpu_throttling - self._stop_cpulimit() - if cpu_throttling: - self._set_cpu_throttling() - - @property - def process_priority(self): - """ - Returns the process priority. - - :returns: string - """ - - return self._process_priority - - @process_priority.setter - def process_priority(self, process_priority): - """ - Sets the process priority. - - :param process_priority: string - """ - - log.info("QEMU VM {name} [id={id}] has set the process priority to {priority}".format(name=self._name, - id=self._id, - priority=process_priority)) - self._process_priority = process_priority - - @property - def ram(self): - """ - Returns the RAM amount for this QEMU VM. - - :returns: RAM amount in MB - """ - - return self._ram - - @ram.setter - def ram(self, ram): - """ - Sets the amount of RAM for this QEMU VM. - - :param ram: RAM amount in MB - """ - - log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name, - id=self._id, - ram=ram)) - self._ram = ram - - @property - def options(self): - """ - Returns the options for this QEMU VM. - - :returns: QEMU options - """ - - return self._options - - @options.setter - def options(self, options): - """ - Sets the options for this QEMU VM. - - :param options: QEMU options - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name, - id=self._id, - options=options)) - self._options = options - - @property - def initrd(self): - """ - Returns the initrd path for this QEMU VM. - - :returns: QEMU initrd path - """ - - return self._initrd - - @initrd.setter - def initrd(self, initrd): - """ - Sets the initrd path for this QEMU VM. - - :param initrd: QEMU initrd path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name, - id=self._id, - initrd=initrd)) - self._initrd = initrd - - @property - def kernel_image(self): - """ - Returns the kernel image path for this QEMU VM. - - :returns: QEMU kernel image path - """ - - return self._kernel_image - - @kernel_image.setter - def kernel_image(self, kernel_image): - """ - Sets the kernel image path for this QEMU VM. - - :param kernel_image: QEMU kernel image path - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name, - id=self._id, - kernel_image=kernel_image)) - self._kernel_image = kernel_image - - @property - def kernel_command_line(self): - """ - Returns the kernel command line for this QEMU VM. - - :returns: QEMU kernel command line - """ - - return self._kernel_command_line - - @kernel_command_line.setter - def kernel_command_line(self, kernel_command_line): - """ - Sets the kernel command line for this QEMU VM. - - :param kernel_command_line: QEMU kernel command line - """ - - log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name, - id=self._id, - kernel_command_line=kernel_command_line)) - self._kernel_command_line = kernel_command_line - - def _set_process_priority(self): - """ - Changes the process priority - """ - - if sys.platform.startswith("win"): - try: - import win32api - import win32con - import win32process - except ImportError: - log.error("pywin32 must be installed to change the priority class for QEMU VM {}".format(self._name)) - else: - log.info("setting QEMU VM {} priority class to BELOW_NORMAL".format(self._name)) - handle = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, 0, self._process.pid) - if self._process_priority == "realtime": - priority = win32process.REALTIME_PRIORITY_CLASS - elif self._process_priority == "very high": - priority = win32process.HIGH_PRIORITY_CLASS - elif self._process_priority == "high": - priority = win32process.ABOVE_NORMAL_PRIORITY_CLASS - elif self._process_priority == "low": - priority = win32process.BELOW_NORMAL_PRIORITY_CLASS - elif self._process_priority == "very low": - priority = win32process.IDLE_PRIORITY_CLASS - else: - priority = win32process.NORMAL_PRIORITY_CLASS - win32process.SetPriorityClass(handle, priority) - else: - if self._process_priority == "realtime": - priority = -20 - elif self._process_priority == "very high": - priority = -15 - elif self._process_priority == "high": - priority = -5 - elif self._process_priority == "low": - priority = 5 - elif self._process_priority == "very low": - priority = 19 - else: - priority = 0 - try: - subprocess.call(['renice', '-n', str(priority), '-p', str(self._process.pid)]) - except (OSError, subprocess.SubprocessError) as e: - log.error("could not change process priority for QEMU VM {}: {}".format(self._name, e)) - - def _stop_cpulimit(self): - """ - Stops the cpulimit process. - """ - - if self._cpulimit_process and self._cpulimit_process.poll() is None: - self._cpulimit_process.kill() - try: - self._process.wait(3) - except subprocess.TimeoutExpired: - log.error("could not kill cpulimit process {}".format(self._cpulimit_process.pid)) - - def _set_cpu_throttling(self): - """ - Limits the CPU usage for current QEMU process. - """ - - if not self.is_running(): - return - - try: - if sys.platform.startswith("win") and hasattr(sys, "frozen"): - cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe") - else: - cpulimit_exec = "cpulimit" - subprocess.Popen([cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)], cwd=self._working_dir) - log.info("CPU throttled to {}%".format(self._cpu_throttling)) - except FileNotFoundError: - raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling") - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not throttle CPU: {}".format(e)) - - def start(self): - """ - Starts this QEMU VM. - """ - - if self.is_running(): - - # resume the VM if it is paused - self.resume() - return - - else: - - if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path): - found = False - paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep) - # look for the qemu binary in the current working directory and $PATH - for path in paths: - try: - if self._qemu_path in os.listdir(path) and os.access(os.path.join(path, self._qemu_path), os.X_OK): - self._qemu_path = os.path.join(path, self._qemu_path) - found = True - break - except OSError: - continue - - if not found: - raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path)) - - if self.cloud_path is not None: - # Download from Cloud Files - if self.hda_disk_image != "": - _, filename = ntpath.split(self.hda_disk_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.hda_disk_image = dst - if self.hdb_disk_image != "": - _, filename = ntpath.split(self.hdb_disk_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.hdb_disk_image = dst - - if self.initrd != "": - _, filename = ntpath.split(self.initrd) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.initrd = dst - if self.kernel_image != "": - _, filename = ntpath.split(self.kernel_image) - src = '{}/{}'.format(self.cloud_path, filename) - dst = os.path.join(self.working_dir, filename) - if not os.path.isfile(dst): - cloud_settings = Config.instance().cloud_settings() - provider = get_provider(cloud_settings) - log.debug("Downloading file from {} to {}...".format(src, dst)) - provider.download_file(src, dst) - log.debug("Download of {} complete.".format(src)) - self.kernel_image = dst - - 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, subprocess.SubprocessError) 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)) - - self._set_process_priority() - if self._cpu_throttling: - self._set_cpu_throttling() - - 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 - self._stop_cpulimit() - - def _control_vm(self, command, expected=None, timeout=30): - """ - Executes a command with QEMU monitor when this VM is running. - - :param command: QEMU monitor command (e.g. info status, stop etc.) - :param timeout: how long to wait for QEMU monitor - - :returns: result of the command (Match object or None) - """ - - result = None - if self.is_running() and self._monitor: - log.debug("Execute QEMU monitor command: {}".format(command)) - try: - tn = telnetlib.Telnet(self._monitor_host, self._monitor, timeout=timeout) - except OSError as e: - log.warn("Could not connect to QEMU monitor: {}".format(e)) - return result - try: - tn.write(command.encode('ascii') + b"\n") - time.sleep(0.1) - except OSError as e: - log.warn("Could not write to QEMU monitor: {}".format(e)) - tn.close() - return result - if expected: - try: - ind, match, dat = tn.expect(list=expected, timeout=timeout) - if match: - result = match - except EOFError as e: - log.warn("Could not read from QEMU monitor: {}".format(e)) - tn.close() - return result - - def _get_vm_status(self): - """ - Returns this VM suspend status (running|paused) - - :returns: status (string) - """ - - result = None - - match = self._control_vm("info status", [b"running", b"paused"]) - if match: - result = match.group(0).decode('ascii') - return result - - def suspend(self): - """ - Suspends this QEMU VM. - """ - - vm_status = self._get_vm_status() - if vm_status == "running": - self._control_vm("stop") - log.debug("QEMU VM has been suspended") - else: - log.info("QEMU VM is not running to be suspended, current status is {}".format(vm_status)) - - def reload(self): - """ - Reloads this QEMU VM. - """ - - self._control_vm("system_reset") - log.debug("QEMU VM has been reset") - - def resume(self): - """ - Resumes this QEMU VM. - """ - - vm_status = self._get_vm_status() - if vm_status == "paused": - self._control_vm("cont") - log.debug("QEMU VM has been resumed") - else: - log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) - - 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)) - - if self.is_running(): - # dynamically configure an UDP tunnel on the QEMU VM adapter - if nio and isinstance(nio, NIO_UDP): - if self._legacy_networking: - self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - self._control_vm("host_net_add udp vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, - adapter_id, - nio.lport, - nio.rport, - nio.rhost)) - else: - # FIXME: does it work? very undocumented feature... - self._control_vm("netdev_del gns3-{}".format(adapter_id)) - self._control_vm("netdev_add socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)) - - 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)) - - if self.is_running(): - # dynamically disable the QEMU VM adapter - if self._legacy_networking: - self._control_vm("host_net_remove {} gns3-{}".format(adapter_id, adapter_id)) - self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_id, adapter_id)) - else: - # FIXME: does it work? very undocumented feature... - self._control_vm("netdev_del gns3-{}".format(adapter_id)) - self._control_vm("netdev_add user,id=gns3-{}".format(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._console_host, self._console)] - else: - return [] - - def _monitor_options(self): - - if self._monitor: - return ["-monitor", "telnet:{}:{},server,nowait".format(self._monitor_host, self._monitor)] - else: - return [] - - def _disk_options(self): - - options = [] - qemu_img_path = "" - qemu_path_dir = os.path.dirname(self._qemu_path) - try: - for f in os.listdir(qemu_path_dir): - if f.startswith("qemu-img"): - qemu_img_path = os.path.join(qemu_path_dir, f) - except OSError as e: - raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e)) - - if not qemu_img_path: - raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir)) - - try: - if self._hda_disk_image: - if not os.path.isfile(self._hda_disk_image) or not os.path.exists(self._hda_disk_image): - if os.path.islink(self._hda_disk_image): - raise QemuError("hda disk image '{}' linked to '{}' is not accessible".format(self._hda_disk_image, os.path.realpath(self._hda_disk_image))) - else: - raise QemuError("hda disk image '{}' is not accessible".format(self._hda_disk_image)) - hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2") - if not os.path.exists(hda_disk): - retcode = subprocess.call([qemu_img_path, "create", "-o", - "backing_file={}".format(self._hda_disk_image), - "-f", "qcow2", hda_disk]) - log.info("{} returned with {}".format(qemu_img_path, retcode)) - else: - # create a "FLASH" with 256MB if no disk image has been specified - hda_disk = os.path.join(self._working_dir, "flash.qcow2") - if not os.path.exists(hda_disk): - retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "128M"]) - log.info("{} returned with {}".format(qemu_img_path, retcode)) - - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create disk image {}".format(e)) - - options.extend(["-hda", hda_disk]) - if self._hdb_disk_image: - if not os.path.isfile(self._hdb_disk_image) or not os.path.exists(self._hdb_disk_image): - if os.path.islink(self._hdb_disk_image): - raise QemuError("hdb disk image '{}' linked to '{}' is not accessible".format(self._hdb_disk_image, os.path.realpath(self._hdb_disk_image))) - else: - raise QemuError("hdb disk image '{}' is not accessible".format(self._hdb_disk_image)) - hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2") - if not os.path.exists(hdb_disk): - try: - retcode = subprocess.call([qemu_img_path, "create", "-o", - "backing_file={}".format(self._hdb_disk_image), - "-f", "qcow2", hdb_disk]) - log.info("{} returned with {}".format(qemu_img_path, retcode)) - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could not create disk image {}".format(e)) - options.extend(["-hdb", hdb_disk]) - - return options - - def _linux_boot_options(self): - - options = [] - if self._initrd: - if not os.path.isfile(self._initrd) or not os.path.exists(self._initrd): - if os.path.islink(self._initrd): - raise QemuError("initrd file '{}' linked to '{}' is not accessible".format(self._initrd, os.path.realpath(self._initrd))) - else: - raise QemuError("initrd file '{}' is not accessible".format(self._initrd)) - options.extend(["-initrd", self._initrd]) - if self._kernel_image: - if not os.path.isfile(self._kernel_image) or not os.path.exists(self._kernel_image): - if os.path.islink(self._kernel_image): - raise QemuError("kernel image '{}' linked to '{}' is not accessible".format(self._kernel_image, os.path.realpath(self._kernel_image))) - else: - raise QemuError("kernel image '{}' is not accessible".format(self._kernel_image)) - options.extend(["-kernel", self._kernel_image]) - if self._kernel_command_line: - options.extend(["-append", self._kernel_command_line]) - - return options - - def _network_options(self): - - network_options = [] - adapter_id = 0 - for adapter in self._ethernet_adapters: - # TODO: let users specify a base mac address - mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id) - if self._legacy_networking: - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_id, mac, self._adapter_type)]) - else: - network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)]) - - nio = adapter.get_nio(0) - if nio and isinstance(nio, NIO_UDP): - if self._legacy_networking: - network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_id, - adapter_id, - nio.lport, - nio.rport, - nio.rhost)]) - else: - network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id, - nio.rhost, - nio.rport, - self._host, - nio.lport)]) - else: - if self._legacy_networking: - network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_id, adapter_id)]) - else: - network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)]) - 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._linux_boot_options()) - command.extend(self._serial_options()) - command.extend(self._monitor_options()) - additional_options = self._options.strip() - if additional_options: - command.extend(shlex.split(additional_options)) - command.extend(self._network_options()) - return command diff --git a/gns3server/old_modules/qemu/schemas.py b/gns3server/old_modules/qemu/schemas.py deleted file mode 100644 index 32b09664..00000000 --- a/gns3server/old_modules/qemu/schemas.py +++ /dev/null @@ -1,423 +0,0 @@ -# -*- 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" - }, - "monitor": { - "description": "monitor 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, - }, - "hda_disk_image": { - "description": "QEMU hda disk image path", - "type": "string", - }, - "hdb_disk_image": { - "description": "QEMU hdb disk image path", - "type": "string", - }, - "ram": { - "description": "amount of RAM in MB", - "type": "integer" - }, - "adapters": { - "description": "number of adapters", - "type": "integer", - "minimum": 0, - "maximum": 32, - }, - "adapter_type": { - "description": "QEMU adapter type", - "type": "string", - "minLength": 1, - }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "monitor": { - "description": "monitor TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" - }, - "initrd": { - "description": "QEMU initrd path", - "type": "string", - }, - "kernel_image": { - "description": "QEMU kernel image path", - "type": "string", - }, - "kernel_command_line": { - "description": "QEMU kernel command line", - "type": "string", - }, - "cloud_path": { - "description": "Path to the image in the cloud object store", - "type": "string", - }, - "legacy_networking": { - "description": "Use QEMU legagy networking commands (-net syntax)", - "type": "boolean", - }, - "cpu_throttling": { - "description": "Percentage of CPU allowed for QEMU", - "minimum": 0, - "maximum": 800, - "type": "integer", - }, - "process_priority": { - "description": "Process priority for QEMU", - "enum": ["realtime", - "very high", - "high", - "normal", - "low", - "very low"] - }, - "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": 32 - }, - "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": 32 - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} From f99d8253465115735bbbb1c236324f30ac41bec9 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Thu, 12 Feb 2015 22:28:12 +0100 Subject: [PATCH 11/11] Support network for IOU --- gns3server/handlers/iou_handler.py | 46 +++++++++++++++++++ gns3server/modules/base_manager.py | 8 ++-- gns3server/modules/iou/iou_vm.py | 74 +++++++++++++++++++++++++++++- gns3server/modules/nios/nio_tap.py | 2 +- gns3server/modules/nios/nio_udp.py | 2 +- gns3server/schemas/iou.py | 72 +++++++++++++++++++++++++++++ gns3server/web/route.py | 6 +-- tests/api/test_iou.py | 30 ++++++++++++ 8 files changed, 230 insertions(+), 10 deletions(-) diff --git a/gns3server/handlers/iou_handler.py b/gns3server/handlers/iou_handler.py index d73a0fb1..eddb85bf 100644 --- a/gns3server/handlers/iou_handler.py +++ b/gns3server/handlers/iou_handler.py @@ -19,6 +19,7 @@ from ..web.route import Route from ..schemas.iou import IOU_CREATE_SCHEMA from ..schemas.iou import IOU_UPDATE_SCHEMA from ..schemas.iou import IOU_OBJECT_SCHEMA +from ..schemas.iou import IOU_NIO_SCHEMA from ..modules.iou import IOU @@ -187,3 +188,48 @@ class IOUHandler: vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) yield from vm.reload() response.set_status(204) + + @Route.post( + r"/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "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 IOU instance", + input=IOU_NIO_SCHEMA, + output=IOU_NIO_SCHEMA) + def create_nio(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + nio = iou_manager.create_nio(vm.iouyap_path, request.json) + vm.slot_add_nio_binding(0, int(request.match_info["port_number"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "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 IOU instance") + def delete_nio(request, response): + + iou_manager = IOU.instance() + vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.slot_remove_nio_binding(0, int(request.match_info["port_number"])) + response.set_status(204) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index ec51f658..f38feacd 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -32,8 +32,8 @@ from ..config import Config from ..utils.asyncio import wait_run_in_executor from .project_manager import ProjectManager -from .nios.nio_udp import NIOUDP -from .nios.nio_tap import NIOTAP +from .nios.nio_udp import NIO_UDP +from .nios.nio_tap import NIO_TAP class BaseManager: @@ -274,11 +274,11 @@ class BaseManager: sock.connect((rhost, rport)) except OSError as e: raise aiohttp.web.HTTPInternalServerError(text="Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e)) - nio = NIOUDP(lport, rhost, rport) + nio = NIO_UDP(lport, rhost, rport) elif nio_settings["type"] == "nio_tap": tap_device = nio_settings["tap_device"] if not self._has_privileged_access(executable): raise aiohttp.web.HTTPForbidden(text="{} has no privileged access to {}.".format(executable, tap_device)) - nio = NIOTAP(tap_device) + nio = NIO_TAP(tap_device) assert nio is not None return nio diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index 6fb7db94..d2387c7d 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -35,6 +35,8 @@ from pkg_resources import parse_version from .iou_error import IOUError from ..adapters.ethernet_adapter import EthernetAdapter from ..adapters.serial_adapter import SerialAdapter +from ..nios.nio_udp import NIO_UDP +from ..nios.nio_tap import NIO_TAP from ..base_vm import BaseVM from .ioucon import start_ioucon @@ -86,7 +88,7 @@ class IOUVM(BaseVM): self._ethernet_adapters = [] self._serial_adapters = [] self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces - self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces + self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces self._use_default_iou_values = True # for RAM & NVRAM values self._nvram = 128 if nvram is None else nvram # Kilobytes self._initial_config = "" @@ -529,6 +531,17 @@ class IOUVM(BaseVM): return True return False + def is_iouyap_running(self): + """ + Checks if the IOUYAP process is running + + :returns: True or False + """ + + if self._iouyap_process: + return True + return False + def _create_netmap_config(self): """ Creates the NETMAP file. @@ -687,3 +700,62 @@ class IOUVM(BaseVM): adapters=len(self._serial_adapters))) self._slots = self._ethernet_adapters + self._serial_adapters + + def slot_add_nio_binding(self, slot_id, port_id, nio): + """ + Adds a slot NIO binding. + :param slot_id: slot ID + :param port_id: port ID + :param nio: NIO instance to add to the slot/port + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + adapter.add_nio(port_id, nio) + log.info("IOU {name} [id={id}]: {nio} added to {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + def slot_remove_nio_binding(self, slot_id, port_id): + """ + Removes a slot NIO binding. + :param slot_id: slot ID + :param port_id: port ID + :returns: NIO instance + """ + + try: + adapter = self._slots[slot_id] + except IndexError: + raise IOUError("Slot {slot_id} doesn't exist on IOU {name}".format(name=self._name, + slot_id=slot_id)) + + if not adapter.port_exists(port_id): + raise IOUError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=adapter, + port_id=port_id)) + + nio = adapter.get_nio(port_id) + adapter.remove_nio(port_id) + log.info("IOU {name} [id={id}]: {nio} removed from {slot_id}/{port_id}".format(name=self._name, + id=self._id, + nio=nio, + slot_id=slot_id, + port_id=port_id)) + if self.is_iouyap_running(): + self._update_iouyap_config() + os.kill(self._iouyap_process.pid, signal.SIGHUP) + + return nio diff --git a/gns3server/modules/nios/nio_tap.py b/gns3server/modules/nios/nio_tap.py index 9f51ce13..a63a72c3 100644 --- a/gns3server/modules/nios/nio_tap.py +++ b/gns3server/modules/nios/nio_tap.py @@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only). from .nio import NIO -class NIOTAP(NIO): +class NIO_TAP(NIO): """ TAP NIO. diff --git a/gns3server/modules/nios/nio_udp.py b/gns3server/modules/nios/nio_udp.py index a87875fe..4af43cd6 100644 --- a/gns3server/modules/nios/nio_udp.py +++ b/gns3server/modules/nios/nio_udp.py @@ -22,7 +22,7 @@ Interface for UDP NIOs. from .nio import NIO -class NIOUDP(NIO): +class NIO_UDP(NIO): """ UDP NIO. diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index 387874cf..6d304a19 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -173,3 +173,75 @@ IOU_OBJECT_SCHEMA = { "additionalProperties": False, "required": ["name", "vm_id", "console", "project_id", "path", "serial_adapters", "ethernet_adapters", "ram", "nvram"] } + +IOU_NIO_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to add a NIO for a VPCS 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 + }, + "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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/TAP"}, + ], + "additionalProperties": True, + "required": ["type"] +} diff --git a/gns3server/web/route.py b/gns3server/web/route.py index d63701fc..4de33da0 100644 --- a/gns3server/web/route.py +++ b/gns3server/web/route.py @@ -28,6 +28,7 @@ log = logging.getLogger(__name__) from ..modules.vm_error import VMError from .response import Response + @asyncio.coroutine def parse_request(request, input_schema): """Parse body of request and raise HTTP errors in case of problems""" @@ -42,9 +43,8 @@ def parse_request(request, input_schema): jsonschema.validate(request.json, input_schema) except jsonschema.ValidationError as e: log.error("Invalid input query. JSON schema error: {}".format(e.message)) - raise aiohttp.web.HTTPBadRequest(text="Request is not {} '{}' in schema: {}".format( - e.validator, - e.validator_value, + raise aiohttp.web.HTTPBadRequest(text="Invalid JSON: {} in schema: {}".format( + e.message, json.dumps(e.schema))) return request diff --git a/tests/api/test_iou.py b/tests/api/test_iou.py index 6ae3f715..61837160 100644 --- a/tests/api/test_iou.py +++ b/tests/api/test_iou.py @@ -133,3 +133,33 @@ def test_iou_update(server, vm, tmpdir, free_console_port): assert response.json["serial_adapters"] == 0 assert response.json["ram"] == 512 assert response.json["nvram"] == 2048 + + +def test_iou_nio_create_udp(server, vm): + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_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}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_udp" + + +def test_iou_nio_create_tap(server, vm): + with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): + response = server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + "tap_device": "test"}) + assert response.status == 201 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_tap" + + +def test_iou_delete_nio(server, vm): + server.post("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/projects/{project_id}/iou/vms/{vm_id}/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 204 + assert response.route == "/projects/{project_id}/iou/vms/{vm_id}/ports/{port_number:\d+}/nio"