mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-28 11:18:11 +00:00
Base for VirtualBox support.
This commit is contained in:
parent
88e03ae312
commit
0ef727ce4b
@ -19,9 +19,9 @@ import sys
|
|||||||
from .base import IModule
|
from .base import IModule
|
||||||
from .dynamips import Dynamips
|
from .dynamips import Dynamips
|
||||||
from .vpcs import VPCS
|
from .vpcs import VPCS
|
||||||
|
from .virtualbox import VirtualBox
|
||||||
|
|
||||||
MODULES = [Dynamips]
|
MODULES = [Dynamips, VPCS, VirtualBox]
|
||||||
MODULES.append(VPCS)
|
|
||||||
|
|
||||||
if sys.platform.startswith("linux"):
|
if sys.platform.startswith("linux"):
|
||||||
# IOU runs only on Linux
|
# IOU runs only on Linux
|
||||||
|
540
gns3server/modules/virtualbox/__init__.py
Normal file
540
gns3server/modules/virtualbox/__init__.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
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)
|
0
gns3server/modules/virtualbox/adapters/__init__.py
Normal file
0
gns3server/modules/virtualbox/adapters/__init__.py
Normal file
104
gns3server/modules/virtualbox/adapters/adapter.py
Normal file
104
gns3server/modules/virtualbox/adapters/adapter.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
31
gns3server/modules/virtualbox/adapters/ethernet_adapter.py
Normal file
31
gns3server/modules/virtualbox/adapters/ethernet_adapter.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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"
|
0
gns3server/modules/virtualbox/nios/__init__.py
Normal file
0
gns3server/modules/virtualbox/nios/__init__.py
Normal file
72
gns3server/modules/virtualbox/nios/nio_udp.py
Normal file
72
gns3server/modules/virtualbox/nios/nio_udp.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
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"
|
357
gns3server/modules/virtualbox/vboxwrapper_client.py
Normal file
357
gns3server/modules/virtualbox/vboxwrapper_client.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
39
gns3server/modules/virtualbox/virtualbox_error.py
Normal file
39
gns3server/modules/virtualbox/virtualbox_error.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
318
gns3server/modules/virtualbox/virtualbox_vm.py
Normal file
318
gns3server/modules/virtualbox/virtualbox_vm.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
@ -362,7 +362,7 @@ class VPCS(IModule):
|
|||||||
self.send_response(response)
|
self.send_response(response)
|
||||||
|
|
||||||
@IModule.route("vpcs.start")
|
@IModule.route("vpcs.start")
|
||||||
def vm_start(self, request):
|
def vpcs_start(self, request):
|
||||||
"""
|
"""
|
||||||
Starts a VPCS instance.
|
Starts a VPCS instance.
|
||||||
|
|
||||||
@ -392,7 +392,7 @@ class VPCS(IModule):
|
|||||||
self.send_response(True)
|
self.send_response(True)
|
||||||
|
|
||||||
@IModule.route("vpcs.stop")
|
@IModule.route("vpcs.stop")
|
||||||
def vm_stop(self, request):
|
def vpcs_stop(self, request):
|
||||||
"""
|
"""
|
||||||
Stops a VPCS instance.
|
Stops a VPCS instance.
|
||||||
|
|
||||||
@ -422,7 +422,7 @@ class VPCS(IModule):
|
|||||||
self.send_response(True)
|
self.send_response(True)
|
||||||
|
|
||||||
@IModule.route("vpcs.reload")
|
@IModule.route("vpcs.reload")
|
||||||
def vm_reload(self, request):
|
def vpcs_reload(self, request):
|
||||||
"""
|
"""
|
||||||
Reloads a VPCS instance.
|
Reloads a VPCS instance.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user