diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py
index abc2cd15..c84e2b02 100644
--- a/gns3server/handlers/vpcs_handler.py
+++ b/gns3server/handlers/vpcs_handler.py
@@ -19,16 +19,13 @@ from ..web.route import Route
from ..schemas.vpcs import VPCS_CREATE_SCHEMA
from ..schemas.vpcs import VPCS_OBJECT_SCHEMA
from ..schemas.vpcs import VPCS_ADD_NIO_SCHEMA
-from ..modules import VPCS
+from ..modules.vpcs import VPCS
class VPCSHandler(object):
@classmethod
@Route.post(
r"/vpcs",
- parameters={
- "vpcs_id": "Id of VPCS instance"
- },
status_codes={
201: "Success of creation of VPCS",
409: "Conflict"
@@ -43,6 +40,38 @@ class VPCSHandler(object):
"vpcs_id": vm.id,
"console": 4242})
+ @classmethod
+ @Route.post(
+ r"/vpcs/{vpcs_id}/start",
+ parameters={
+ "vpcs_id": "Id of VPCS instance"
+ },
+ status_codes={
+ 201: "Success of creation of VPCS",
+ },
+ description="Start VPCS",
+ )
+ def create(request, response):
+ vpcs_manager = VPCS.instance()
+ vm = yield from vpcs_manager.start_vm(int(request.match_info['vpcs_id']))
+ response.json({})
+
+ @classmethod
+ @Route.post(
+ r"/vpcs/{vpcs_id}/stop",
+ parameters={
+ "vpcs_id": "Id of VPCS instance"
+ },
+ status_codes={
+ 201: "Success of stopping VPCS",
+ },
+ description="Stop VPCS",
+ )
+ def create(request, response):
+ vpcs_manager = VPCS.instance()
+ vm = yield from vpcs_manager.stop_vm(int(request.match_info['vpcs_id']))
+ response.json({})
+
@classmethod
@Route.get(
r"/vpcs/{vpcs_id}",
diff --git a/gns3server/modules/vm_manager.py b/gns3server/modules/base_manager.py
similarity index 88%
rename from gns3server/modules/vm_manager.py
rename to gns3server/modules/base_manager.py
index 7065a084..dbb29dce 100644
--- a/gns3server/modules/vm_manager.py
+++ b/gns3server/modules/base_manager.py
@@ -19,12 +19,12 @@
import asyncio
import aiohttp
-from .vm_error import VMError
+from .device_error import DeviceError
-class VMManager:
+class BaseManager:
"""
- Base class for all VMManager.
+ Base class for all Manager.
Responsible of management of a VM pool
"""
@@ -69,10 +69,10 @@ class VMManager:
identifier = i
break
if identifier == 0:
- raise VMError("Maximum number of VM instances reached")
+ raise DeviceError("Maximum number of VM instances reached")
else:
if identifier in self._vms:
- raise VMError("VM identifier {} is already used by another VM instance".format(identifier))
+ raise DeviceError("VM identifier {} is already used by another VM instance".format(identifier))
vm = self._VM_CLASS(vmname, identifier)
yield from vm.wait_for_creation()
self._vms[vm.id] = vm
diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py
index 5e618059..3f8e4723 100644
--- a/gns3server/modules/base_vm.py
+++ b/gns3server/modules/base_vm.py
@@ -17,18 +17,74 @@
import asyncio
-from .vm_error import VMError
+from .device_error import DeviceError
+from .attic import find_unused_port
+import logging
+log = logging.getLogger(__name__)
class BaseVM:
+ _allocated_console_ports = []
def __init__(self, name, identifier):
+ self._loop = asyncio.get_event_loop()
+ self._allocate_console()
self._queue = asyncio.Queue()
self._name = name
self._id = identifier
self._created = asyncio.Future()
self._worker = asyncio.async(self._run())
+ log.info("{type} device {name} [id={id}] has been created".format(
+ type=self.__class__.__name__,
+ name=self._name,
+ id=self._id))
+
+ def _allocate_console(self):
+ 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 DeviceError(e)
+
+ if self._console in self._allocated_console_ports:
+ raise DeviceError("Console port {} is already used by another device".format(console))
+ self._allocated_console_ports.append(self._console)
+
+
+ @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 VPCSError("Console port {} is already used by another VPCS device".format(console))
+
+ self._allocated_console_ports.remove(self._console)
+ self._console = console
+ self._allocated_console_ports.append(self._console)
+ log.info("{type} {name} [id={id}]: console port set to {port}".format(
+ type=self.__class__.__name__,
+ name=self._name,
+ id=self._id,
+ port=console))
@property
def id(self):
"""
@@ -65,7 +121,7 @@ class BaseVM:
try:
yield from self._create()
self._created.set_result(True)
- except VMError as e:
+ except DeviceError as e:
self._created.set_exception(e)
return
@@ -75,7 +131,7 @@ class BaseVM:
try:
yield from asyncio.wait_for(self._execute(subcommand, args), timeout=timeout)
except asyncio.TimeoutError:
- raise VMError("{} has timed out after {} seconds!".format(subcommand, timeout))
+ raise DeviceError("{} has timed out after {} seconds!".format(subcommand, timeout))
future.set_result(True)
except Exception as e:
future.set_exception(e)
@@ -83,6 +139,15 @@ class BaseVM:
def wait_for_creation(self):
return self._created
+ @asyncio.coroutine
+ def start():
+ """
+ Starts the VM process.
+ """
+ raise NotImplementedError
+
+
+
def put(self, *args):
"""
Add to the processing queue of the VM
@@ -95,5 +160,5 @@ class BaseVM:
args.insert(0, future)
self._queue.put_nowait(args)
except asyncio.qeues.QueueFull:
- raise VMError("Queue is full")
+ raise DeviceError("Queue is full")
return future
diff --git a/gns3server/modules/vm_error.py b/gns3server/modules/device_error.py
similarity index 95%
rename from gns3server/modules/vm_error.py
rename to gns3server/modules/device_error.py
index d7b71e14..b8eca500 100644
--- a/gns3server/modules/vm_error.py
+++ b/gns3server/modules/device_error.py
@@ -16,5 +16,5 @@
# along with this program. If not, see .
-class VMError(Exception):
+class DeviceError(Exception):
pass
diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py
index 618124d1..52191f2f 100644
--- a/gns3server/modules/vpcs/__init__.py
+++ b/gns3server/modules/vpcs/__init__.py
@@ -19,12 +19,9 @@
VPCS server module.
"""
-from ..vm_manager import VMManager
+from ..base_manager import BaseManager
from .vpcs_device import VPCSDevice
-class VPCS(VMManager):
+class VPCS(BaseManager):
_VM_CLASS = VPCSDevice
-
- def create_vm(self, name):
- return super().create_vm(name)
diff --git a/gns3server/modules/vpcs/adapters/__init__.py b/gns3server/modules/vpcs/adapters/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gns3server/modules/vpcs/adapters/adapter.py b/gns3server/modules/vpcs/adapters/adapter.py
new file mode 100644
index 00000000..cf439427
--- /dev/null
+++ b/gns3server/modules/vpcs/adapters/adapter.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+class Adapter(object):
+ """
+ Base class for adapters.
+
+ :param interfaces: number of interfaces supported by this adapter.
+ """
+
+ def __init__(self, interfaces=1):
+
+ self._interfaces = interfaces
+
+ self._ports = {}
+ for port_id in range(0, interfaces):
+ self._ports[port_id] = None
+
+ def removable(self):
+ """
+ Returns True if the adapter can be removed from a slot
+ and False if not.
+
+ :returns: boolean
+ """
+
+ return True
+
+ def port_exists(self, port_id):
+ """
+ Checks if a port exists on this adapter.
+
+ :returns: True is the port exists,
+ False otherwise.
+ """
+
+ if port_id in self._ports:
+ return True
+ return False
+
+ def add_nio(self, port_id, nio):
+ """
+ Adds a NIO to a port on this adapter.
+
+ :param port_id: port ID (integer)
+ :param nio: NIO instance
+ """
+
+ self._ports[port_id] = nio
+
+ def remove_nio(self, port_id):
+ """
+ Removes a NIO from a port on this adapter.
+
+ :param port_id: port ID (integer)
+ """
+
+ self._ports[port_id] = None
+
+ def get_nio(self, port_id):
+ """
+ Returns the NIO assigned to a port.
+
+ :params port_id: port ID (integer)
+
+ :returns: NIO instance
+ """
+
+ return self._ports[port_id]
+
+ @property
+ def ports(self):
+ """
+ Returns port to NIO mapping
+
+ :returns: dictionary port -> NIO
+ """
+
+ return self._ports
+
+ @property
+ def interfaces(self):
+ """
+ Returns the number of interfaces supported by this adapter.
+
+ :returns: number of interfaces
+ """
+
+ return self._interfaces
diff --git a/gns3server/modules/vpcs/adapters/ethernet_adapter.py b/gns3server/modules/vpcs/adapters/ethernet_adapter.py
new file mode 100644
index 00000000..bbca7f40
--- /dev/null
+++ b/gns3server/modules/vpcs/adapters/ethernet_adapter.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from .adapter import Adapter
+
+
+class EthernetAdapter(Adapter):
+ """
+ VPCS Ethernet adapter.
+ """
+
+ def __init__(self):
+ Adapter.__init__(self, interfaces=1)
+
+ def __str__(self):
+
+ return "VPCS Ethernet adapter"
diff --git a/gns3server/modules/vpcs/nios/__init__.py b/gns3server/modules/vpcs/nios/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gns3server/modules/vpcs/nios/nio_tap.py b/gns3server/modules/vpcs/nios/nio_tap.py
new file mode 100644
index 00000000..4c3ed6b2
--- /dev/null
+++ b/gns3server/modules/vpcs/nios/nio_tap.py
@@ -0,0 +1,46 @@
+# -*- 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).
+"""
+
+
+class NIO_TAP(object):
+ """
+ TAP NIO.
+
+ :param tap_device: TAP device name (e.g. tap0)
+ """
+
+ def __init__(self, tap_device):
+
+ 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/modules/vpcs/nios/nio_udp.py b/gns3server/modules/vpcs/nios/nio_udp.py
new file mode 100644
index 00000000..0527f675
--- /dev/null
+++ b/gns3server/modules/vpcs/nios/nio_udp.py
@@ -0,0 +1,72 @@
+# -*- 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.
+"""
+
+
+class NIO_UDP(object):
+ """
+ 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):
+
+ self._lport = lport
+ self._rhost = rhost
+ self._rport = rport
+
+ @property
+ def lport(self):
+ """
+ Returns the local port
+
+ :returns: local port number
+ """
+
+ return self._lport
+
+ @property
+ def rhost(self):
+ """
+ Returns the remote host
+
+ :returns: remote address/host
+ """
+
+ return self._rhost
+
+ @property
+ def rport(self):
+ """
+ Returns the remote port
+
+ :returns: remote port number
+ """
+
+ return self._rport
+
+ def __str__(self):
+
+ return "NIO UDP"
diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py
index 734344db..a5d0c6ca 100644
--- a/gns3server/modules/vpcs/vpcs_device.py
+++ b/gns3server/modules/vpcs/vpcs_device.py
@@ -15,9 +15,370 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+"""
+VPCS device management (creates command line, processes, files etc.) in
+order to run an VPCS instance.
+"""
+
+import os
+import sys
+import subprocess
+import signal
+import shutil
+import re
+import asyncio
+
+from pkg_resources import parse_version
+from .vpcs_error import VPCSError
+from .adapters.ethernet_adapter import EthernetAdapter
+from .nios.nio_udp import NIO_UDP
+from .nios.nio_tap import NIO_TAP
from ..base_vm import BaseVM
+import logging
+log = logging.getLogger(__name__)
class VPCSDevice(BaseVM):
- pass
+ """
+ VPCS device implementation.
+
+ :param name: name of this VPCS device
+ :param vpcs_id: VPCS instance ID
+ :param path: path to VPCS executable
+ :param working_dir: path to a working directory
+ :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
+ """
+ def __init__(self, name, vpcs_id,
+ path = None,
+ working_dir = None,
+ console=None,
+ console_host="0.0.0.0",
+ console_start_port_range=4512,
+ console_end_port_range=5000):
+
+ #self._path = path
+ #self._working_dir = working_dir
+ # TODO: Hardcodded for testing
+ self._path = "/usr/local/bin/vpcs"
+ self._working_dir = "/tmp"
+
+ self._console = console
+ self._console_host = console_host
+ self._command = []
+ self._process = None
+ self._vpcs_stdout_file = ""
+ self._started = False
+ self._console_start_port_range = console_start_port_range
+ self._console_end_port_range = console_end_port_range
+
+ # VPCS settings
+ self._script_file = ""
+ self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
+
+ # working_dir_path = os.path.join(working_dir, "vpcs", "pc-{}".format(self._id))
+ #
+ # if vpcs_id and not os.path.isdir(working_dir_path):
+ # raise VPCSError("Working directory {} doesn't exist".format(working_dir_path))
+ #
+ # # create the device own working directory
+ # self.working_dir = working_dir_path
+ #
+
+ super().__init__(name, vpcs_id)
+
+ @asyncio.coroutine
+ def _create(self):
+ """Called when run loop is started"""
+ self._check_requirement()
+
+ def _check_requirement(self):
+ """Check if VPCS is available with the correct version"""
+ if not self._path:
+ raise VPCSError("No path to a VPCS executable has been set")
+
+ if not os.path.isfile(self._path):
+ raise VPCSError("VPCS program '{}' is not accessible".format(self._path))
+
+ if not os.access(self._path, os.X_OK):
+ raise VPCSError("VPCS program '{}' is not executable".format(self._path))
+
+ yield from self._check_vpcs_version()
+
+ def defaults(self):
+ """
+ Returns all the default attribute values for VPCS.
+
+ :returns: default values (dictionary)
+ """
+
+ vpcs_defaults = {"name": self._name,
+ "script_file": self._script_file,
+ "console": self._console}
+
+ return vpcs_defaults
+
+
+ @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 VPCS device.
+
+ :returns: name
+ """
+
+ return self._name
+
+ @name.setter
+ def name(self, new_name):
+ """
+ Sets the name of this VPCS device.
+
+ :param new_name: name
+ """
+
+ if self._script_file:
+ # update the startup.vpc
+ config_path = os.path.join(self._working_dir, "startup.vpc")
+ 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 VPCSError("Could not amend the configuration {}: {}".format(config_path, e))
+
+ log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name,
+ id=self._id,
+ new_name=new_name))
+ self._name = new_name
+
+ @asyncio.coroutine
+ def _check_vpcs_version(self):
+ """
+ Checks if the VPCS executable version is >= 0.5b1.
+ """
+ #TODO: should be async
+ try:
+ output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir)
+ match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
+ if match:
+ version = match.group(1)
+ if parse_version(version) < parse_version("0.5b1"):
+ raise VPCSError("VPCS executable version must be >= 0.5b1")
+ else:
+ raise VPCSError("Could not determine the VPCS version for {}".format(self._path))
+ except (OSError, subprocess.SubprocessError) as e:
+ raise VPCSError("Error while looking for the VPCS version: {}".format(e))
+
+
+ @asyncio.coroutine
+ def start(self):
+ """
+ Starts the VPCS process.
+ """
+
+ if not self.is_running():
+ # if not self._ethernet_adapter.get_nio(0):
+ # raise VPCSError("This VPCS instance must be connected in order to start")
+
+ self._command = self._build_command()
+ try:
+ log.info("starting VPCS: {}".format(self._command))
+ self._vpcs_stdout_file = os.path.join(self._working_dir, "vpcs.log")
+ log.info("logging to {}".format(self._vpcs_stdout_file))
+ flags = 0
+ if sys.platform.startswith("win32"):
+ flags = subprocess.CREATE_NEW_PROCESS_GROUP
+ with open(self._vpcs_stdout_file, "w") as fd:
+ self._process = yield from asyncio.create_subprocess_exec(*self._command,
+ stdout=fd,
+ stderr=subprocess.STDOUT,
+ cwd=self._working_dir,
+ creationflags=flags)
+ log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid))
+ self._started = True
+ except (OSError, subprocess.SubprocessError) as e:
+ vpcs_stdout = self.read_vpcs_stdout()
+ log.error("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
+ raise VPCSError("could not start VPCS {}: {}\n{}".format(self._path, e, vpcs_stdout))
+
+ @asyncio.coroutine
+ def stop(self):
+ """
+ Stops the VPCS process.
+ """
+
+ # stop the VPCS process
+ if self.is_running():
+ log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid))
+ if sys.platform.startswith("win32"):
+ self._process.send_signal(signal.CTRL_BREAK_EVENT)
+ else:
+ self._process.terminate()
+
+ self._process.wait()
+
+ self._process = None
+ self._started = False
+
+ def read_vpcs_stdout(self):
+ """
+ Reads the standard output of the VPCS process.
+ Only use when the process has been stopped or has crashed.
+ """
+
+ output = ""
+ if self._vpcs_stdout_file:
+ try:
+ with open(self._vpcs_stdout_file, errors="replace") as file:
+ output = file.read()
+ except OSError as e:
+ log.warn("could not read {}: {}".format(self._vpcs_stdout_file, e))
+ return output
+
+ def is_running(self):
+ """
+ Checks if the VPCS process is running
+
+ :returns: True or False
+ """
+
+ if self._process:
+ return True
+ return False
+
+ def port_add_nio_binding(self, port_id, nio):
+ """
+ Adds a port NIO binding.
+
+ :param port_id: port ID
+ :param nio: NIO instance to add to the slot/port
+ """
+
+ if not self._ethernet_adapter.port_exists(port_id):
+ raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
+ port_id=port_id))
+
+ self._ethernet_adapter.add_nio(port_id, nio)
+ log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port_id=port_id))
+
+ def port_remove_nio_binding(self, port_id):
+ """
+ Removes a port NIO binding.
+
+ :param port_id: port ID
+
+ :returns: NIO instance
+ """
+
+ if not self._ethernet_adapter.port_exists(port_id):
+ raise VPCSError("Port {port_id} doesn't exist in adapter {adapter}".format(adapter=self._ethernet_adapter,
+ port_id=port_id))
+
+ nio = self._ethernet_adapter.get_nio(port_id)
+ self._ethernet_adapter.remove_nio(port_id)
+ log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port_id=port_id))
+ return nio
+
+ def _build_command(self):
+ """
+ Command to start the VPCS process.
+ (to be passed to subprocess.Popen())
+
+ VPCS command line:
+ usage: vpcs [options] [scriptfile]
+ Option:
+ -h print this help then exit
+ -v print version information then exit
+
+ -i num number of vpc instances to start (default is 9)
+ -p port run as a daemon listening on the tcp 'port'
+ -m num start byte of ether address, default from 0
+ -r file load and execute script file
+ compatible with older versions, DEPRECATED.
+
+ -e tap mode, using /dev/tapx by default (linux only)
+ -u udp mode, default
+
+ udp mode options:
+ -s port local udp base port, default from 20000
+ -c port remote udp base port (dynamips udp port), default from 30000
+ -t ip remote host IP, default 127.0.0.1
+
+ tap mode options:
+ -d device device name, works only when -i is set to 1
+
+ hypervisor mode option:
+ -H port run as the hypervisor listening on the tcp 'port'
+
+ If no 'scriptfile' specified, vpcs will read and execute the file named
+ 'startup.vpc' if it exsits in the current directory.
+
+ """
+
+ command = [self._path]
+ command.extend(["-p", str(self._console)]) # listen to console port
+
+ nio = self._ethernet_adapter.get_nio(0)
+ if nio:
+ if isinstance(nio, NIO_UDP):
+ # UDP tunnel
+ command.extend(["-s", str(nio.lport)]) # source UDP port
+ command.extend(["-c", str(nio.rport)]) # destination UDP port
+ command.extend(["-t", nio.rhost]) # destination host
+
+ elif isinstance(nio, NIO_TAP):
+ # TAP interface
+ command.extend(["-e"])
+ command.extend(["-d", nio.tap_device])
+
+ command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset
+ command.extend(["-i", "1"]) # option to start only one VPC instance
+ command.extend(["-F"]) # option to avoid the daemonization of VPCS
+ if self._script_file:
+ command.extend([self._script_file])
+ return command
+
+ @property
+ def script_file(self):
+ """
+ Returns the script-file for this VPCS instance.
+
+ :returns: path to script-file
+ """
+
+ return self._script_file
+
+ @script_file.setter
+ def script_file(self, script_file):
+ """
+ Sets the script-file for this VPCS instance.
+
+ :param script_file: path to base-script-file
+ """
+
+ self._script_file = script_file
+ log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name,
+ id=self._id,
+ config=self._script_file))
diff --git a/gns3server/modules/vpcs/vpcs_error.py b/gns3server/modules/vpcs/vpcs_error.py
new file mode 100644
index 00000000..acb10f71
--- /dev/null
+++ b/gns3server/modules/vpcs/vpcs_error.py
@@ -0,0 +1,40 @@
+# -*- 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 VPCS module.
+"""
+
+from ..device_error import DeviceError
+
+class VPCSError(DeviceError):
+
+ 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/web/route.py b/gns3server/web/route.py
index 7e0a091f..381b35e0 100644
--- a/gns3server/web/route.py
+++ b/gns3server/web/route.py
@@ -20,6 +20,7 @@ import jsonschema
import asyncio
import aiohttp
+from ..modules.device_error import DeviceError
from .response import Response
@@ -97,6 +98,10 @@ class Route(object):
response = Response(route=route)
response.set_status(e.status)
response.json({"message": e.text, "status": e.status})
+ except DeviceError as e:
+ response = Response(route=route)
+ response.set_status(400)
+ response.json({"message": str(e), "status": 400})
return response
cls._routes.append((method, cls._path, control_schema))
diff --git a/tests/modules/vpcs/test_vpcs_device.py b/tests/modules/vpcs/test_vpcs_device.py
new file mode 100644
index 00000000..69a03d0d
--- /dev/null
+++ b/tests/modules/vpcs/test_vpcs_device.py
@@ -0,0 +1,41 @@
+# -*- 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
+from unittest.mock import patch
+from gns3server.modules.vpcs.vpcs_device import VPCSDevice
+from gns3server.modules.vpcs.vpcs_error import VPCSError
+
+@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.6".encode("utf-8"))
+def test_vm(tmpdir):
+ vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test")
+ assert vm.name == "test"
+ assert vm.id == 42
+
+@patch("subprocess.check_output", return_value="Welcome to Virtual PC Simulator, version 0.1".encode("utf-8"))
+def test_vm_invalid_vpcs_version(tmpdir):
+ with pytest.raises(VPCSError):
+ vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test")
+ assert vm.name == "test"
+ assert vm.id == 42
+
+def test_vm_invalid_vpcs_path(tmpdir):
+ with pytest.raises(VPCSError):
+ vm = VPCSDevice("test", 42, working_dir=str(tmpdir), path="/bin/test_fake")
+ assert vm.name == "test"
+ assert vm.id == 42
+