mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
UUID support for VMs.
Basic VirtualBox support (create, start and stop). Some refactoring for BaseVM class. Updated CURL command in tests.
This commit is contained in:
1 parent
fe22576ae2
commit
7fff25a9a9
15 files changed
+397
-609
No files matched your search
@@ -17,8 +17,9 @@
|
||||
|
||||
import sys
|
||||
from .vpcs import VPCS
|
||||
from .virtualbox import VirtualBox
|
||||
|
||||
MODULES = [VPCS]
|
||||
MODULES = [VPCS, VirtualBox]
|
||||
|
||||
#if sys.platform.startswith("linux"):
|
||||
# # IOU runs only on Linux
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
from .vm_error import VMError
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
|
||||
class BaseManager:
|
||||
@@ -63,44 +63,52 @@ class BaseManager:
|
||||
@classmethod
|
||||
@asyncio.coroutine # FIXME: why coroutine?
|
||||
def destroy(cls):
|
||||
|
||||
cls._instance = None
|
||||
|
||||
def get_vm(self, vm_id):
|
||||
def get_vm(self, uuid):
|
||||
"""
|
||||
Returns a VM instance.
|
||||
|
||||
:param vm_id: VM identifier
|
||||
:param uuid: VM UUID
|
||||
|
||||
:returns: VM instance
|
||||
"""
|
||||
|
||||
if vm_id not in self._vms:
|
||||
raise aiohttp.web.HTTPNotFound(text="ID {} doesn't exist".format(vm_id))
|
||||
return self._vms[vm_id]
|
||||
try:
|
||||
UUID(uuid, version=4)
|
||||
except ValueError:
|
||||
raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid))
|
||||
|
||||
if uuid not in self._vms:
|
||||
raise aiohttp.web.HTTPNotFound(text="UUID {} doesn't exist".format(uuid))
|
||||
return self._vms[uuid]
|
||||
|
||||
@asyncio.coroutine
|
||||
def create_vm(self, vmname, identifier=None):
|
||||
if not identifier:
|
||||
for i in range(1, 1024):
|
||||
if i not in self._vms:
|
||||
identifier = i
|
||||
break
|
||||
if identifier == 0:
|
||||
raise VMError("Maximum number of VM instances reached")
|
||||
else:
|
||||
if identifier in self._vms:
|
||||
raise VMError("VM identifier {} is already used by another VM instance".format(identifier))
|
||||
vm = self._VM_CLASS(vmname, identifier, self)
|
||||
yield from vm.wait_for_creation()
|
||||
self._vms[vm.id] = vm
|
||||
def create_vm(self, name, uuid=None):
|
||||
|
||||
#TODO: support for old projects with normal IDs.
|
||||
|
||||
#TODO: supports specific args: pass kwargs to VM_CLASS?
|
||||
|
||||
if not uuid:
|
||||
uuid = str(uuid4())
|
||||
|
||||
vm = self._VM_CLASS(name, uuid, self)
|
||||
future = vm.create()
|
||||
if isinstance(future, asyncio.Future):
|
||||
yield from future
|
||||
self._vms[vm.uuid] = vm
|
||||
return vm
|
||||
|
||||
@asyncio.coroutine
|
||||
def start_vm(self, vm_id):
|
||||
vm = self.get_vm(vm_id)
|
||||
def start_vm(self, uuid):
|
||||
|
||||
vm = self.get_vm(uuid)
|
||||
yield from vm.start()
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop_vm(self, vm_id):
|
||||
vm = self.get_vm(vm_id)
|
||||
def stop_vm(self, uuid):
|
||||
|
||||
vm = self.get_vm(uuid)
|
||||
yield from vm.stop()
|
||||
@@ -15,9 +15,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import asyncio
|
||||
from .vm_error import VMError
|
||||
from ..config import Config
|
||||
|
||||
import logging
|
||||
@@ -26,64 +23,62 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class BaseVM:
|
||||
|
||||
def __init__(self, name, identifier, manager):
|
||||
def __init__(self, name, uuid, manager):
|
||||
|
||||
self._name = name
|
||||
self._id = identifier
|
||||
self._created = asyncio.Future()
|
||||
self._uuid = uuid
|
||||
self._manager = manager
|
||||
self._config = Config.instance()
|
||||
asyncio.async(self._create())
|
||||
log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__,
|
||||
name=self._name,
|
||||
id=self._id))
|
||||
|
||||
#TODO: When delete release console ports
|
||||
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""
|
||||
Returns the unique ID for this VM.
|
||||
|
||||
:returns: id (integer)
|
||||
"""
|
||||
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Returns the name for this VM.
|
||||
|
||||
:returns: name (string)
|
||||
:returns: name
|
||||
"""
|
||||
|
||||
return self._name
|
||||
|
||||
@asyncio.coroutine
|
||||
def _execute(self, command):
|
||||
@name.setter
|
||||
def name(self, new_name):
|
||||
"""
|
||||
Called when we receive an event.
|
||||
Sets the name of this VM.
|
||||
|
||||
:param new_name: name
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
self._name = new_name
|
||||
|
||||
@asyncio.coroutine
|
||||
def _create(self):
|
||||
@property
|
||||
def uuid(self):
|
||||
"""
|
||||
Called when the run module is created and ready to receive
|
||||
commands. It's asynchronous.
|
||||
Returns the UUID for this VM.
|
||||
|
||||
:returns: uuid (string)
|
||||
"""
|
||||
self._created.set_result(True)
|
||||
log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__,
|
||||
name=self._name,
|
||||
id=self._id))
|
||||
|
||||
def wait_for_creation(self):
|
||||
return self._created
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def manager(self):
|
||||
"""
|
||||
Returns the manager for this VM.
|
||||
|
||||
:returns: instance of manager
|
||||
"""
|
||||
|
||||
return self._manager
|
||||
|
||||
def create(self):
|
||||
"""
|
||||
Creates the VM.
|
||||
"""
|
||||
|
||||
return
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self):
|
||||
"""
|
||||
Starts the VM process.
|
||||
@@ -91,8 +86,6 @@ class BaseVM:
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop(self):
|
||||
"""
|
||||
Starts the VM process.
|
||||
|
||||
@@ -43,13 +43,23 @@ class Project:
|
||||
self._path = os.path.join(self._location, self._uuid)
|
||||
if os.path.exists(self._path) is False:
|
||||
os.mkdir(self._path)
|
||||
os.mkdir(os.path.join(self._path, 'files'))
|
||||
os.mkdir(os.path.join(self._path, "files"))
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
|
||||
return self._location
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
|
||||
return self._path
|
||||
|
||||
def __json__(self):
|
||||
|
||||
return {
|
||||
|
||||
@@ -28,11 +28,13 @@ import tempfile
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
from .virtualbox_error import VirtualBoxError
|
||||
from ..adapters.ethernet_adapter import EthernetAdapter
|
||||
from ..attic import find_unused_port
|
||||
from .telnet_server import TelnetServer
|
||||
from ..base_vm import BaseVM
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
import msvcrt
|
||||
@@ -42,55 +44,32 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VirtualBoxVM(object):
|
||||
class VirtualBoxVM(BaseVM):
|
||||
"""
|
||||
VirtualBox VM implementation.
|
||||
|
||||
:param vboxmanage_path: path to the VBoxManage tool
|
||||
:param name: name of this VirtualBox VM
|
||||
:param vmname: name of this VirtualBox VM in VirtualBox itself
|
||||
:param linked_clone: flag if a linked clone must be created
|
||||
:param working_dir: path to a working directory
|
||||
:param vbox_id: VirtalBox VM instance ID
|
||||
:param console: TCP console port
|
||||
:param console_host: IP address to bind for console connections
|
||||
:param console_start_port_range: TCP console port range start
|
||||
:param console_end_port_range: TCP console port range end
|
||||
"""
|
||||
|
||||
_instances = []
|
||||
_allocated_console_ports = []
|
||||
|
||||
def __init__(self,
|
||||
vboxmanage_path,
|
||||
vbox_user,
|
||||
name,
|
||||
vmname,
|
||||
linked_clone,
|
||||
working_dir,
|
||||
vbox_id=None,
|
||||
console=None,
|
||||
console_host="0.0.0.0",
|
||||
console_start_port_range=4512,
|
||||
console_end_port_range=5000):
|
||||
def __init__(self, name, uuid, manager):
|
||||
|
||||
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
|
||||
super().__init__(name, uuid, manager)
|
||||
|
||||
if self._id == 0:
|
||||
raise VirtualBoxError("Maximum number of VirtualBox VM instances reached")
|
||||
self._system_properties = {}
|
||||
|
||||
#FIXME: harcoded values
|
||||
if sys.platform.startswith("win"):
|
||||
self._vboxmanage_path = r"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe"
|
||||
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._vboxmanage_path = "/usr/bin/vboxmanage"
|
||||
|
||||
self._queue = asyncio.Queue()
|
||||
self._created = asyncio.Future()
|
||||
self._worker = asyncio.async(self._run())
|
||||
|
||||
return
|
||||
|
||||
self._name = name
|
||||
self._linked_clone = linked_clone
|
||||
self._working_dir = None
|
||||
self._command = []
|
||||
@@ -158,6 +137,82 @@ class VirtualBoxVM(object):
|
||||
log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name,
|
||||
id=self._id))
|
||||
|
||||
@asyncio.coroutine
|
||||
def _execute(self, subcommand, args, timeout=60):
|
||||
|
||||
command = [self._vboxmanage_path, "--nologo", subcommand]
|
||||
command.extend(args)
|
||||
try:
|
||||
process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise VirtualBoxError("Could not execute VBoxManage: {}".format(e))
|
||||
|
||||
try:
|
||||
stdout_data, stderr_data = yield from asyncio.wait_for(process.communicate(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout))
|
||||
|
||||
if process.returncode:
|
||||
# only the first line of the output is useful
|
||||
vboxmanage_error = stderr_data.decode("utf-8", errors="ignore").splitlines()[0]
|
||||
raise VirtualBoxError(vboxmanage_error)
|
||||
|
||||
return stdout_data.decode("utf-8", errors="ignore").splitlines()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _get_system_properties(self):
|
||||
|
||||
properties = yield from self._execute("list", ["systemproperties"])
|
||||
for prop in properties:
|
||||
try:
|
||||
name, value = prop.split(':', 1)
|
||||
except ValueError:
|
||||
continue
|
||||
self._system_properties[name.strip()] = value.strip()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _run(self):
|
||||
|
||||
try:
|
||||
yield from self._get_system_properties()
|
||||
self._created.set_result(True)
|
||||
except VirtualBoxError as e:
|
||||
self._created.set_exception(e)
|
||||
return
|
||||
|
||||
while True:
|
||||
future, subcommand, args = yield from self._queue.get()
|
||||
try:
|
||||
yield from self._execute(subcommand, args)
|
||||
future.set_result(True)
|
||||
except VirtualBoxError as e:
|
||||
future.set_exception(e)
|
||||
|
||||
def create(self):
|
||||
|
||||
return self._created
|
||||
|
||||
def _put(self, item):
|
||||
|
||||
try:
|
||||
self._queue.put_nowait(item)
|
||||
except asyncio.qeues.QueueFull:
|
||||
raise VirtualBoxError("Queue is full")
|
||||
|
||||
def start(self):
|
||||
|
||||
args = [self._name]
|
||||
future = asyncio.Future()
|
||||
self._put((future, "startvm", args))
|
||||
return future
|
||||
|
||||
def stop(self):
|
||||
|
||||
args = [self._name, "poweroff"]
|
||||
future = asyncio.Future()
|
||||
self._put((future, "controlvm", args))
|
||||
return future
|
||||
|
||||
def defaults(self):
|
||||
"""
|
||||
Returns all the default attribute values for this VirtualBox VM.
|
||||
@@ -176,49 +231,6 @@ class VirtualBoxVM(object):
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -540,7 +552,7 @@ class VirtualBoxVM(object):
|
||||
id=self._id,
|
||||
adapter_type=adapter_type))
|
||||
|
||||
def _execute(self, subcommand, args, timeout=60):
|
||||
def _old_execute(self, subcommand, args, timeout=60):
|
||||
"""
|
||||
Executes a command with VBoxManage.
|
||||
|
||||
@@ -831,7 +843,7 @@ class VirtualBoxVM(object):
|
||||
self._serial_pipe.close()
|
||||
self._serial_pipe = None
|
||||
|
||||
def start(self):
|
||||
def old_start(self):
|
||||
"""
|
||||
Starts this VirtualBox VM.
|
||||
"""
|
||||
@@ -864,7 +876,7 @@ class VirtualBoxVM(object):
|
||||
if self._enable_remote_console:
|
||||
self._start_remote_console()
|
||||
|
||||
def stop(self):
|
||||
def old_stop(self):
|
||||
"""
|
||||
Stops this VirtualBox VM.
|
||||
"""
|
||||
|
||||
@@ -47,14 +47,14 @@ class VPCSDevice(BaseVM):
|
||||
VPCS device implementation.
|
||||
|
||||
:param name: name of this VPCS device
|
||||
:param vpcs_id: VPCS instance ID
|
||||
:param uuid: VPCS instance UUID
|
||||
:param manager: parent VM Manager
|
||||
:param working_dir: path to a working directory
|
||||
:param console: TCP console port
|
||||
"""
|
||||
def __init__(self, name, vpcs_id, manager, working_dir=None, console=None):
|
||||
def __init__(self, name, uuid, manager, working_dir=None, console=None):
|
||||
|
||||
super().__init__(name, vpcs_id, manager)
|
||||
super().__init__(name, uuid, manager)
|
||||
|
||||
# TODO: Hardcodded for testing
|
||||
#self._working_dir = working_dir
|
||||
@@ -120,17 +120,8 @@ class VPCSDevice(BaseVM):
|
||||
|
||||
return self._console
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Returns the name of this VPCS device.
|
||||
|
||||
:returns: name
|
||||
"""
|
||||
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
#FIXME: correct way to subclass a property?
|
||||
@BaseVM.name.setter
|
||||
def name(self, new_name):
|
||||
"""
|
||||
Sets the name of this VPCS device.
|
||||
@@ -151,10 +142,10 @@ class VPCSDevice(BaseVM):
|
||||
except OSError as e:
|
||||
raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e))
|
||||
|
||||
log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name,
|
||||
id=self._id,
|
||||
new_name=new_name))
|
||||
self._name = new_name
|
||||
log.info("VPCS {name} [{uuid}]: renamed to {new_name}".format(name=self._name,
|
||||
uuid=self.uuid,
|
||||
new_name=new_name))
|
||||
BaseVM.name = new_name
|
||||
|
||||
def _check_vpcs_version(self):
|
||||
"""
|
||||
@@ -197,7 +188,7 @@ class VPCSDevice(BaseVM):
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=self._working_dir,
|
||||
creationflags=flags)
|
||||
log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid))
|
||||
log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid))
|
||||
self._started = True
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
vpcs_stdout = self.read_vpcs_stdout()
|
||||
@@ -212,7 +203,7 @@ class VPCSDevice(BaseVM):
|
||||
|
||||
# stop the VPCS process
|
||||
if self.is_running():
|
||||
log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid))
|
||||
log.info("stopping VPCS instance {} PID={}".format(self.name, self._process.pid))
|
||||
if sys.platform.startswith("win32"):
|
||||
self._process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
@@ -283,10 +274,10 @@ class VPCSDevice(BaseVM):
|
||||
|
||||
|
||||
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))
|
||||
log.info("VPCS {name} {uuid}]: {nio} added to port {port_id}".format(name=self._name,
|
||||
uuid=self.uuid,
|
||||
nio=nio,
|
||||
port_id=port_id))
|
||||
return nio
|
||||
|
||||
def port_remove_nio_binding(self, port_id):
|
||||
@@ -304,10 +295,10 @@ class VPCSDevice(BaseVM):
|
||||
|
||||
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))
|
||||
log.info("VPCS {name} [{uuid}]: {nio} removed from port {port_id}".format(name=self._name,
|
||||
uuid=self.uuid,
|
||||
nio=nio,
|
||||
port_id=port_id))
|
||||
return nio
|
||||
|
||||
def _build_command(self):
|
||||
@@ -364,7 +355,8 @@ class VPCSDevice(BaseVM):
|
||||
command.extend(["-e"])
|
||||
command.extend(["-d", nio.tap_device])
|
||||
|
||||
command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset
|
||||
#FIXME: find workaround
|
||||
#command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset
|
||||
command.extend(["-i", "1"]) # option to start only one VPC instance
|
||||
command.extend(["-F"]) # option to avoid the daemonization of VPCS
|
||||
if self._script_file:
|
||||
@@ -390,6 +382,6 @@ class VPCSDevice(BaseVM):
|
||||
"""
|
||||
|
||||
self._script_file = script_file
|
||||
log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name,
|
||||
id=self._id,
|
||||
config=self._script_file))
|
||||
log.info("VPCS {name} [{uuid}]: script_file set to {config}".format(name=self._name,
|
||||
uuid=self.uuid,
|
||||
config=self._script_file))
|
||||
Reference in new issue
Block a user