mirror of https://github.com/GNS3/gns3-server
commit
ee88d6f808
@ -0,0 +1,657 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
QEMU server module.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import socket
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
from gns3server.modules import IModule
|
||||
from gns3server.config import Config
|
||||
from .qemu_vm import QemuVM
|
||||
from .qemu_error import QemuError
|
||||
from .nios.nio_udp import NIO_UDP
|
||||
from ..attic import find_unused_port
|
||||
|
||||
from .schemas import QEMU_CREATE_SCHEMA
|
||||
from .schemas import QEMU_DELETE_SCHEMA
|
||||
from .schemas import QEMU_UPDATE_SCHEMA
|
||||
from .schemas import QEMU_START_SCHEMA
|
||||
from .schemas import QEMU_STOP_SCHEMA
|
||||
from .schemas import QEMU_SUSPEND_SCHEMA
|
||||
from .schemas import QEMU_RELOAD_SCHEMA
|
||||
from .schemas import QEMU_ALLOCATE_UDP_PORT_SCHEMA
|
||||
from .schemas import QEMU_ADD_NIO_SCHEMA
|
||||
from .schemas import QEMU_DELETE_NIO_SCHEMA
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Qemu(IModule):
|
||||
"""
|
||||
QEMU 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._qemu_instances = {}
|
||||
|
||||
config = Config.instance()
|
||||
qemu_config = config.get_section_config(name.upper())
|
||||
self._console_start_port_range = qemu_config.get("console_start_port_range", 5001)
|
||||
self._console_end_port_range = qemu_config.get("console_end_port_range", 5500)
|
||||
self._allocated_udp_ports = []
|
||||
self._udp_start_port_range = qemu_config.get("udp_start_port_range", 40001)
|
||||
self._udp_end_port_range = qemu_config.get("udp_end_port_range", 45500)
|
||||
self._host = qemu_config.get("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 QEMU instances
|
||||
for qemu_id in self._qemu_instances:
|
||||
qemu_instance = self._qemu_instances[qemu_id]
|
||||
qemu_instance.delete()
|
||||
|
||||
IModule.stop(self, signum) # this will stop the I/O loop
|
||||
|
||||
def get_qemu_instance(self, qemu_id):
|
||||
"""
|
||||
Returns a QEMU VM instance.
|
||||
|
||||
:param qemu_id: QEMU VM identifier
|
||||
|
||||
:returns: QemuVM instance
|
||||
"""
|
||||
|
||||
if qemu_id not in self._qemu_instances:
|
||||
log.debug("QEMU VM ID {} doesn't exist".format(qemu_id), exc_info=1)
|
||||
self.send_custom_error("QEMU VM ID {} doesn't exist".format(qemu_id))
|
||||
return None
|
||||
return self._qemu_instances[qemu_id]
|
||||
|
||||
@IModule.route("qemu.reset")
|
||||
def reset(self, request):
|
||||
"""
|
||||
Resets the module.
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# delete all QEMU instances
|
||||
for qemu_id in self._qemu_instances:
|
||||
qemu_instance = self._qemu_instances[qemu_id]
|
||||
qemu_instance.delete()
|
||||
|
||||
# resets the instance IDs
|
||||
QemuVM.reset()
|
||||
|
||||
self._qemu_instances.clear()
|
||||
self._allocated_udp_ports.clear()
|
||||
|
||||
log.info("QEMU module has been reset")
|
||||
|
||||
@IModule.route("qemu.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 qemu_id in self._qemu_instances:
|
||||
qemu_instance = self._qemu_instances[qemu_id]
|
||||
qemu_instance.working_dir = os.path.join(self._working_dir, "qemu", "vm-{}".format(qemu_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("qemu.create")
|
||||
def qemu_create(self, request):
|
||||
"""
|
||||
Creates a new QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- name (QEMU VM name)
|
||||
- qemu_path (path to the Qemu binary)
|
||||
|
||||
Optional request parameters:
|
||||
- console (QEMU VM console port)
|
||||
|
||||
Response parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
- name (QEMU VM name)
|
||||
- default settings
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_CREATE_SCHEMA):
|
||||
return
|
||||
|
||||
name = request["name"]
|
||||
qemu_path = request["qemu_path"]
|
||||
console = request.get("console")
|
||||
qemu_id = request.get("qemu_id")
|
||||
|
||||
try:
|
||||
qemu_instance = QemuVM(name,
|
||||
qemu_path,
|
||||
self._working_dir,
|
||||
self._host,
|
||||
qemu_id,
|
||||
console,
|
||||
self._console_start_port_range,
|
||||
self._console_end_port_range)
|
||||
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
response = {"name": qemu_instance.name,
|
||||
"id": qemu_instance.id}
|
||||
|
||||
defaults = qemu_instance.defaults()
|
||||
response.update(defaults)
|
||||
self._qemu_instances[qemu_instance.id] = qemu_instance
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("qemu.delete")
|
||||
def qemu_delete(self, request):
|
||||
"""
|
||||
Deletes a QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
|
||||
Response parameter:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_DELETE_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
qemu_instance.clean_delete()
|
||||
del self._qemu_instances[request["id"]]
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("qemu.update")
|
||||
def qemu_update(self, request):
|
||||
"""
|
||||
Updates a QEMU VM instance
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU 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, QEMU_UPDATE_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
# update the QEMU VM settings
|
||||
response = {}
|
||||
for name, value in request.items():
|
||||
if hasattr(qemu_instance, name) and getattr(qemu_instance, name) != value:
|
||||
try:
|
||||
setattr(qemu_instance, name, value)
|
||||
response[name] = value
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("qemu.start")
|
||||
def qemu_start(self, request):
|
||||
"""
|
||||
Starts a QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
|
||||
Response parameters:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_START_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
qemu_instance.start()
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("qemu.stop")
|
||||
def qemu_stop(self, request):
|
||||
"""
|
||||
Stops a QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
|
||||
Response parameters:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_STOP_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
qemu_instance.stop()
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("qemu.reload")
|
||||
def qemu_reload(self, request):
|
||||
"""
|
||||
Reloads a QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU VM identifier)
|
||||
|
||||
Response parameters:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_RELOAD_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
qemu_instance.reload()
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("qemu.stop")
|
||||
def qemu_stop(self, request):
|
||||
"""
|
||||
Stops a QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
|
||||
Response parameters:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_STOP_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
qemu_instance.stop()
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("qemu.suspend")
|
||||
def qemu_suspend(self, request):
|
||||
"""
|
||||
Suspends a QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
|
||||
Response parameters:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_SUSPEND_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
try:
|
||||
qemu_instance.suspend()
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@IModule.route("qemu.allocate_udp_port")
|
||||
def allocate_udp_port(self, request):
|
||||
"""
|
||||
Allocates a UDP port in order to create an UDP NIO.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU 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, QEMU_ALLOCATE_UDP_PORT_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_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(qemu_instance.name,
|
||||
qemu_instance.id,
|
||||
port,
|
||||
self._host))
|
||||
|
||||
response = {"lport": port,
|
||||
"port_id": request["port_id"]}
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("qemu.add_nio")
|
||||
def add_nio(self, request):
|
||||
"""
|
||||
Adds an NIO (Network Input/Output) for a QEMU VM instance.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU 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, QEMU_ADD_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_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 QemuError("Could not create an UDP connection to {}:{}: {}".format(rhost, rport, e))
|
||||
nio = NIO_UDP(lport, rhost, rport)
|
||||
if not nio:
|
||||
raise QemuError("Requested NIO does not exist or is not supported: {}".format(request["nio"]["type"]))
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
try:
|
||||
qemu_instance.port_add_nio_binding(port, nio)
|
||||
except QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response({"port_id": request["port_id"]})
|
||||
|
||||
@IModule.route("qemu.delete_nio")
|
||||
def delete_nio(self, request):
|
||||
"""
|
||||
Deletes an NIO (Network Input/Output).
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (QEMU VM instance identifier)
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
- True on success
|
||||
|
||||
:param request: JSON request
|
||||
"""
|
||||
|
||||
# validate the request
|
||||
if not self.validate_request(request, QEMU_DELETE_NIO_SCHEMA):
|
||||
return
|
||||
|
||||
# get the instance
|
||||
qemu_instance = self.get_qemu_instance(request["id"])
|
||||
if not qemu_instance:
|
||||
return
|
||||
|
||||
port = request["port"]
|
||||
try:
|
||||
nio = qemu_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 QemuError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
self.send_response(True)
|
||||
|
||||
def _get_qemu_version(self, qemu_path):
|
||||
"""
|
||||
Gets the Qemu version.
|
||||
|
||||
:param qemu_path: path to Qemu
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
return ""
|
||||
try:
|
||||
output = subprocess.check_output([qemu_path, "--version"])
|
||||
match = re.search("QEMU emulator version ([0-9a-z\-\.]+)", output.decode("utf-8"))
|
||||
if match:
|
||||
version = match.group(1)
|
||||
return version
|
||||
else:
|
||||
raise QemuError("Could not determine the Qemu version for {}".format(qemu_path))
|
||||
except (OSError, subprocess.CalledProcessError) as e:
|
||||
raise QemuError("Error while looking for the Qemu version: {}".format(e))
|
||||
|
||||
@IModule.route("qemu.qemu_list")
|
||||
def qemu_list(self, request):
|
||||
"""
|
||||
Gets QEMU binaries list.
|
||||
|
||||
Response parameters:
|
||||
- Server address/host
|
||||
- List of Qemu binaries
|
||||
"""
|
||||
|
||||
qemus = []
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for Qemu binaries in the current working directory and $PATH
|
||||
if sys.platform.startswith("win"):
|
||||
# add specific Windows paths
|
||||
paths.append(os.path.join(os.getcwd(), "qemu"))
|
||||
if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
|
||||
paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
|
||||
if "PROGRAMFILES" in os.environ and os.path.exists(os.environ["PROGRAMFILES"]):
|
||||
paths.append(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
|
||||
for path in paths:
|
||||
try:
|
||||
for f in os.listdir(path):
|
||||
if f.startswith("qemu-system") and os.access(os.path.join(path, f), os.X_OK):
|
||||
qemu_path = os.path.join(path, f)
|
||||
version = self._get_qemu_version(qemu_path)
|
||||
qemus.append({"path": qemu_path, "version": version})
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
response = {"server": self._host,
|
||||
"qemus": qemus}
|
||||
self.send_response(response)
|
||||
|
||||
@IModule.route("qemu.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,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
|
@ -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):
|
||||
"""
|
||||
QEMU Ethernet adapter.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Adapter.__init__(self, interfaces=1)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "QEMU Ethernet adapter"
|
@ -0,0 +1,65 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Base interface for NIOs.
|
||||
"""
|
||||
|
||||
|
||||
class NIO(object):
|
||||
"""
|
||||
Network Input/Output.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self._capturing = False
|
||||
self._pcap_output_file = ""
|
||||
|
||||
def startPacketCapture(self, pcap_output_file):
|
||||
"""
|
||||
|
||||
:param pcap_output_file: PCAP destination file for the capture
|
||||
"""
|
||||
|
||||
self._capturing = True
|
||||
self._pcap_output_file = pcap_output_file
|
||||
|
||||
def stopPacketCapture(self):
|
||||
|
||||
self._capturing = False
|
||||
self._pcap_output_file = ""
|
||||
|
||||
@property
|
||||
def capturing(self):
|
||||
"""
|
||||
Returns either a capture is configured on this NIO.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._capturing
|
||||
|
||||
@property
|
||||
def pcap_output_file(self):
|
||||
"""
|
||||
Returns the path to the PCAP output file.
|
||||
|
||||
:returns: path to the PCAP output file
|
||||
"""
|
||||
|
||||
return self._pcap_output_file
|
@ -0,0 +1,75 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
|
||||
from .nio import NIO
|
||||
|
||||
|
||||
class NIO_UDP(NIO):
|
||||
"""
|
||||
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):
|
||||
|
||||
NIO.__init__(self)
|
||||
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"
|
@ -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 QEMU module.
|
||||
"""
|
||||
|
||||
|
||||
class QemuError(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
|
@ -0,0 +1,785 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
QEMU VM instance.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
import subprocess
|
||||
import shlex
|
||||
|
||||
from .qemu_error import QemuError
|
||||
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 QemuVM(object):
|
||||
"""
|
||||
QEMU VM implementation.
|
||||
|
||||
:param name: name of this QEMU VM
|
||||
:param qemu_path: path to the QEMU binary
|
||||
:param working_dir: path to a working directory
|
||||
:param host: host/address to bind for console and UDP connections
|
||||
:param qemu_id: QEMU 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,
|
||||
qemu_path,
|
||||
working_dir,
|
||||
host="127.0.0.1",
|
||||
qemu_id=None,
|
||||
console=None,
|
||||
console_start_port_range=5001,
|
||||
console_end_port_range=5500):
|
||||
|
||||
if not qemu_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 QemuError("Maximum number of QEMU VM instances reached")
|
||||
else:
|
||||
if qemu_id in self._instances:
|
||||
raise QemuError("QEMU identifier {} is already used by another QEMU VM instance".format(qemu_id))
|
||||
self._id = qemu_id
|
||||
self._instances.append(self._id)
|
||||
|
||||
self._name = name
|
||||
self._working_dir = None
|
||||
self._host = host
|
||||
self._command = []
|
||||
self._started = False
|
||||
self._process = None
|
||||
self._stdout_file = ""
|
||||
self._console_start_port_range = console_start_port_range
|
||||
self._console_end_port_range = console_end_port_range
|
||||
|
||||
# QEMU settings
|
||||
self._qemu_path = qemu_path
|
||||
self._hda_disk_image = ""
|
||||
self._hdb_disk_image = ""
|
||||
self._options = ""
|
||||
self._ram = 256
|
||||
self._console = console
|
||||
self._ethernet_adapters = []
|
||||
self._adapter_type = "e1000"
|
||||
self._initrd = ""
|
||||
self._kernel_image = ""
|
||||
self._kernel_command_line = ""
|
||||
|
||||
working_dir_path = os.path.join(working_dir, "qemu", "vm-{}".format(self._id))
|
||||
|
||||
if qemu_id and not os.path.isdir(working_dir_path):
|
||||
raise QemuError("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 QemuError(e)
|
||||
|
||||
if self._console in self._allocated_console_ports:
|
||||
raise QemuError("Console port {} is already used by another QEMU VM".format(console))
|
||||
self._allocated_console_ports.append(self._console)
|
||||
|
||||
self.adapters = 1 # creates 1 adapter by default
|
||||
log.info("QEMU 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 QEMU VM.
|
||||
|
||||
:returns: default values (dictionary)
|
||||
"""
|
||||
|
||||
qemu_defaults = {"name": self._name,
|
||||
"qemu_path": self._qemu_path,
|
||||
"ram": self._ram,
|
||||
"hda_disk_image": self._hda_disk_image,
|
||||
"hdb_disk_image": self._hdb_disk_image,
|
||||
"options": self._options,
|
||||
"adapters": self.adapters,
|
||||
"adapter_type": self._adapter_type,
|
||||
"console": self._console,
|
||||
"initrd": self._initrd,
|
||||
"kernel_image": self._kernel_image,
|
||||
"kernel_command_line": self._kernel_command_line}
|
||||
|
||||
return qemu_defaults
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""
|
||||
Returns the unique ID for this QEMU 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 QEMU VM.
|
||||
|
||||
:returns: name
|
||||
"""
|
||||
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, new_name):
|
||||
"""
|
||||
Sets the name of this QEMU VM.
|
||||
|
||||
:param new_name: name
|
||||
"""
|
||||
|
||||
log.info("QEMU 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 QEMU VM.
|
||||
|
||||
:param working_dir: path to the working directory
|
||||
"""
|
||||
|
||||
try:
|
||||
os.makedirs(working_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
raise QemuError("Could not create working directory {}: {}".format(working_dir, e))
|
||||
|
||||
self._working_dir = working_dir
|
||||
log.info("QEMU 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 QemuError("Console port {} is already used by another QEMU VM".format(console))
|
||||
|
||||
self._allocated_console_ports.remove(self._console)
|
||||
self._console = console
|
||||
self._allocated_console_ports.append(self._console)
|
||||
|
||||
log.info("QEMU VM {name} [id={id}]: console port set to {port}".format(name=self._name,
|
||||
id=self._id,
|
||||
port=console))
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Deletes this QEMU 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("QEMU VM {name} [id={id}] has been deleted".format(name=self._name,
|
||||
id=self._id))
|
||||
|
||||
def clean_delete(self):
|
||||
"""
|
||||
Deletes this QEMU 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 QEMU VM {name} [id={id}]: {error}".format(name=self._name,
|
||||
id=self._id,
|
||||
error=e))
|
||||
return
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has been deleted (including associated files)".format(name=self._name,
|
||||
id=self._id))
|
||||
|
||||
@property
|
||||
def qemu_path(self):
|
||||
"""
|
||||
Returns the QEMU binary path for this QEMU VM.
|
||||
|
||||
:returns: QEMU path
|
||||
"""
|
||||
|
||||
return self._qemu_path
|
||||
|
||||
@qemu_path.setter
|
||||
def qemu_path(self, qemu_path):
|
||||
"""
|
||||
Sets the QEMU binary path this QEMU VM.
|
||||
|
||||
:param qemu_path: QEMU path
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the QEMU path to {qemu_path}".format(name=self._name,
|
||||
id=self._id,
|
||||
qemu_path=qemu_path))
|
||||
self._qemu_path = qemu_path
|
||||
|
||||
@property
|
||||
def hda_disk_image(self):
|
||||
"""
|
||||
Returns the hda disk image path for this QEMU VM.
|
||||
|
||||
:returns: QEMU hda disk image path
|
||||
"""
|
||||
|
||||
return self._hda_disk_image
|
||||
|
||||
@hda_disk_image.setter
|
||||
def hda_disk_image(self, hda_disk_image):
|
||||
"""
|
||||
Sets the hda disk image for this QEMU VM.
|
||||
|
||||
:param hda_disk_image: QEMU hda disk image path
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the QEMU hda disk image path to {disk_image}".format(name=self._name,
|
||||
id=self._id,
|
||||
disk_image=hda_disk_image))
|
||||
self._hda_disk_image = hda_disk_image
|
||||
|
||||
@property
|
||||
def hdb_disk_image(self):
|
||||
"""
|
||||
Returns the hdb disk image path for this QEMU VM.
|
||||
|
||||
:returns: QEMU hdb disk image path
|
||||
"""
|
||||
|
||||
return self._hdb_disk_image
|
||||
|
||||
@hdb_disk_image.setter
|
||||
def hdb_disk_image(self, hdb_disk_image):
|
||||
"""
|
||||
Sets the hdb disk image for this QEMU VM.
|
||||
|
||||
:param hdb_disk_image: QEMU hdb disk image path
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the QEMU hdb disk image path to {disk_image}".format(name=self._name,
|
||||
id=self._id,
|
||||
disk_image=hdb_disk_image))
|
||||
self._hdb_disk_image = hdb_disk_image
|
||||
|
||||
|
||||
@property
|
||||
def adapters(self):
|
||||
"""
|
||||
Returns the number of Ethernet adapters for this QEMU VM instance.
|
||||
|
||||
:returns: number of adapters
|
||||
"""
|
||||
|
||||
return len(self._ethernet_adapters)
|
||||
|
||||
@adapters.setter
|
||||
def adapters(self, adapters):
|
||||
"""
|
||||
Sets the number of Ethernet adapters for this QEMU VM instance.
|
||||
|
||||
:param adapters: number of adapters
|
||||
"""
|
||||
|
||||
self._ethernet_adapters.clear()
|
||||
for adapter_id in range(0, adapters):
|
||||
self._ethernet_adapters.append(EthernetAdapter())
|
||||
|
||||
log.info("QEMU VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
|
||||
id=self._id,
|
||||
adapters=adapters))
|
||||
|
||||
@property
|
||||
def adapter_type(self):
|
||||
"""
|
||||
Returns the adapter type for this QEMU VM instance.
|
||||
|
||||
:returns: adapter type (string)
|
||||
"""
|
||||
|
||||
return self._adapter_type
|
||||
|
||||
@adapter_type.setter
|
||||
def adapter_type(self, adapter_type):
|
||||
"""
|
||||
Sets the adapter type for this QEMU VM instance.
|
||||
|
||||
:param adapter_type: adapter type (string)
|
||||
"""
|
||||
|
||||
self._adapter_type = adapter_type
|
||||
|
||||
log.info("QEMU VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name,
|
||||
id=self._id,
|
||||
adapter_type=adapter_type))
|
||||
|
||||
@property
|
||||
def ram(self):
|
||||
"""
|
||||
Returns the RAM amount for this QEMU VM.
|
||||
|
||||
:returns: RAM amount in MB
|
||||
"""
|
||||
|
||||
return self._ram
|
||||
|
||||
@ram.setter
|
||||
def ram(self, ram):
|
||||
"""
|
||||
Sets the amount of RAM for this QEMU VM.
|
||||
|
||||
:param ram: RAM amount in MB
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the RAM to {ram}".format(name=self._name,
|
||||
id=self._id,
|
||||
ram=ram))
|
||||
self._ram = ram
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
"""
|
||||
Returns the options for this QEMU VM.
|
||||
|
||||
:returns: QEMU options
|
||||
"""
|
||||
|
||||
return self._options
|
||||
|
||||
@options.setter
|
||||
def options(self, options):
|
||||
"""
|
||||
Sets the options for this QEMU VM.
|
||||
|
||||
:param options: QEMU options
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the QEMU options to {options}".format(name=self._name,
|
||||
id=self._id,
|
||||
options=options))
|
||||
self._options = options
|
||||
|
||||
@property
|
||||
def initrd(self):
|
||||
"""
|
||||
Returns the initrd path for this QEMU VM.
|
||||
|
||||
:returns: QEMU initrd path
|
||||
"""
|
||||
|
||||
return self._initrd
|
||||
|
||||
@initrd.setter
|
||||
def initrd(self, initrd):
|
||||
"""
|
||||
Sets the initrd path for this QEMU VM.
|
||||
|
||||
:param initrd: QEMU initrd path
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the QEMU initrd path to {initrd}".format(name=self._name,
|
||||
id=self._id,
|
||||
initrd=initrd))
|
||||
self._initrd = initrd
|
||||
|
||||
@property
|
||||
def kernel_image(self):
|
||||
"""
|
||||
Returns the kernel image path for this QEMU VM.
|
||||
|
||||
:returns: QEMU kernel image path
|
||||
"""
|
||||
|
||||
return self._kernel_image
|
||||
|
||||
@kernel_image.setter
|
||||
def kernel_image(self, kernel_image):
|
||||
"""
|
||||
Sets the kernel image path for this QEMU VM.
|
||||
|
||||
:param kernel_image: QEMU kernel image path
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the QEMU kernel image path to {kernel_image}".format(name=self._name,
|
||||
id=self._id,
|
||||
kernel_image=kernel_image))
|
||||
self._kernel_image = kernel_image
|
||||
|
||||
@property
|
||||
def kernel_command_line(self):
|
||||
"""
|
||||
Returns the kernel command line for this QEMU VM.
|
||||
|
||||
:returns: QEMU kernel command line
|
||||
"""
|
||||
|
||||
return self._kernel_command_line
|
||||
|
||||
@kernel_command_line.setter
|
||||
def kernel_command_line(self, kernel_command_line):
|
||||
"""
|
||||
Sets the kernel command line for this QEMU VM.
|
||||
|
||||
:param kernel_command_line: QEMU kernel command line
|
||||
"""
|
||||
|
||||
log.info("QEMU VM {name} [id={id}] has set the QEMU kernel command line to {kernel_command_line}".format(name=self._name,
|
||||
id=self._id,
|
||||
kernel_command_line=kernel_command_line))
|
||||
self._kernel_command_line = kernel_command_line
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Starts this QEMU VM.
|
||||
"""
|
||||
|
||||
if not self.is_running():
|
||||
|
||||
if not os.path.isfile(self._qemu_path) or not os.path.exists(self._qemu_path):
|
||||
raise QemuError("QEMU binary '{}' is not accessible".format(self._qemu_path))
|
||||
|
||||
self._command = self._build_command()
|
||||
try:
|
||||
log.info("starting QEMU: {}".format(self._command))
|
||||
self._stdout_file = os.path.join(self._working_dir, "qemu.log")
|
||||
log.info("logging to {}".format(self._stdout_file))
|
||||
with open(self._stdout_file, "w") as fd:
|
||||
self._process = subprocess.Popen(self._command,
|
||||
stdout=fd,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=self._working_dir)
|
||||
log.info("QEMU VM instance {} started PID={}".format(self._id, self._process.pid))
|
||||
self._started = True
|
||||
except OSError as e:
|
||||
stdout = self.read_stdout()
|
||||
log.error("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
|
||||
raise QemuError("could not start QEMU {}: {}\n{}".format(self._qemu_path, e, stdout))
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops this QEMU VM.
|
||||
"""
|
||||
|
||||
# stop the QEMU process
|
||||
if self.is_running():
|
||||
log.info("stopping QEMU VM instance {} PID={}".format(self._id, self._process.pid))
|
||||
try:
|
||||
self._process.terminate()
|
||||
self._process.wait(1)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._process.kill()
|
||||
if self._process.poll() is None:
|
||||
log.warn("QEMU VM instance {} PID={} is still running".format(self._id,
|
||||
self._process.pid))
|
||||
self._process = None
|
||||
self._started = False
|
||||
|
||||
def suspend(self):
|
||||
"""
|
||||
Suspends this QEMU VM.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
Reloads this QEMU VM.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resumes this QEMU VM.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def port_add_nio_binding(self, adapter_id, nio):
|
||||
"""
|
||||
Adds a port NIO binding.
|
||||
|
||||
:param adapter_id: adapter ID
|
||||
:param nio: NIO instance to add to the slot/port
|
||||
"""
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_id]
|
||||
except IndexError:
|
||||
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
|
||||
adapter_id=adapter_id))
|
||||
|
||||
adapter.add_nio(0, nio)
|
||||
log.info("QEMU VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name,
|
||||
id=self._id,
|
||||
nio=nio,
|
||||
adapter_id=adapter_id))
|
||||
|
||||
def port_remove_nio_binding(self, adapter_id):
|
||||
"""
|
||||
Removes a port NIO binding.
|
||||
|
||||
:param adapter_id: adapter ID
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_id]
|
||||
except IndexError:
|
||||
raise QemuError("Adapter {adapter_id} doesn't exist on QEMU VM {name}".format(name=self._name,
|
||||
adapter_id=adapter_id))
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
adapter.remove_nio(0)
|
||||
log.info("QEMU VM {name} [id={id}]: {nio} removed from adapter {adapter_id}".format(name=self._name,
|
||||
id=self._id,
|
||||
nio=nio,
|
||||
adapter_id=adapter_id))
|
||||
return nio
|
||||
|
||||
@property
|
||||
def started(self):
|
||||
"""
|
||||
Returns either this QEMU VM has been started or not.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._started
|
||||
|
||||
def read_stdout(self):
|
||||
"""
|
||||
Reads the standard output of the QEMU process.
|
||||
Only use when the process has been stopped or has crashed.
|
||||
"""
|
||||
|
||||
output = ""
|
||||
if self._stdout_file:
|
||||
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 QEMU process is running
|
||||
|
||||
:returns: True or False
|
||||
"""
|
||||
|
||||
if self._process and self._process.poll() is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
def command(self):
|
||||
"""
|
||||
Returns the QEMU command line.
|
||||
|
||||
:returns: QEMU command line (string)
|
||||
"""
|
||||
|
||||
return " ".join(self._build_command())
|
||||
|
||||
def _serial_options(self):
|
||||
|
||||
if self._console:
|
||||
return ["-serial", "telnet:{}:{},server,nowait".format(self._host, self._console)]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _disk_options(self):
|
||||
|
||||
options = []
|
||||
qemu_img_path = ""
|
||||
qemu_path_dir = os.path.dirname(self._qemu_path)
|
||||
try:
|
||||
for f in os.listdir(qemu_path_dir):
|
||||
if f.startswith("qemu-img"):
|
||||
qemu_img_path = os.path.join(qemu_path_dir, f)
|
||||
except OSError as e:
|
||||
raise QemuError("Error while looking for qemu-img in {}: {}".format(qemu_path_dir, e))
|
||||
|
||||
if not qemu_img_path:
|
||||
raise QemuError("Could not find qemu-img in {}".format(qemu_path_dir))
|
||||
|
||||
try:
|
||||
if self._hda_disk_image:
|
||||
hda_disk = os.path.join(self._working_dir, "hda_disk.qcow2")
|
||||
if not os.path.exists(hda_disk):
|
||||
retcode = subprocess.call([qemu_img_path, "create", "-o",
|
||||
"backing_file={}".format(self._hda_disk_image),
|
||||
"-f", "qcow2", hda_disk])
|
||||
log.info("{} returned with {}".format(qemu_img_path, retcode))
|
||||
else:
|
||||
# create a "FLASH" with 256MB if no disk image has been specified
|
||||
hda_disk = os.path.join(self._working_dir, "flash.qcow2")
|
||||
if not os.path.exists(hda_disk):
|
||||
retcode = subprocess.call([qemu_img_path, "create", "-f", "qcow2", hda_disk, "256M"])
|
||||
log.info("{} returned with {}".format(qemu_img_path, retcode))
|
||||
|
||||
except OSError as e:
|
||||
raise QemuError("Could not create disk image {}".format(e))
|
||||
|
||||
options.extend(["-hda", hda_disk])
|
||||
if self._hdb_disk_image:
|
||||
hdb_disk = os.path.join(self._working_dir, "hdb_disk.qcow2")
|
||||
if not os.path.exists(hdb_disk):
|
||||
try:
|
||||
retcode = subprocess.call([qemu_img_path, "create", "-o",
|
||||
"backing_file={}".format(self._hdb_disk_image),
|
||||
"-f", "qcow2", hdb_disk])
|
||||
log.info("{} returned with {}".format(qemu_img_path, retcode))
|
||||
except OSError as e:
|
||||
raise QemuError("Could not create disk image {}".format(e))
|
||||
options.extend(["-hdb", hdb_disk])
|
||||
|
||||
return options
|
||||
|
||||
def _linux_boot_options(self):
|
||||
|
||||
options = []
|
||||
if self._initrd:
|
||||
options.extend(["-initrd", self._initrd])
|
||||
if self._kernel_image:
|
||||
options.extend(["-kernel", self._kernel_image])
|
||||
if self._kernel_command_line:
|
||||
options.extend(["-append", self._kernel_command_line])
|
||||
|
||||
return options
|
||||
|
||||
def _network_options(self):
|
||||
|
||||
network_options = []
|
||||
adapter_id = 0
|
||||
for adapter in self._ethernet_adapters:
|
||||
#TODO: let users specify a base mac address
|
||||
mac = "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_id)
|
||||
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_id)])
|
||||
nio = adapter.get_nio(0)
|
||||
if nio and isinstance(nio, NIO_UDP):
|
||||
network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_id,
|
||||
nio.rhost,
|
||||
nio.rport,
|
||||
self._host,
|
||||
nio.lport)])
|
||||
else:
|
||||
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_id)])
|
||||
adapter_id += 1
|
||||
|
||||
return network_options
|
||||
|
||||
def _build_command(self):
|
||||
"""
|
||||
Command to start the QEMU process.
|
||||
(to be passed to subprocess.Popen())
|
||||
"""
|
||||
|
||||
command = [self._qemu_path]
|
||||
command.extend(["-name", self._name])
|
||||
command.extend(["-m", str(self._ram)])
|
||||
command.extend(self._disk_options())
|
||||
command.extend(self._linux_boot_options())
|
||||
command.extend(self._serial_options())
|
||||
additional_options = self._options.strip()
|
||||
if additional_options:
|
||||
command.extend(shlex.split(additional_options))
|
||||
command.extend(self._network_options())
|
||||
return command
|
@ -0,0 +1,393 @@
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
QEMU_CREATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to create a new QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "QEMU VM instance name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"qemu_path": {
|
||||
"description": "Path to QEMU",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"qemu_id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["name", "qemu_path"],
|
||||
}
|
||||
|
||||
QEMU_DELETE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
QEMU_UPDATE_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to update a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "QEMU VM instance name",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"qemu_path": {
|
||||
"description": "path to QEMU",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"hda_disk_image": {
|
||||
"description": "QEMU hda disk image path",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"hdb_disk_image": {
|
||||
"description": "QEMU hdb disk image path",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"ram": {
|
||||
"description": "amount of RAM in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"adapters": {
|
||||
"description": "number of adapters",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 8,
|
||||
},
|
||||
"adapter_type": {
|
||||
"description": "QEMU adapter type",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"minimum": 1,
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
"initrd": {
|
||||
"description": "QEMU initrd path",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"kernel_image": {
|
||||
"description": "QEMU kernel image path",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"kernel_command_line": {
|
||||
"description": "QEMU kernel command line",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
},
|
||||
"options": {
|
||||
"description": "additional QEMU options",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
QEMU_START_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to start a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
QEMU_STOP_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to stop a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
QEMU_SUSPEND_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to suspend a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
QEMU_RELOAD_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to reload a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id"]
|
||||
}
|
||||
|
||||
QEMU_ALLOCATE_UDP_PORT_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to allocate an UDP port for a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the QEMU VM instance",
|
||||
"type": "integer"
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id"]
|
||||
}
|
||||
|
||||
QEMU_ADD_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to add a NIO for a QEMU VM instance",
|
||||
"type": "object",
|
||||
|
||||
"definitions": {
|
||||
"UDP": {
|
||||
"description": "UDP Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_udp"]
|
||||
},
|
||||
"lport": {
|
||||
"description": "Local port",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"rhost": {
|
||||
"description": "Remote host",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"rport": {
|
||||
"description": "Remote port",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
}
|
||||
},
|
||||
"required": ["type", "lport", "rhost", "rport"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"Ethernet": {
|
||||
"description": "Generic Ethernet Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_generic_ethernet"]
|
||||
},
|
||||
"ethernet_device": {
|
||||
"description": "Ethernet device name e.g. eth0",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
},
|
||||
"required": ["type", "ethernet_device"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"LinuxEthernet": {
|
||||
"description": "Linux Ethernet Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_linux_ethernet"]
|
||||
},
|
||||
"ethernet_device": {
|
||||
"description": "Ethernet device name e.g. eth0",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
},
|
||||
"required": ["type", "ethernet_device"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"TAP": {
|
||||
"description": "TAP Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_tap"]
|
||||
},
|
||||
"tap_device": {
|
||||
"description": "TAP device name e.g. tap0",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
},
|
||||
"required": ["type", "tap_device"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"UNIX": {
|
||||
"description": "UNIX Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_unix"]
|
||||
},
|
||||
"local_file": {
|
||||
"description": "path to the UNIX socket file (local)",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"remote_file": {
|
||||
"description": "path to the UNIX socket file (remote)",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
},
|
||||
"required": ["type", "local_file", "remote_file"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"VDE": {
|
||||
"description": "VDE Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_vde"]
|
||||
},
|
||||
"control_file": {
|
||||
"description": "path to the VDE control file",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"local_file": {
|
||||
"description": "path to the VDE control file",
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
},
|
||||
"required": ["type", "control_file", "local_file"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"NULL": {
|
||||
"description": "NULL Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_null"]
|
||||
},
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
},
|
||||
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port_id": {
|
||||
"description": "Unique port identifier for the QEMU VM instance",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8
|
||||
},
|
||||
"nio": {
|
||||
"type": "object",
|
||||
"description": "Network Input/Output",
|
||||
"oneOf": [
|
||||
{"$ref": "#/definitions/UDP"},
|
||||
{"$ref": "#/definitions/Ethernet"},
|
||||
{"$ref": "#/definitions/LinuxEthernet"},
|
||||
{"$ref": "#/definitions/TAP"},
|
||||
{"$ref": "#/definitions/UNIX"},
|
||||
{"$ref": "#/definitions/VDE"},
|
||||
{"$ref": "#/definitions/NULL"},
|
||||
]
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port_id", "port", "nio"]
|
||||
}
|
||||
|
||||
|
||||
QEMU_DELETE_NIO_SCHEMA = {
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Request validation to delete a NIO for a QEMU VM instance",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "QEMU VM instance ID",
|
||||
"type": "integer"
|
||||
},
|
||||
"port": {
|
||||
"description": "Port number",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 8
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["id", "port"]
|
||||
}
|
@ -0,0 +1,557 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Controls VirtualBox using the VBox API.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import re
|
||||
import time
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
import msvcrt
|
||||
import win32file
|
||||
|
||||
from .virtualbox_error import VirtualBoxError
|
||||
from .pipe_proxy import PipeProxy
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VirtualBoxController(object):
|
||||
|
||||
def __init__(self, vmname, vboxmanager, host):
|
||||
|
||||
self._host = host
|
||||
self._machine = None
|
||||
self._session = None
|
||||
self._vboxmanager = vboxmanager
|
||||
self._maximum_adapters = 0
|
||||
self._serial_pipe_thread = None
|
||||
self._serial_pipe = None
|
||||
|
||||
self._vmname = vmname
|
||||
self._console = 0
|
||||
self._adapters = []
|
||||
self._headless = False
|
||||
self._enable_console = True
|
||||
self._adapter_type = "Automatic"
|
||||
|
||||
try:
|
||||
self._machine = self._vboxmanager.vbox.findMachine(self._vmname)
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
# The maximum support network cards depends on the Chipset (PIIX3 or ICH9)
|
||||
self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType)
|
||||
|
||||
@property
|
||||
def vmname(self):
|
||||
|
||||
return self._vmname
|
||||
|
||||
@vmname.setter
|
||||
def vmname(self, new_vmname):
|
||||
|
||||
self._vmname = new_vmname
|
||||
try:
|
||||
self._machine = self._vboxmanager.vbox.findMachine(new_vmname)
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
# The maximum support network cards depends on the Chipset (PIIX3 or ICH9)
|
||||
self._maximum_adapters = self._vboxmanager.vbox.systemProperties.getMaxNetworkAdapters(self._machine.chipsetType)
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
|
||||
return self._console
|
||||
|
||||
@console.setter
|
||||
def console(self, console):
|
||||
|
||||
self._console = console
|
||||
|
||||
@property
|
||||
def headless(self):
|
||||
|
||||
return self._headless
|
||||
|
||||
@headless.setter
|
||||
def headless(self, headless):
|
||||
|
||||
self._headless = headless
|
||||
|
||||
@property
|
||||
def enable_console(self):
|
||||
|
||||
return self._enable_console
|
||||
|
||||
@enable_console.setter
|
||||
def enable_console(self, enable_console):
|
||||
|
||||
self._enable_console = enable_console
|
||||
|
||||
@property
|
||||
def adapters(self):
|
||||
|
||||
return self._adapters
|
||||
|
||||
@adapters.setter
|
||||
def adapters(self, adapters):
|
||||
|
||||
self._adapters = adapters
|
||||
|
||||
@property
|
||||
def adapter_type(self):
|
||||
|
||||
return self._adapter_type
|
||||
|
||||
@adapter_type.setter
|
||||
def adapter_type(self, adapter_type):
|
||||
|
||||
self._adapter_type = adapter_type
|
||||
|
||||
def start(self):
|
||||
|
||||
if len(self._adapters) > self._maximum_adapters:
|
||||
raise VirtualBoxError("Number of adapters above the maximum supported of {}".format(self._maximum_adapters))
|
||||
|
||||
if self._machine.state == self._vboxmanager.constants.MachineState_Paused:
|
||||
self.resume()
|
||||
return
|
||||
|
||||
self._get_session()
|
||||
self._set_network_options()
|
||||
if self._enable_console:
|
||||
self._set_console_options()
|
||||
|
||||
progress = self._launch_vm_process()
|
||||
log.info("VM is starting with {}% completed".format(progress.percent))
|
||||
if progress.percent != 100:
|
||||
# This will happen if you attempt to start VirtualBox with unloaded "vboxdrv" module.
|
||||
# or have too little RAM or damaged vHDD, or connected to non-existent network.
|
||||
# We must unlock machine, otherwise it locks the VirtualBox Manager GUI. (on Linux hosts)
|
||||
self._unlock_machine()
|
||||
raise VirtualBoxError("Unable to start the VM (failed at {}%)".format(progress.percent))
|
||||
|
||||
try:
|
||||
self._machine.setGuestPropertyValue("NameInGNS3", self._name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self._enable_console:
|
||||
# starts the Telnet to pipe thread
|
||||
pipe_name = self._get_pipe_name()
|
||||
if sys.platform.startswith('win'):
|
||||
try:
|
||||
self._serial_pipe = open(pipe_name, "a+b")
|
||||
except OSError as e:
|
||||
raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e))
|
||||
self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console)
|
||||
#self._serial_pipe_thread.setDaemon(True)
|
||||
self._serial_pipe_thread.start()
|
||||
else:
|
||||
try:
|
||||
self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self._serial_pipe.connect(pipe_name)
|
||||
except OSError as e:
|
||||
raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e))
|
||||
self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console)
|
||||
#self._serial_pipe_thread.setDaemon(True)
|
||||
self._serial_pipe_thread.start()
|
||||
|
||||
def stop(self):
|
||||
|
||||
if self._serial_pipe_thread:
|
||||
self._serial_pipe_thread.stop()
|
||||
self._serial_pipe_thread.join(1)
|
||||
if self._serial_pipe_thread.isAlive():
|
||||
log.warn("Serial pire thread is still alive!")
|
||||
self._serial_pipe_thread = None
|
||||
|
||||
if self._serial_pipe:
|
||||
if sys.platform.startswith('win'):
|
||||
win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno()))
|
||||
else:
|
||||
self._serial_pipe.close()
|
||||
self._serial_pipe = None
|
||||
|
||||
if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \
|
||||
self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline:
|
||||
try:
|
||||
if sys.platform.startswith('win') and "VBOX_INSTALL_PATH" in os.environ:
|
||||
# work around VirtualBox bug #9239
|
||||
vboxmanage_path = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
|
||||
command = '"{}" controlvm "{}" poweroff'.format(vboxmanage_path, self._vmname)
|
||||
subprocess.call(command, timeout=3)
|
||||
else:
|
||||
progress = self._session.console.powerDown()
|
||||
# wait for VM to actually go down
|
||||
progress.waitForCompletion(3000)
|
||||
log.info("VM is stopping with {}% completed".format(self.vmname, progress.percent))
|
||||
|
||||
self._lock_machine()
|
||||
|
||||
for adapter_id in range(0, len(self._adapters)):
|
||||
if self._adapters[adapter_id] is None:
|
||||
continue
|
||||
self._disable_adapter(adapter_id, disable=True)
|
||||
if self._enable_console:
|
||||
serial_port = self._session.machine.getSerialPort(0)
|
||||
serial_port.enabled = False
|
||||
self._session.machine.saveSettings()
|
||||
self._unlock_machine()
|
||||
except Exception as e:
|
||||
# Do not crash "vboxwrapper", if stopping VM fails.
|
||||
# But return True anyway, so VM state in GNS3 can become "stopped"
|
||||
# This can happen, if user manually kills VBox VM.
|
||||
log.warn("could not stop VM for {}: {}".format(self._vmname, e))
|
||||
return
|
||||
|
||||
def suspend(self):
|
||||
|
||||
try:
|
||||
self._session.console.pause()
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
def reload(self):
|
||||
|
||||
try:
|
||||
self._session.console.reset()
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
def resume(self):
|
||||
|
||||
try:
|
||||
self._session.console.resume()
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
def _get_session(self):
|
||||
|
||||
log.debug("getting session for {}".format(self._vmname))
|
||||
try:
|
||||
self._session = self._vboxmanager.mgr.getSessionObject(self._vboxmanager.vbox)
|
||||
except Exception as e:
|
||||
# fails on heavily loaded hosts...
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
def _set_network_options(self):
|
||||
|
||||
log.debug("setting network options for {}".format(self._vmname))
|
||||
|
||||
self._lock_machine()
|
||||
|
||||
first_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM
|
||||
try:
|
||||
first_adapter = self._session.machine.getNetworkAdapter(0)
|
||||
first_adapter_type = first_adapter.adapterType
|
||||
except Exception as e:
|
||||
pass
|
||||
#raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
for adapter_id in range(0, len(self._adapters)):
|
||||
|
||||
try:
|
||||
# VirtualBox starts counting from 0
|
||||
adapter = self._session.machine.getNetworkAdapter(adapter_id)
|
||||
if self._adapters[adapter_id] is None:
|
||||
# force enable to avoid any discrepancy in the interface numbering inside the VM
|
||||
# e.g. Ethernet2 in GNS3 becoming eth0 inside the VM when using a start index of 2.
|
||||
adapter.enabled = True
|
||||
continue
|
||||
|
||||
vbox_adapter_type = adapter.adapterType
|
||||
if self._adapter_type == "PCnet-PCI II (Am79C970A)":
|
||||
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A
|
||||
if self._adapter_type == "PCNet-FAST III (Am79C973)":
|
||||
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C973
|
||||
if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)":
|
||||
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM
|
||||
if self._adapter_type == "Intel PRO/1000 T Server (82543GC)":
|
||||
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82543GC
|
||||
if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)":
|
||||
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82545EM
|
||||
if self._adapter_type == "Paravirtualized Network (virtio-net)":
|
||||
vbox_adapter_type = self._vboxmanager.constants.NetworkAdapterType_Virtio
|
||||
if self._adapter_type == "Automatic": # "Auto-guess, based on first NIC"
|
||||
vbox_adapter_type = first_adapter_type
|
||||
|
||||
adapter.adapterType = vbox_adapter_type
|
||||
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
nio = self._adapters[adapter_id].get_nio(0)
|
||||
if nio:
|
||||
log.debug("setting UDP params on adapter {}".format(adapter_id))
|
||||
try:
|
||||
adapter.enabled = True
|
||||
adapter.cableConnected = True
|
||||
adapter.traceEnabled = False
|
||||
# Temporary hack around VBox-UDP patch limitation: inability to use DNS
|
||||
if nio.rhost == 'localhost':
|
||||
rhost = '127.0.0.1'
|
||||
else:
|
||||
rhost = nio.rhost
|
||||
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic
|
||||
adapter.genericDriver = "UDPTunnel"
|
||||
adapter.setProperty("sport", str(nio.lport))
|
||||
adapter.setProperty("dest", rhost)
|
||||
adapter.setProperty("dport", str(nio.rport))
|
||||
except Exception as e:
|
||||
# usually due to COM Error: "The object is not ready"
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
if nio.capturing:
|
||||
self._enable_capture(adapter, nio.pcap_output_file)
|
||||
|
||||
else:
|
||||
# shutting down unused adapters...
|
||||
try:
|
||||
adapter.enabled = True
|
||||
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
|
||||
adapter.cableConnected = False
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
for adapter_id in range(len(self._adapters), self._maximum_adapters):
|
||||
log.debug("disabling remaining adapter {}".format(adapter_id))
|
||||
self._disable_adapter(adapter_id)
|
||||
|
||||
try:
|
||||
self._session.machine.saveSettings()
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
self._unlock_machine()
|
||||
|
||||
def _disable_adapter(self, adapter_id, disable=True):
|
||||
|
||||
log.debug("disabling network adapter for {}".format(self._vmname))
|
||||
# this command is retried several times, because it fails more often...
|
||||
retries = 6
|
||||
last_exception = None
|
||||
for retry in range(retries):
|
||||
if retry == (retries - 1):
|
||||
raise VirtualBoxError("Could not disable network adapter after 4 retries: {}".format(last_exception))
|
||||
try:
|
||||
adapter = self._session.machine.getNetworkAdapter(adapter_id)
|
||||
adapter.traceEnabled = False
|
||||
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
|
||||
if disable:
|
||||
adapter.enabled = False
|
||||
break
|
||||
except Exception as e:
|
||||
# usually due to COM Error: "The object is not ready"
|
||||
log.warn("cannot disable network adapter for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
|
||||
last_exception = e
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
def _enable_capture(self, adapter, output_file):
|
||||
|
||||
log.debug("enabling capture for {}".format(self._vmname))
|
||||
# this command is retried several times, because it fails more often...
|
||||
retries = 4
|
||||
last_exception = None
|
||||
for retry in range(retries):
|
||||
if retry == (retries - 1):
|
||||
raise VirtualBoxError("Could not enable packet capture after 4 retries: {}".format(last_exception))
|
||||
try:
|
||||
adapter.traceEnabled = True
|
||||
adapter.traceFile = output_file
|
||||
break
|
||||
except Exception as e:
|
||||
log.warn("cannot enable packet capture for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
|
||||
last_exception = e
|
||||
time.sleep(0.75)
|
||||
continue
|
||||
|
||||
def create_udp(self, adapter_id, sport, daddr, dport):
|
||||
|
||||
if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \
|
||||
self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline:
|
||||
# the machine is being executed
|
||||
retries = 4
|
||||
last_exception = None
|
||||
for retry in range(retries):
|
||||
if retry == (retries - 1):
|
||||
raise VirtualBoxError("Could not create an UDP tunnel after 4 retries :{}".format(last_exception))
|
||||
try:
|
||||
adapter = self._session.machine.getNetworkAdapter(adapter_id)
|
||||
adapter.cableConnected = True
|
||||
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
|
||||
self._session.machine.saveSettings()
|
||||
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Generic
|
||||
adapter.genericDriver = "UDPTunnel"
|
||||
adapter.setProperty("sport", str(sport))
|
||||
adapter.setProperty("dest", daddr)
|
||||
adapter.setProperty("dport", str(dport))
|
||||
self._session.machine.saveSettings()
|
||||
break
|
||||
except Exception as e:
|
||||
# usually due to COM Error: "The object is not ready"
|
||||
log.warn("cannot create UDP tunnel for {}: {}".format(self._vmname, e))
|
||||
last_exception = e
|
||||
time.sleep(0.75)
|
||||
continue
|
||||
|
||||
def delete_udp(self, adapter_id):
|
||||
|
||||
if self._machine.state >= self._vboxmanager.constants.MachineState_FirstOnline and \
|
||||
self._machine.state <= self._vboxmanager.constants.MachineState_LastOnline:
|
||||
# the machine is being executed
|
||||
retries = 4
|
||||
last_exception = None
|
||||
for retry in range(retries):
|
||||
if retry == (retries - 1):
|
||||
raise VirtualBoxError("Could not delete an UDP tunnel after 4 retries :{}".format(last_exception))
|
||||
try:
|
||||
adapter = self._session.machine.getNetworkAdapter(adapter_id)
|
||||
adapter.attachmentType = self._vboxmanager.constants.NetworkAttachmentType_Null
|
||||
adapter.cableConnected = False
|
||||
self._session.machine.saveSettings()
|
||||
break
|
||||
except Exception as e:
|
||||
# usually due to COM Error: "The object is not ready"
|
||||
log.debug("cannot delete UDP tunnel for {}: {}".format(self._vmname, e))
|
||||
last_exception = e
|
||||
time.sleep(0.75)
|
||||
continue
|
||||
|
||||
def _get_pipe_name(self):
|
||||
|
||||
p = re.compile('\s+', re.UNICODE)
|
||||
pipe_name = p.sub("_", self._vmname)
|
||||
if sys.platform.startswith('win'):
|
||||
pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name)
|
||||
else:
|
||||
pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name))
|
||||
return pipe_name
|
||||
|
||||
def _set_console_options(self):
|
||||
"""
|
||||
# Example to manually set serial parameters using Python
|
||||
|
||||
from vboxapi import VirtualBoxManager
|
||||
mgr = VirtualBoxManager(None, None)
|
||||
mach = mgr.vbox.findMachine("My VM")
|
||||
session = mgr.mgr.getSessionObject(mgr.vbox)
|
||||
mach.lockMachine(session, 1)
|
||||
mach2=session.machine
|
||||
serial_port = mach2.getSerialPort(0)
|
||||
serial_port.enabled = True
|
||||
serial_port.path = "/tmp/test_pipe"
|
||||
serial_port.hostMode = 1
|
||||
serial_port.server = True
|
||||
session.unlockMachine()
|
||||
"""
|
||||
|
||||
log.info("setting console options for {}".format(self._vmname))
|
||||
|
||||
self._lock_machine()
|
||||
pipe_name = self._get_pipe_name()
|
||||
|
||||
try:
|
||||
serial_port = self._session.machine.getSerialPort(0)
|
||||
serial_port.enabled = True
|
||||
serial_port.path = pipe_name
|
||||
serial_port.hostMode = 1
|
||||
serial_port.server = True
|
||||
self._session.machine.saveSettings()
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
self._unlock_machine()
|
||||
|
||||
def _launch_vm_process(self):
|
||||
|
||||
log.debug("launching VM {}".format(self._vmname))
|
||||
# this command is retried several times, because it fails more often...
|
||||
retries = 4
|
||||
last_exception = None
|
||||
for retry in range(retries):
|
||||
if retry == (retries - 1):
|
||||
raise VirtualBoxError("Could not launch the VM after 4 retries: {}".format(last_exception))
|
||||
try:
|
||||
if self._headless:
|
||||
mode = "headless"
|
||||
else:
|
||||
mode = "gui"
|
||||
log.info("starting {} in {} mode".format(self._vmname, mode))
|
||||
progress = self._machine.launchVMProcess(self._session, mode, "")
|
||||
break
|
||||
except Exception as e:
|
||||
# This will usually happen if you try to start the same VM twice,
|
||||
# but may happen on loaded hosts too...
|
||||
log.warn("cannot launch VM {}, retrying {}: {}".format(self._vmname, retry + 1, e))
|
||||
last_exception = e
|
||||
time.sleep(0.6)
|
||||
continue
|
||||
|
||||
try:
|
||||
progress.waitForCompletion(-1)
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
return progress
|
||||
|
||||
def _lock_machine(self):
|
||||
|
||||
log.debug("locking machine for {}".format(self._vmname))
|
||||
# this command is retried several times, because it fails more often...
|
||||
retries = 4
|
||||
last_exception = None
|
||||
for retry in range(retries):
|
||||
if retry == (retries - 1):
|
||||
raise VirtualBoxError("Could not lock the machine after 4 retries: {}".format(last_exception))
|
||||
try:
|
||||
self._machine.lockMachine(self._session, 1)
|
||||
break
|
||||
except Exception as e:
|
||||
log.warn("cannot lock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
|
||||
last_exception = e
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
def _unlock_machine(self):
|
||||
|
||||
log.debug("unlocking machine for {}".format(self._vmname))
|
||||
# this command is retried several times, because it fails more often...
|
||||
retries = 4
|
||||
last_exception = None
|
||||
for retry in range(retries):
|
||||
if retry == (retries - 1):
|
||||
raise VirtualBoxError("Could not unlock the machine after 4 retries: {}".format(last_exception))
|
||||
try:
|
||||
self._session.unlockMachine()
|
||||
break
|
||||
except Exception as e:
|
||||
log.warn("cannot unlock the machine for {}, retrying {}: {}".format(self._vmname, retry + 1, e))
|
||||
time.sleep(1)
|
||||
last_exception = e
|
||||
continue
|
Loading…
Reference in new issue