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)]