diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py
index 7fd76401..5bd4c110 100644
--- a/gns3server/modules/__init__.py
+++ b/gns3server/modules/__init__.py
@@ -19,9 +19,9 @@ import sys
from .base import IModule
from .dynamips import Dynamips
from .vpcs import VPCS
+from .virtualbox import VirtualBox
-MODULES = [Dynamips]
-MODULES.append(VPCS)
+MODULES = [Dynamips, VPCS, VirtualBox]
if sys.platform.startswith("linux"):
# IOU runs only on Linux
diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py
new file mode 100644
index 00000000..ee944615
--- /dev/null
+++ b/gns3server/modules/virtualbox/__init__.py
@@ -0,0 +1,540 @@
+# -*- 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 .
+
+"""
+VirtualBox server module.
+"""
+
+import os
+import base64
+import socket
+import shutil
+
+from gns3server.modules import IModule
+from gns3server.config import Config
+from .virtualbox_vm import VirtualBoxVM
+from .virtualbox_error import VirtualBoxError
+from .nios.nio_udp import NIO_UDP
+from ..attic import find_unused_port
+
+#from .schemas import VBOX_CREATE_SCHEMA
+#from .schemas import VBOX_DELETE_SCHEMA
+#from .schemas import VBOX_UPDATE_SCHEMA
+#from .schemas import VBOX_START_SCHEMA
+#from .schemas import VBOX_STOP_SCHEMA
+#from .schemas import VBOX_RELOAD_SCHEMA
+#from .schemas import VBOX_ALLOCATE_UDP_PORT_SCHEMA
+#from .schemas import VBOX_ADD_NIO_SCHEMA
+#from .schemas import VBOX_DELETE_NIO_SCHEMA
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class VirtualBox(IModule):
+ """
+ VirtualBox 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._vbox_instances = {}
+
+ config = Config.instance()
+ vbox_config = config.get_section_config(name.upper())
+ self._console_start_port_range = vbox_config.get("console_start_port_range", 3501)
+ self._console_end_port_range = vbox_config.get("console_end_port_range", 4000)
+ self._allocated_udp_ports = []
+ self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001)
+ self._udp_end_port_range = vbox_config.get("udp_end_port_range", 35500)
+ self._host = kwargs["host"]
+ self._projects_dir = kwargs["projects_dir"]
+ self._tempdir = kwargs["temp_dir"]
+ self._working_dir = self._projects_dir
+
+ def stop(self, signum=None):
+ """
+ Properly stops the module.
+
+ :param signum: signal number (if called by the signal handler)
+ """
+
+ # delete all VirtualBox instances
+ for vbox_id in self._vbox_instances:
+ vbox_instance = self._vbox_instances[vbox_id]
+ vbox_instance.delete()
+
+ IModule.stop(self, signum) # this will stop the I/O loop
+
+ def get_vbox_instance(self, vbox_id):
+ """
+ Returns a VirtualBox VM instance.
+
+ :param vbox_id: VirtualBox device identifier
+
+ :returns: VBoxDevice instance
+ """
+
+ if vbox_id not in self._vbox_instances:
+ log.debug("VirtualBox VM ID {} doesn't exist".format(vbox_id), exc_info=1)
+ self.send_custom_error("VirtualBox VM ID {} doesn't exist".format(vbox_id))
+ return None
+ return self._vbox_instances[vbox_id]
+
+ @IModule.route("virtualbox.reset")
+ def reset(self, request):
+ """
+ Resets the module.
+
+ :param request: JSON request
+ """
+
+ # delete all VirtualBox instances
+ for vbox_id in self._vbox_instances:
+ vbox_instance = self._vbox_instances[vbox_id]
+ vbox_instance.delete()
+
+ # resets the instance IDs
+ VirtualBoxVM.reset()
+
+ self._vbox_instances.clear()
+ self._allocated_udp_ports.clear()
+
+ log.info("VirtualBox module has been reset")
+
+ @IModule.route("virtualbox.settings")
+ def settings(self, request):
+ """
+ Set or update settings.
+
+ Optional request parameters:
+ - working_dir (path to a working directory)
+ - project_name
+ - console_start_port_range
+ - console_end_port_range
+ - udp_start_port_range
+ - udp_end_port_range
+
+ :param request: JSON request
+ """
+
+ if request is None:
+ self.send_param_error()
+ return
+
+ if "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 vbox_id in self._vbox_instances:
+ vbox_instance = self._vbox_instances[vbox_id]
+ vbox_instance.working_dir = os.path.join(self._working_dir, "vbox", "vm-{}".format(vbox_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("virtualbox.create")
+ def vbox_create(self, request):
+ """
+ Creates a new VirtualBox VM instance.
+
+ Mandatory request parameters:
+ - name (VirtualBox VM name)
+
+ Optional request parameters:
+ - console (VirtualBox VM console port)
+
+ Response parameters:
+ - id (VirtualBox VM instance identifier)
+ - name (VirtualBox VM name)
+ - default settings
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ #if not self.validate_request(request, VBOX_CREATE_SCHEMA):
+ # return
+
+ name = request["name"]
+ console = request.get("console")
+ vbox_id = request.get("vbox_id")
+
+ try:
+
+ vbox_instance = VirtualBoxVM(name,
+ self._working_dir,
+ self._host,
+ vbox_id,
+ console,
+ self._console_start_port_range,
+ self._console_end_port_range)
+
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+
+ response = {"name": vbox_instance.name,
+ "id": vbox_instance.id}
+
+ defaults = vbox_instance.defaults()
+ response.update(defaults)
+ self._vbox_instances[vbox_instance.id] = vbox_instance
+ self.send_response(response)
+
+ @IModule.route("virtualbox.delete")
+ def vbox_delete(self, request):
+ """
+ Deletes a VirtualBox VM instance.
+
+ Mandatory request parameters:
+ - id (VirtualBox VM instance identifier)
+
+ Response parameter:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ #if not self.validate_request(request, VBOX_DELETE_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vbox_instance(request["id"])
+ if not vbox_instance:
+ return
+
+ try:
+ vbox_instance.clean_delete()
+ del self._vbox_instances[request["id"]]
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(True)
+
+ @IModule.route("virtualbox.update")
+ def vbox_update(self, request):
+ """
+ Updates a VirtualBox VM instance
+
+ Mandatory request parameters:
+ - id (VirtualBox 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, VBOX_UPDATE_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vbox_instance(request["id"])
+ if not vbox_instance:
+ return
+
+ # update the VirtualBox VM settings
+ response = {}
+ for name, value in request.items():
+ if hasattr(vbox_instance, name) and getattr(vbox_instance, name) != value:
+ try:
+ setattr(vbox_instance, name, value)
+ response[name] = value
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(response)
+
+ @IModule.route("virtualbox.start")
+ def vbox_start(self, request):
+ """
+ Starts a VirtualBox VM instance.
+
+ Mandatory request parameters:
+ - id (VirtualBox VM instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ #if not self.validate_request(request, VBOX_START_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vbox_instance(request["id"])
+ if not vbox_instance:
+ return
+
+ try:
+ vbox_instance.start()
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("virtualbox.stop")
+ def vbox_stop(self, request):
+ """
+ Stops a VirtualBox VM instance.
+
+ Mandatory request parameters:
+ - id (VirtualBox VM instance identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ #if not self.validate_request(request, VBOX_STOP_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vbox_instance(request["id"])
+ if not vbox_instance:
+ return
+
+ try:
+ vbox_instance.stop()
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("virtualbox.reload")
+ def vbox_reload(self, request):
+ """
+ Reloads a VirtualBox VM instance.
+
+ Mandatory request parameters:
+ - id (VirtualBox VM identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ #if not self.validate_request(request, VBOX_RELOAD_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vpcs_instance(request["id"])
+ if not vbox_instance:
+ return
+
+ try:
+ if vbox_instance.is_running():
+ vbox_instance.stop()
+ vbox_instance.start()
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+ self.send_response(True)
+
+ @IModule.route("virtualbox.allocate_udp_port")
+ def allocate_udp_port(self, request):
+ """
+ Allocates a UDP port in order to create an UDP NIO.
+
+ Mandatory request parameters:
+ - id (VirtualBox 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, VBOX_ALLOCATE_UDP_PORT_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vbox_instance(request["id"])
+ if not vbox_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(vbox_instance.name,
+ vbox_instance.id,
+ port,
+ self._host))
+
+ response = {"lport": port,
+ "port_id": request["port_id"]}
+ self.send_response(response)
+
+ @IModule.route("virtualbox.add_nio")
+ def add_nio(self, request):
+ """
+ Adds an NIO (Network Input/Output) for a VirtualBox VM instance.
+
+ Mandatory request parameters:
+ - id (VirtualBox 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, VBOX_ADD_NIO_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vbox_instance(request["id"])
+ if not vbox_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 VirtualBoxError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
+ nio = NIO_UDP(lport, rhost, rport)
+ if not nio:
+ raise VirtualBoxError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+
+ try:
+ vbox_instance.port_add_nio_binding(port, nio)
+ except VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response({"port_id": request["port_id"]})
+
+ @IModule.route("virtualbox.delete_nio")
+ def delete_nio(self, request):
+ """
+ Deletes an NIO (Network Input/Output).
+
+ Mandatory request parameters:
+ - id (VPCS instance identifier)
+ - port (port identifier)
+
+ Response parameters:
+ - True on success
+
+ :param request: JSON request
+ """
+
+ # validate the request
+ #if not self.validate_request(request, VBOX_DELETE_NIO_SCHEMA):
+ # return
+
+ # get the instance
+ vbox_instance = self.get_vbox_instance(request["id"])
+ if not vbox_instance:
+ return
+
+ port = request["port"]
+ try:
+ nio = vbox_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 VirtualBoxError as e:
+ self.send_custom_error(str(e))
+ return
+
+ self.send_response(True)
+
+ @IModule.route("virtualbox.echo")
+ def echo(self, request):
+ """
+ Echo end point for testing purposes.
+
+ :param request: JSON request
+ """
+
+ if request is None:
+ self.send_param_error()
+ else:
+ log.debug("received request {}".format(request))
+ self.send_response(request)
diff --git a/gns3server/modules/virtualbox/adapters/__init__.py b/gns3server/modules/virtualbox/adapters/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gns3server/modules/virtualbox/adapters/adapter.py b/gns3server/modules/virtualbox/adapters/adapter.py
new file mode 100644
index 00000000..cf439427
--- /dev/null
+++ b/gns3server/modules/virtualbox/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/virtualbox/adapters/ethernet_adapter.py b/gns3server/modules/virtualbox/adapters/ethernet_adapter.py
new file mode 100644
index 00000000..8951ee8d
--- /dev/null
+++ b/gns3server/modules/virtualbox/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):
+ """
+ VirtualBox Ethernet adapter.
+ """
+
+ def __init__(self):
+ Adapter.__init__(self, interfaces=1)
+
+ def __str__(self):
+
+ return "VirtualBox Ethernet adapter"
diff --git a/gns3server/modules/virtualbox/nios/__init__.py b/gns3server/modules/virtualbox/nios/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/gns3server/modules/virtualbox/nios/nio_udp.py b/gns3server/modules/virtualbox/nios/nio_udp.py
new file mode 100644
index 00000000..3142d70e
--- /dev/null
+++ b/gns3server/modules/virtualbox/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):
+ """
+ IOU 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/virtualbox/vboxwrapper_client.py b/gns3server/modules/virtualbox/vboxwrapper_client.py
new file mode 100644
index 00000000..d1d5d0ed
--- /dev/null
+++ b/gns3server/modules/virtualbox/vboxwrapper_client.py
@@ -0,0 +1,357 @@
+# -*- 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 .
+
+"""
+Client to VirtualBox wrapper.
+"""
+
+import os
+import time
+import subprocess
+import tempfile
+import socket
+import re
+
+from .virtualbox_error import VirtualBoxError
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class VboxWrapperClient(object):
+ """
+ VirtualBox Wrapper client.
+
+ :param path: path to VirtualBox wrapper executable
+ :param working_dir: working directory
+ :param port: port
+ :param host: host/address
+ """
+
+ # Used to parse the VirtualBox wrapper response codes
+ error_re = re.compile(r"""^2[0-9]{2}-""")
+ success_re = re.compile(r"""^1[0-9]{2}\s{1}""")
+
+ def __init__(self, path, working_dir, host, port=11525, timeout=30.0):
+
+ self._path = path
+ self._command = []
+ self._process = None
+ self._stdout_file = ""
+ self._started = False
+ self._host = host
+ self._port = port
+ self._timeout = timeout
+ self._socket = None
+
+ @property
+ def started(self):
+ """
+ Returns either VirtualBox wrapper has been started or not.
+
+ :returns: boolean
+ """
+
+ return self._started
+
+ @property
+ def path(self):
+ """
+ Returns the path to the VirtualBox wrapper executable.
+
+ :returns: path to VirtualBox wrapper
+ """
+
+ return self._path
+
+ @path.setter
+ def path(self, path):
+ """
+ Sets the path to the VirtualBox wrapper executable.
+
+ :param path: path to VirtualBox wrapper
+ """
+
+ self._path = path
+
+ @property
+ def port(self):
+ """
+ Returns the port used to start the VirtualBox wrapper.
+
+ :returns: port number (integer)
+ """
+
+ return self._port
+
+ @port.setter
+ def port(self, port):
+ """
+ Sets the port used to start the VirtualBox wrapper.
+
+ :param port: port number (integer)
+ """
+
+ self._port = port
+
+ @property
+ def host(self):
+ """
+ Returns the host (binding) used to start the VirtualBox wrapper.
+
+ :returns: host/address (string)
+ """
+
+ return self._host
+
+ @host.setter
+ def host(self, host):
+ """
+ Sets the host (binding) used to start the VirtualBox wrapper.
+
+ :param host: host/address (string)
+ """
+
+ self._host = host
+
+ def start(self):
+ """
+ Starts the VirtualBox wrapper process.
+ """
+
+ self._command = self._build_command()
+ try:
+ log.info("starting VirtualBox wrapper: {}".format(self._command))
+ with tempfile.NamedTemporaryFile(delete=False) as fd:
+ self._stdout_file = fd.name
+ log.info("VirtualBox wrapper process logging to {}".format(fd.name))
+ self._process = subprocess.Popen(self._command,
+ stdout=fd,
+ stderr=subprocess.STDOUT,
+ cwd=self._working_dir)
+ log.info("VirtualBox wrapper started PID={}".format(self._process.pid))
+ self._started = True
+ except OSError as e:
+ log.error("could not start VirtualBox wrapper: {}".format(e))
+ raise VirtualBoxError("could not start VirtualBox wrapper: {}".format(e))
+
+ def stop(self):
+ """
+ Stops the VirtualBox wrapper process.
+ """
+
+ if self.is_running():
+ self.send("hypervisor stop")
+ self._socket.shutdown(socket.SHUT_RDWR)
+ self._socket.close()
+ self._socket = None
+ log.info("stopping VirtualBox wrapper PID={}".format(self._process.pid))
+ try:
+ # give some time for the VirtualBox wrapper to properly stop.
+ time.sleep(0.01)
+ self._process.terminate()
+ self._process.wait(1)
+ except subprocess.TimeoutExpired:
+ self._process.kill()
+ if self._process.poll() is None:
+ log.warn("VirtualBox wrapper process {} is still running".format(self._process.pid))
+
+ if self._stdout_file and os.access(self._stdout_file, os.W_OK):
+ try:
+ os.remove(self._stdout_file)
+ except OSError as e:
+ log.warning("could not delete temporary VirtualBox wrapper log file: {}".format(e))
+ self._started = False
+
+ def read_stdout(self):
+ """
+ Reads the standard output of the VirtualBox wrapper process.
+ Only use when the process has been stopped or has crashed.
+ """
+
+ output = ""
+ if self._stdout_file and os.access(self._stdout_file, os.R_OK):
+ 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 process is running
+
+ :returns: True or False
+ """
+
+ if self._process and self._process.poll() is None:
+ return True
+ return False
+
+ def _build_command(self):
+ """
+ Command to start the VirtualBox wrapper process.
+ (to be passed to subprocess.Popen())
+ """
+
+ command = [self._path]
+ #if self._host != "0.0.0.0" and self._host != "::":
+ # command.extend(["-H", "{}:{}".format(self._host, self._port)])
+ #else:
+ # command.extend(["-H", str(self._port)])
+ return command
+
+ def connect(self):
+ """
+ Connects to the VirtualBox wrapper.
+ """
+
+ # connect to a local address by default
+ # if listening to all addresses (IPv4 or IPv6)
+ if self._host == "0.0.0.0":
+ host = "127.0.0.1"
+ elif self._host == "::":
+ host = "::1"
+ else:
+ host = self._host
+
+ try:
+ self._socket = socket.create_connection((host, self._port), self._timeout)
+ except OSError as e:
+ raise VirtualBoxError("Could not connect to the VirtualBox wrapper: {}".format(e))
+
+ def reset(self):
+ """
+ Resets the VirtualBox wrapper (used to get an empty configuration).
+ """
+
+ pass
+
+ @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 the VirtualBox wrapper.
+
+ :param working_dir: path to the working directory
+ """
+
+ # encase working_dir in quotes to protect spaces in the path
+ #self.send("hypervisor working_dir {}".format('"' + working_dir + '"'))
+ self._working_dir = working_dir
+ log.debug("working directory set to {}".format(self._working_dir))
+
+ @property
+ def socket(self):
+ """
+ Returns the current socket used to communicate with the VirtualBox wrapper.
+
+ :returns: socket instance
+ """
+
+ assert self._socket
+ return self._socket
+
+ def send(self, command):
+ """
+ Sends commands to the VirtualBox wrapper.
+
+ :param command: a VirtualBox wrapper command
+
+ :returns: results as a list
+ """
+
+ # VirtualBox wrapper responses are of the form:
+ # 1xx yyyyyy\r\n
+ # 1xx yyyyyy\r\n
+ # ...
+ # 100-yyyy\r\n
+ # or
+ # 2xx-yyyy\r\n
+ #
+ # Where 1xx is a code from 100-199 for a success or 200-299 for an error
+ # The result might be multiple lines and might be less than the buffer size
+ # but still have more data. The only thing we know for sure is the last line
+ # will begin with '100-' or a '2xx-' and end with '\r\n'
+
+ if not self._socket:
+ raise VirtualBoxError("Not connected")
+
+ try:
+ command = command.strip() + '\n'
+ log.debug("sending {}".format(command))
+ self.socket.sendall(command.encode('utf-8'))
+ except OSError as e:
+ raise VirtualBoxError("Lost communication with {host}:{port} :{error}"
+ .format(host=self._host, port=self._port, error=e))
+
+ # Now retrieve the result
+ data = []
+ buf = ''
+ while True:
+ try:
+ chunk = self.socket.recv(1024)
+ buf += chunk.decode("utf-8")
+ except OSError as e:
+ raise VirtualBoxError("Communication timed out with {host}:{port} :{error}"
+ .format(host=self._host, port=self._port, error=e))
+
+ # If the buffer doesn't end in '\n' then we can't be done
+ try:
+ if buf[-1] != '\n':
+ continue
+ except IndexError:
+ raise VirtualBoxError("Could not communicate with {host}:{port}"
+ .format(host=self._host, port=self._port))
+
+ data += buf.split('\r\n')
+ if data[-1] == '':
+ data.pop()
+ buf = ''
+
+ if len(data) == 0:
+ raise VirtualBoxError("no data returned from {host}:{port}"
+ .format(host=self._host, port=self._port))
+
+ # Does it contain an error code?
+ if self.error_re.search(data[-1]):
+ raise VirtualBoxError(data[-1][4:])
+
+ # Or does the last line begin with '100-'? Then we are done!
+ if data[-1][:4] == '100-':
+ data[-1] = data[-1][4:]
+ if data[-1] == 'OK':
+ data.pop()
+ break
+
+ # Remove success responses codes
+ for index in range(len(data)):
+ if self.success_re.search(data[index]):
+ data[index] = data[index][4:]
+
+ log.debug("returned result {}".format(data))
+ return data
diff --git a/gns3server/modules/virtualbox/virtualbox_error.py b/gns3server/modules/virtualbox/virtualbox_error.py
new file mode 100644
index 00000000..74b05171
--- /dev/null
+++ b/gns3server/modules/virtualbox/virtualbox_error.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Custom exceptions for VirtualBox module.
+"""
+
+
+class VirtualBoxError(Exception):
+
+ def __init__(self, message, original_exception=None):
+
+ Exception.__init__(self, message)
+ if isinstance(message, Exception):
+ message = str(message)
+ self._message = message
+ self._original_exception = original_exception
+
+ def __repr__(self):
+
+ return self._message
+
+ def __str__(self):
+
+ return self._message
diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py
new file mode 100644
index 00000000..aedc5d8a
--- /dev/null
+++ b/gns3server/modules/virtualbox/virtualbox_vm.py
@@ -0,0 +1,318 @@
+# -*- 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 .
+
+"""
+VirtualBox VM instance.
+"""
+
+import os
+import shutil
+
+from pkg_resources import parse_version
+from .virtualbox_error import VirtualBoxError
+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 VirtualBoxVM(object):
+ """
+ VirtualBox VM implementation.
+
+ :param name: name of this VirtualBox VM
+ :param working_dir: path to a working directory
+ :param host: host/address to bind for console and UDP connections
+ :param vbox_id: VirtalBox VM instance ID
+ :param console: TCP console port
+ :param console_start_port_range: TCP console port range start
+ :param console_end_port_range: TCP console port range end
+ """
+
+ _instances = []
+ _allocated_console_ports = []
+
+ def __init__(self,
+ name,
+ path,
+ working_dir,
+ host="127.0.0.1",
+ vbox_id=None,
+ console=None,
+ console_start_port_range=4512,
+ console_end_port_range=5000):
+
+ if not vbox_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 VirtualBoxError("Maximum number of VirtualBox VM instances reached")
+ else:
+ if vbox_id in self._instances:
+ raise VirtualBoxError("VirtualBox identifier {} is already used by another VirtualBox VM instance".format(vbox_id))
+ self._id = vbox_id
+ self._instances.append(self._id)
+
+ self._name = name
+ self._console = console
+ self._working_dir = None
+ self._host = host
+ self._command = []
+ self._vboxwrapper_process = None
+ self._vboxwrapper_stdout_file = ""
+ self._host = "127.0.0.1"
+ self._started = False
+ self._console_start_port_range = console_start_port_range
+ self._console_end_port_range = console_end_port_range
+
+ # VirtualBox settings
+ self._ethernet_adapter = EthernetAdapter() # one adapter with 1 Ethernet interface
+
+ working_dir_path = os.path.join(working_dir, "vbox", "vm-{}".format(self._id))
+
+ if vbox_id and not os.path.isdir(working_dir_path):
+ raise VirtualBoxError("Working directory {} doesn't exist".format(working_dir_path))
+
+ # create the device own working directory
+ self.working_dir = working_dir_path
+
+ if not self._console:
+ # allocate a console port
+ try:
+ self._console = find_unused_port(self._console_start_port_range,
+ self._console_end_port_range,
+ self._host,
+ ignore_ports=self._allocated_console_ports)
+ except Exception as e:
+ raise VirtualBoxError(e)
+
+ if self._console in self._allocated_console_ports:
+ raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console))
+ self._allocated_console_ports.append(self._console)
+
+ log.info("VirtualBox 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 VirtualBox VM.
+
+ :returns: default values (dictionary)
+ """
+
+ vbox_defaults = {"name": self._name,
+ "console": self._console}
+
+ return vbox_defaults
+
+ @property
+ def id(self):
+ """
+ Returns the unique ID for this VirtualBox VM.
+
+ :returns: id (integer)
+ """
+
+ return self._id
+
+ @classmethod
+ def reset(cls):
+ """
+ Resets allocated instance list.
+ """
+
+ cls._instances.clear()
+ cls._allocated_console_ports.clear()
+
+ @property
+ def name(self):
+ """
+ Returns the name of this VirtualBox VM.
+
+ :returns: name
+ """
+
+ return self._name
+
+ @name.setter
+ def name(self, new_name):
+ """
+ Sets the name of this VirtualBox VM.
+
+ :param new_name: name
+ """
+
+ log.info("VirtualBox 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 VirtualBox VM.
+
+ :param working_dir: path to the working directory
+ """
+
+ try:
+ os.makedirs(working_dir)
+ except FileExistsError:
+ pass
+ except OSError as e:
+ raise VirtualBoxError("Could not create working directory {}: {}".format(working_dir, e))
+
+ self._working_dir = working_dir
+ log.info("VirtualBox 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 VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console))
+
+ self._allocated_console_ports.remove(self._console)
+ self._console = console
+ self._allocated_console_ports.append(self._console)
+ log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name,
+ id=self._id,
+ port=console))
+
+ def delete(self):
+ """
+ Deletes this VirtualBox VM.
+ """
+
+ self.stop()
+ if self._id in self._instances:
+ self._instances.remove(self._id)
+
+ if self.console and self.console in self._allocated_console_ports:
+ self._allocated_console_ports.remove(self.console)
+
+ log.info("VirtualBox VM {name} [id={id}] has been deleted".format(name=self._name,
+ id=self._id))
+
+ def clean_delete(self):
+ """
+ Deletes this VirtualBox VM & all files.
+ """
+
+ self.stop()
+ if self._id in self._instances:
+ self._instances.remove(self._id)
+
+ if self.console:
+ self._allocated_console_ports.remove(self.console)
+
+ try:
+ shutil.rmtree(self._working_dir)
+ except OSError as e:
+ log.error("could not delete VirtualBox VM {name} [id={id}]: {error}".format(name=self._name,
+ id=self._id,
+ error=e))
+ return
+
+ log.info("VirtualBox VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
+ id=self._id))
+
+ def start(self):
+ """
+ Starts this VirtualBox VM.
+ """
+
+ pass
+
+ def stop(self):
+ """
+ Stops this VirtualBox VM.
+ """
+
+ pass
+
+# 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
diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py
index 71709016..61de9308 100644
--- a/gns3server/modules/vpcs/__init__.py
+++ b/gns3server/modules/vpcs/__init__.py
@@ -362,7 +362,7 @@ class VPCS(IModule):
self.send_response(response)
@IModule.route("vpcs.start")
- def vm_start(self, request):
+ def vpcs_start(self, request):
"""
Starts a VPCS instance.
@@ -392,7 +392,7 @@ class VPCS(IModule):
self.send_response(True)
@IModule.route("vpcs.stop")
- def vm_stop(self, request):
+ def vpcs_stop(self, request):
"""
Stops a VPCS instance.
@@ -422,7 +422,7 @@ class VPCS(IModule):
self.send_response(True)
@IModule.route("vpcs.reload")
- def vm_reload(self, request):
+ def vpcs_reload(self, request):
"""
Reloads a VPCS instance.