1
0
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:
grossmj 2014-07-12 13:18:25 -06:00
parent 88e03ae312
commit 0ef727ce4b
11 changed files with 1466 additions and 5 deletions

View File

@ -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

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

View 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

View 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"

View 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"

View 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

View 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

View 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

View File

@ -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.