mirror of
https://github.com/GNS3/gns3-server.git
synced 2025-01-18 07:23:47 +02:00
Merge branch 'master' into dev
This commit is contained in:
commit
ee88d6f808
17
.travis.yml
17
.travis.yml
@ -1,14 +1,19 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
env:
|
||||
- TOX_ENV=py33
|
||||
- TOX_ENV=py34
|
||||
|
||||
before_install:
|
||||
- sudo add-apt-repository ppa:gns3/ppa -y
|
||||
- sudo apt-get update -q
|
||||
|
||||
install:
|
||||
- "pip install -r requirements.txt --use-mirrors"
|
||||
- "pip install tox"
|
||||
- pip install tox
|
||||
- sudo apt-get install vpcs dynamips
|
||||
|
||||
script: "python setup.py test"
|
||||
script:
|
||||
- tox -e $TOX_ENV
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
19
README.rst
19
README.rst
@ -1,7 +1,7 @@
|
||||
GNS3-server
|
||||
===========
|
||||
|
||||
New GNS3 server repository (alpha stage).
|
||||
New GNS3 server repository (beta stage).
|
||||
|
||||
The GNS3 server manages emulators such as Dynamips, VirtualBox or Qemu/KVM.
|
||||
Clients like the GNS3 GUI controls the server using a JSON-RPC API over Websockets.
|
||||
@ -34,4 +34,19 @@ Please use our all-in-one installer.
|
||||
Mac OS X
|
||||
--------
|
||||
|
||||
Please use our DMG package.
|
||||
Please use our DMG package for a simple installation.
|
||||
|
||||
|
||||
If you want to test the current git version or contribute to the project.
|
||||
|
||||
You can follow this instructions with virtualenwrapper: http://virtualenvwrapper.readthedocs.org/
|
||||
and homebrew: http://brew.sh/.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
brew install python3
|
||||
mkvirtualenv gns3-server --python=/usr/local/bin/python3.4
|
||||
python3 setup.py install
|
||||
gns3server
|
||||
|
||||
|
||||
|
@ -53,6 +53,9 @@ class JSONRPCWebSocket(GNS3WebSocketBaseHandler):
|
||||
self._session_id = str(uuid.uuid4())
|
||||
self.zmq_router = zmq_router
|
||||
|
||||
def check_origin(self, origin):
|
||||
return True
|
||||
|
||||
@property
|
||||
def session_id(self):
|
||||
"""
|
||||
|
@ -17,12 +17,13 @@
|
||||
|
||||
import sys
|
||||
from .base import IModule
|
||||
from .deadman import DeadMan
|
||||
from .dynamips import Dynamips
|
||||
from .qemu import Qemu
|
||||
from .vpcs import VPCS
|
||||
from .virtualbox import VirtualBox
|
||||
from .deadman import DeadMan
|
||||
|
||||
MODULES = [Dynamips, VPCS, VirtualBox, DeadMan]
|
||||
MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
# IOU runs only on Linux
|
||||
|
@ -111,7 +111,7 @@ class Dynamips(IModule):
|
||||
dynamips_config = config.get_section_config(name.upper())
|
||||
self._dynamips = dynamips_config.get("dynamips_path")
|
||||
if not self._dynamips or not os.path.isfile(self._dynamips):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(":")
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for Dynamips in the current working directory and $PATH
|
||||
for path in paths:
|
||||
try:
|
||||
@ -137,7 +137,7 @@ class Dynamips(IModule):
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
self._host = kwargs["host"]
|
||||
self._host = dynamips_config.get("host", kwargs["host"])
|
||||
|
||||
if not sys.platform.startswith("win32"):
|
||||
#FIXME: pickle issues Windows
|
||||
|
@ -158,7 +158,8 @@ class VM(object):
|
||||
router = PLATFORMS[platform](hypervisor, name, router_id)
|
||||
router.ram = ram
|
||||
router.image = image
|
||||
router.sparsemem = self._hypervisor_manager.sparse_memory_support
|
||||
if platform not in ("c1700", "c2600"):
|
||||
router.sparsemem = self._hypervisor_manager.sparse_memory_support
|
||||
router.mmap = self._hypervisor_manager.mmap_support
|
||||
if "console" in request:
|
||||
router.console = request["console"]
|
||||
|
@ -51,6 +51,7 @@ class C1700(Router):
|
||||
self._chassis = chassis
|
||||
self._iomem = 15 # percentage
|
||||
self._clock_divisor = 8
|
||||
self._sparsemem = False
|
||||
|
||||
if chassis != "1720":
|
||||
self.chassis = chassis
|
||||
@ -72,7 +73,8 @@ class C1700(Router):
|
||||
"disk1": self._disk1,
|
||||
"chassis": self._chassis,
|
||||
"iomem": self._iomem,
|
||||
"clock_divisor": self._clock_divisor}
|
||||
"clock_divisor": self._clock_divisor,
|
||||
"sparsemem": self._sparsemem}
|
||||
|
||||
# update the router defaults with the platform specific defaults
|
||||
router_defaults.update(platform_defaults)
|
||||
|
@ -66,6 +66,7 @@ class C2600(Router):
|
||||
self._chassis = chassis
|
||||
self._iomem = 15 # percentage
|
||||
self._clock_divisor = 8
|
||||
self._sparsemem = False
|
||||
|
||||
if chassis != "2610":
|
||||
self.chassis = chassis
|
||||
@ -87,7 +88,8 @@ class C2600(Router):
|
||||
"disk1": self._disk1,
|
||||
"iomem": self._iomem,
|
||||
"chassis": self._chassis,
|
||||
"clock_divisor": self._clock_divisor}
|
||||
"clock_divisor": self._clock_divisor,
|
||||
"sparsemem": self._sparsemem}
|
||||
|
||||
# update the router defaults with the platform specific defaults
|
||||
router_defaults.update(platform_defaults)
|
||||
|
@ -24,7 +24,6 @@ from ..dynamips_error import DynamipsError
|
||||
from .router import Router
|
||||
from ..adapters.c7200_io_2fe import C7200_IO_2FE
|
||||
from ..adapters.c7200_io_ge_e import C7200_IO_GE_E
|
||||
from pkg_resources import parse_version
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@ -55,10 +54,6 @@ class C7200(Router):
|
||||
if npe != "npe-400":
|
||||
self.npe = npe
|
||||
|
||||
if parse_version(hypervisor.version) <= parse_version('0.2.13'):
|
||||
# work around a bug when rebooting a router with NPE-400 in Dynamips <= 0.2.13
|
||||
self.npe = "npe-200"
|
||||
|
||||
# 4 sensors with a default temperature of 22C:
|
||||
# sensor 1 = I/0 controller inlet
|
||||
# sensor 2 = I/0 controller outlet
|
||||
|
@ -68,7 +68,7 @@ class IOU(IModule):
|
||||
iou_config = config.get_section_config(name.upper())
|
||||
self._iouyap = iou_config.get("iouyap_path")
|
||||
if not self._iouyap or not os.path.isfile(self._iouyap):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(":")
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for iouyap in the current working directory and $PATH
|
||||
for path in paths:
|
||||
try:
|
||||
@ -87,11 +87,11 @@ class IOU(IModule):
|
||||
IModule.__init__(self, name, *args, **kwargs)
|
||||
self._iou_instances = {}
|
||||
self._console_start_port_range = iou_config.get("console_start_port_range", 4001)
|
||||
self._console_end_port_range = iou_config.get("console_end_port_range", 4512)
|
||||
self._console_end_port_range = iou_config.get("console_end_port_range", 4500)
|
||||
self._allocated_udp_ports = []
|
||||
self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001)
|
||||
self._udp_end_port_range = iou_config.get("udp_end_port_range", 40001)
|
||||
self._host = kwargs["host"]
|
||||
self._udp_end_port_range = iou_config.get("udp_end_port_range", 35000)
|
||||
self._host = iou_config.get("host", kwargs["host"])
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
|
@ -22,7 +22,7 @@ Base interface for NIOs.
|
||||
|
||||
class NIO(object):
|
||||
"""
|
||||
IOU NIO.
|
||||
Network Input/Output.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
||||
|
||||
class NIO_GenericEthernet(NIO):
|
||||
"""
|
||||
NIO generic Ethernet NIO.
|
||||
Generic Ethernet NIO.
|
||||
|
||||
:param ethernet_device: Ethernet device name (e.g. eth0)
|
||||
"""
|
||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
||||
|
||||
class NIO_TAP(NIO):
|
||||
"""
|
||||
IOU TAP NIO.
|
||||
TAP NIO.
|
||||
|
||||
:param tap_device: TAP device name (e.g. tap0)
|
||||
"""
|
||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
||||
|
||||
class NIO_UDP(NIO):
|
||||
"""
|
||||
IOU UDP NIO.
|
||||
UDP NIO.
|
||||
|
||||
:param lport: local port number
|
||||
:param rhost: remote address/host
|
||||
|
657
gns3server/modules/qemu/__init__.py
Normal file
657
gns3server/modules/qemu/__init__.py
Normal file
@ -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
gns3server/modules/qemu/adapters/__init__.py
Normal file
0
gns3server/modules/qemu/adapters/__init__.py
Normal file
104
gns3server/modules/qemu/adapters/adapter.py
Normal file
104
gns3server/modules/qemu/adapters/adapter.py
Normal file
@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
class Adapter(object):
|
||||
"""
|
||||
Base class for adapters.
|
||||
|
||||
:param interfaces: number of interfaces supported by this adapter.
|
||||
"""
|
||||
|
||||
def __init__(self, interfaces=1):
|
||||
|
||||
self._interfaces = interfaces
|
||||
|
||||
self._ports = {}
|
||||
for port_id in range(0, interfaces):
|
||||
self._ports[port_id] = None
|
||||
|
||||
def removable(self):
|
||||
"""
|
||||
Returns True if the adapter can be removed from a slot
|
||||
and False if not.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
def port_exists(self, port_id):
|
||||
"""
|
||||
Checks if a port exists on this adapter.
|
||||
|
||||
:returns: True is the port exists,
|
||||
False otherwise.
|
||||
"""
|
||||
|
||||
if port_id in self._ports:
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_nio(self, port_id, nio):
|
||||
"""
|
||||
Adds a NIO to a port on this adapter.
|
||||
|
||||
:param port_id: port ID (integer)
|
||||
:param nio: NIO instance
|
||||
"""
|
||||
|
||||
self._ports[port_id] = nio
|
||||
|
||||
def remove_nio(self, port_id):
|
||||
"""
|
||||
Removes a NIO from a port on this adapter.
|
||||
|
||||
:param port_id: port ID (integer)
|
||||
"""
|
||||
|
||||
self._ports[port_id] = None
|
||||
|
||||
def get_nio(self, port_id):
|
||||
"""
|
||||
Returns the NIO assigned to a port.
|
||||
|
||||
:params port_id: port ID (integer)
|
||||
|
||||
:returns: NIO instance
|
||||
"""
|
||||
|
||||
return self._ports[port_id]
|
||||
|
||||
@property
|
||||
def ports(self):
|
||||
"""
|
||||
Returns port to NIO mapping
|
||||
|
||||
:returns: dictionary port -> NIO
|
||||
"""
|
||||
|
||||
return self._ports
|
||||
|
||||
@property
|
||||
def interfaces(self):
|
||||
"""
|
||||
Returns the number of interfaces supported by this adapter.
|
||||
|
||||
:returns: number of interfaces
|
||||
"""
|
||||
|
||||
return self._interfaces
|
31
gns3server/modules/qemu/adapters/ethernet_adapter.py
Normal file
31
gns3server/modules/qemu/adapters/ethernet_adapter.py
Normal file
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .adapter import Adapter
|
||||
|
||||
|
||||
class EthernetAdapter(Adapter):
|
||||
"""
|
||||
QEMU Ethernet adapter.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Adapter.__init__(self, interfaces=1)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "QEMU Ethernet adapter"
|
0
gns3server/modules/qemu/nios/__init__.py
Normal file
0
gns3server/modules/qemu/nios/__init__.py
Normal file
65
gns3server/modules/qemu/nios/nio.py
Normal file
65
gns3server/modules/qemu/nios/nio.py
Normal file
@ -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
|
75
gns3server/modules/qemu/nios/nio_udp.py
Normal file
75
gns3server/modules/qemu/nios/nio_udp.py
Normal 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"
|
39
gns3server/modules/qemu/qemu_error.py
Normal file
39
gns3server/modules/qemu/qemu_error.py
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Custom exceptions for 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
|
785
gns3server/modules/qemu/qemu_vm.py
Normal file
785
gns3server/modules/qemu/qemu_vm.py
Normal file
@ -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
|
393
gns3server/modules/qemu/schemas.py
Normal file
393
gns3server/modules/qemu/schemas.py
Normal file
@ -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"]
|
||||
}
|
@ -24,6 +24,7 @@ import os
|
||||
import socket
|
||||
import shutil
|
||||
|
||||
from pkg_resources import parse_version
|
||||
from gns3server.modules import IModule
|
||||
from gns3server.config import Config
|
||||
from .virtualbox_vm import VirtualBoxVM
|
||||
@ -60,25 +61,26 @@ class VirtualBox(IModule):
|
||||
|
||||
def __init__(self, name, *args, **kwargs):
|
||||
|
||||
# get the vboxwrapper location
|
||||
config = Config.instance()
|
||||
vbox_config = config.get_section_config(name.upper())
|
||||
self._vboxwrapper_path = vbox_config.get("vboxwrapper_path")
|
||||
if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(":")
|
||||
# look for iouyap in the current working directory and $PATH
|
||||
for path in paths:
|
||||
try:
|
||||
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
|
||||
self._vboxwrapper_path = os.path.join(path, "vboxwrapper")
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
# get the vboxwrapper location (only non-Windows platforms)
|
||||
if not sys.platform.startswith("win"):
|
||||
config = Config.instance()
|
||||
vbox_config = config.get_section_config(name.upper())
|
||||
self._vboxwrapper_path = vbox_config.get("vboxwrapper_path")
|
||||
if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for vboxwrapper in the current working directory and $PATH
|
||||
for path in paths:
|
||||
try:
|
||||
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
|
||||
self._vboxwrapper_path = os.path.join(path, "vboxwrapper")
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if not self._vboxwrapper_path:
|
||||
log.warning("vboxwrapper couldn't be found!")
|
||||
elif not os.access(self._vboxwrapper_path, os.X_OK):
|
||||
log.warning("vboxwrapper is not executable")
|
||||
if not self._vboxwrapper_path:
|
||||
log.warning("vboxwrapper couldn't be found!")
|
||||
elif not os.access(self._vboxwrapper_path, os.X_OK):
|
||||
log.warning("vboxwrapper is not executable")
|
||||
|
||||
# a new process start when calling IModule
|
||||
IModule.__init__(self, name, *args, **kwargs)
|
||||
@ -91,7 +93,7 @@ class VirtualBox(IModule):
|
||||
self._allocated_udp_ports = []
|
||||
self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001)
|
||||
self._udp_end_port_range = vbox_config.get("udp_end_port_range", 35500)
|
||||
self._host = kwargs["host"]
|
||||
self._host = vbox_config.get("host", kwargs["host"])
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
@ -105,18 +107,30 @@ class VirtualBox(IModule):
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
import pywintypes
|
||||
import win32com.client
|
||||
if win32com.client.gencache.is_readonly is True:
|
||||
# dynamically generate the cache
|
||||
# http://www.py2exe.org/index.cgi/IncludingTypelibs
|
||||
# http://www.py2exe.org/index.cgi/UsingEnsureDispatch
|
||||
win32com.client.gencache.is_readonly = False
|
||||
#win32com.client.gencache.Rebuild()
|
||||
win32com.client.gencache.GetGeneratePath()
|
||||
|
||||
try:
|
||||
if win32com.client.gencache.is_readonly is True:
|
||||
# dynamically generate the cache
|
||||
# http://www.py2exe.org/index.cgi/IncludingTypelibs
|
||||
# http://www.py2exe.org/index.cgi/UsingEnsureDispatch
|
||||
win32com.client.gencache.is_readonly = False
|
||||
#win32com.client.gencache.Rebuild()
|
||||
win32com.client.gencache.GetGeneratePath()
|
||||
|
||||
win32com.client.gencache.EnsureDispatch("VirtualBox.VirtualBox")
|
||||
except pywintypes.com_error:
|
||||
raise VirtualBoxError("VirtualBox is not installed.")
|
||||
|
||||
try:
|
||||
from .vboxapi_py3 import VirtualBoxManager
|
||||
self._vboxmanager = VirtualBoxManager(None, None)
|
||||
vbox_major_version, vbox_minor_version, _ = self._vboxmanager.vbox.version.split('.')
|
||||
if parse_version("{}.{}".format(vbox_major_version, vbox_minor_version)) <= parse_version("4.1"):
|
||||
raise VirtualBoxError("VirtualBox version must be >= 4.2")
|
||||
except Exception as e:
|
||||
self._vboxmanager = None
|
||||
raise VirtualBoxError("Could not initialize the VirtualBox Manager: {}".format(e))
|
||||
|
||||
log.info("VirtualBox Manager has successful started: version is {} r{}".format(self._vboxmanager.vbox.version,
|
||||
@ -131,7 +145,11 @@ class VirtualBox(IModule):
|
||||
|
||||
self._vboxwrapper = VboxWrapperClient(self._vboxwrapper_path, self._tempdir, "127.0.0.1")
|
||||
#self._vboxwrapper.connect()
|
||||
self._vboxwrapper.start()
|
||||
try:
|
||||
self._vboxwrapper.start()
|
||||
except VirtualBoxError:
|
||||
self._vboxwrapper = None
|
||||
raise
|
||||
|
||||
def stop(self, signum=None):
|
||||
"""
|
||||
@ -154,9 +172,9 @@ class VirtualBox(IModule):
|
||||
"""
|
||||
Returns a VirtualBox VM instance.
|
||||
|
||||
:param vbox_id: VirtualBox device identifier
|
||||
:param vbox_id: VirtualBox VM identifier
|
||||
|
||||
:returns: VBoxDevice instance
|
||||
:returns: VirtualBoxVM instance
|
||||
"""
|
||||
|
||||
if vbox_id not in self._vbox_instances:
|
||||
@ -253,6 +271,7 @@ class VirtualBox(IModule):
|
||||
|
||||
Mandatory request parameters:
|
||||
- name (VirtualBox VM name)
|
||||
- vmname (VirtualBox VM name in VirtualBox)
|
||||
|
||||
Optional request parameters:
|
||||
- console (VirtualBox VM console port)
|
||||
@ -399,7 +418,10 @@ class VirtualBox(IModule):
|
||||
try:
|
||||
vbox_instance.start()
|
||||
except VirtualBoxError as e:
|
||||
self.send_custom_error(str(e))
|
||||
if self._vboxwrapper:
|
||||
self.send_custom_error("{}: {}".format(e, self._vboxwrapper.read_stderr()))
|
||||
else:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
self.send_response(True)
|
||||
|
||||
@ -632,7 +654,7 @@ class VirtualBox(IModule):
|
||||
Deletes an NIO (Network Input/Output).
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (VPCS instance identifier)
|
||||
- id (VirtualBox instance identifier)
|
||||
- port (port identifier)
|
||||
|
||||
Response parameters:
|
||||
@ -667,7 +689,7 @@ class VirtualBox(IModule):
|
||||
Starts a packet capture.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (vm identifier)
|
||||
- id (VirtualBox VM identifier)
|
||||
- port (port number)
|
||||
- port_id (port identifier)
|
||||
- capture_file_name
|
||||
@ -708,7 +730,7 @@ class VirtualBox(IModule):
|
||||
Stops a packet capture.
|
||||
|
||||
Mandatory request parameters:
|
||||
- id (vm identifier)
|
||||
- id (VirtualBox VM identifier)
|
||||
- port (port number)
|
||||
- port_id (port identifier)
|
||||
|
||||
@ -748,7 +770,11 @@ class VirtualBox(IModule):
|
||||
"""
|
||||
|
||||
if not self._vboxwrapper and not self._vboxmanager:
|
||||
self._start_vbox_service()
|
||||
try:
|
||||
self._start_vbox_service()
|
||||
except VirtualBoxError as e:
|
||||
self.send_custom_error(str(e))
|
||||
return
|
||||
|
||||
if self._vboxwrapper:
|
||||
vms = self._vboxwrapper.get_vm_list()
|
||||
|
@ -22,7 +22,7 @@ Base interface for NIOs.
|
||||
|
||||
class NIO(object):
|
||||
"""
|
||||
IOU NIO.
|
||||
Network Input/Output.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
||||
|
||||
class NIO_UDP(NIO):
|
||||
"""
|
||||
IOU UDP NIO.
|
||||
UDP NIO.
|
||||
|
||||
:param lport: local port number
|
||||
:param rhost: remote address/host
|
||||
|
@ -82,9 +82,15 @@ VBOX_UPDATE_SCHEMA = {
|
||||
"adapters": {
|
||||
"description": "number of adapters",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"minimum": 1,
|
||||
"maximum": 36, # maximum given by the ICH9 chipset in VirtualBox
|
||||
},
|
||||
"adapter_start_index": {
|
||||
"description": "adapter index from which to start using adapters",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 35, # maximum given by the ICH9 chipset in VirtualBox
|
||||
},
|
||||
"adapter_type": {
|
||||
"description": "VirtualBox adapter type",
|
||||
"type": "string",
|
||||
@ -96,6 +102,10 @@ VBOX_UPDATE_SCHEMA = {
|
||||
"maximum": 65535,
|
||||
"type": "integer"
|
||||
},
|
||||
"enable_console": {
|
||||
"description": "enable the console",
|
||||
"type": "boolean"
|
||||
},
|
||||
"headless": {
|
||||
"description": "headless mode",
|
||||
"type": "boolean"
|
||||
|
@ -26,6 +26,7 @@ import tempfile
|
||||
import socket
|
||||
import re
|
||||
|
||||
from pkg_resources import parse_version
|
||||
from ..attic import wait_socket_is_ready
|
||||
from .virtualbox_error import VirtualBoxError
|
||||
|
||||
@ -53,7 +54,7 @@ class VboxWrapperClient(object):
|
||||
self._command = []
|
||||
self._process = None
|
||||
self._working_dir = working_dir
|
||||
self._stdout_file = ""
|
||||
self._stderr_file = ""
|
||||
self._started = False
|
||||
self._host = host
|
||||
self._port = port
|
||||
@ -139,19 +140,31 @@ class VboxWrapperClient(object):
|
||||
try:
|
||||
log.info("starting VirtualBox wrapper: {}".format(self._command))
|
||||
with tempfile.NamedTemporaryFile(delete=False) as fd:
|
||||
self._stdout_file = fd.name
|
||||
log.info("VirtualBox wrapper process logging to {}".format(fd.name))
|
||||
self._process = subprocess.Popen(self._command,
|
||||
stdout=fd,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=self._working_dir)
|
||||
with open(os.devnull, "w") as null:
|
||||
self._stderr_file = fd.name
|
||||
log.info("VirtualBox wrapper process logging to {}".format(fd.name))
|
||||
self._process = subprocess.Popen(self._command,
|
||||
stdout=null,
|
||||
stderr=fd,
|
||||
cwd=self._working_dir)
|
||||
log.info("VirtualBox wrapper started PID={}".format(self._process.pid))
|
||||
|
||||
time.sleep(0.1) # give some time for vboxwrapper to start
|
||||
if self._process.poll() is not None:
|
||||
raise VirtualBoxError("Could not start VirtualBox wrapper: {}".format(self.read_stderr()))
|
||||
|
||||
self.wait_for_vboxwrapper(self._host, self._port)
|
||||
self.connect()
|
||||
self._started = True
|
||||
|
||||
version = self.send('vboxwrapper version')[0]
|
||||
if parse_version(version) < parse_version("0.9.1"):
|
||||
self.stop()
|
||||
raise VirtualBoxError("VirtualBox wrapper version must be >= 0.9.1")
|
||||
|
||||
except OSError as e:
|
||||
log.error("could not start VirtualBox wrapper: {}".format(e))
|
||||
raise VirtualBoxError("could not start VirtualBox wrapper: {}".format(e))
|
||||
raise VirtualBoxError("Could not start VirtualBox wrapper: {}".format(e))
|
||||
|
||||
def wait_for_vboxwrapper(self, host, port):
|
||||
"""
|
||||
@ -198,26 +211,26 @@ class VboxWrapperClient(object):
|
||||
if self._process.poll() is None:
|
||||
log.warn("VirtualBox wrapper process {} is still running".format(self._process.pid))
|
||||
|
||||
if self._stdout_file and os.access(self._stdout_file, os.W_OK):
|
||||
if self._stderr_file and os.access(self._stderr_file, os.W_OK):
|
||||
try:
|
||||
os.remove(self._stdout_file)
|
||||
os.remove(self._stderr_file)
|
||||
except OSError as e:
|
||||
log.warning("could not delete temporary VirtualBox wrapper log file: {}".format(e))
|
||||
self._started = False
|
||||
|
||||
def read_stdout(self):
|
||||
def read_stderr(self):
|
||||
"""
|
||||
Reads the standard output of the VirtualBox wrapper process.
|
||||
Reads the standard error output of the VirtualBox wrapper process.
|
||||
Only use when the process has been stopped or has crashed.
|
||||
"""
|
||||
|
||||
output = ""
|
||||
if self._stdout_file and os.access(self._stdout_file, os.R_OK):
|
||||
if self._stderr_file and os.access(self._stderr_file, os.R_OK):
|
||||
try:
|
||||
with open(self._stdout_file, errors="replace") as file:
|
||||
with open(self._stderr_file, errors="replace") as file:
|
||||
output = file.read()
|
||||
except OSError as e:
|
||||
log.warn("could not read {}: {}".format(self._stdout_file, e))
|
||||
log.warn("could not read {}: {}".format(self._stderr_file, e))
|
||||
return output
|
||||
|
||||
def is_running(self):
|
||||
|
557
gns3server/modules/virtualbox/virtualbox_controller.py
Normal file
557
gns3server/modules/virtualbox/virtualbox_controller.py
Normal file
@ -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
|
@ -19,24 +19,14 @@
|
||||
VirtualBox VM instance.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import re
|
||||
import time
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
from .pipe_proxy import PipeProxy
|
||||
from .virtualbox_error import VirtualBoxError
|
||||
from .virtualbox_controller import VirtualBoxController
|
||||
from .adapters.ethernet_adapter import EthernetAdapter
|
||||
from ..attic import find_unused_port
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
import msvcrt
|
||||
import win32file
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -97,21 +87,13 @@ class VirtualBoxVM(object):
|
||||
self._console_start_port_range = console_start_port_range
|
||||
self._console_end_port_range = console_end_port_range
|
||||
|
||||
# Telnet to pipe mini-server
|
||||
self._serial_pipe_thread = None
|
||||
self._serial_pipe = None
|
||||
|
||||
# VirtualBox API variables
|
||||
self._machine = None
|
||||
self._session = None
|
||||
self._vboxmanager = vboxmanager
|
||||
self._maximum_adapters = 0
|
||||
|
||||
# VirtualBox settings
|
||||
self._console = console
|
||||
self._ethernet_adapters = []
|
||||
self._headless = False
|
||||
self._enable_console = True
|
||||
self._vmname = vmname
|
||||
self._adapter_start_index = 0
|
||||
self._adapter_type = "Automatic"
|
||||
|
||||
working_dir_path = os.path.join(working_dir, "vbox", "vm-{}".format(self._id))
|
||||
@ -140,19 +122,11 @@ class VirtualBoxVM(object):
|
||||
self._vboxwrapper.send('vbox create vbox "{}"'.format(self._name))
|
||||
self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname))
|
||||
self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console))
|
||||
self._vboxwrapper.send('vbox setattr "{}" console_support True'.format(self._name))
|
||||
self._vboxwrapper.send('vbox setattr "{}" console_telnet_server True'.format(self._name))
|
||||
else:
|
||||
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)
|
||||
|
||||
self.adapters = 2
|
||||
self._vboxcontroller = VirtualBoxController(self._vmname, vboxmanager, self._host)
|
||||
self._vboxcontroller.console = self._console
|
||||
|
||||
self.adapters = 2 # creates 2 adapters by default
|
||||
log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name,
|
||||
id=self._id))
|
||||
|
||||
@ -165,9 +139,11 @@ class VirtualBoxVM(object):
|
||||
|
||||
vbox_defaults = {"name": self._name,
|
||||
"vmname": self._vmname,
|
||||
"adapters": len(self._ethernet_adapters),
|
||||
"adapters": self.adapters,
|
||||
"adapter_start_index": self._adapter_start_index,
|
||||
"adapter_type": "Automatic",
|
||||
"console": self._console,
|
||||
"enable_console": self._enable_console,
|
||||
"headless": self._headless}
|
||||
|
||||
return vbox_defaults
|
||||
@ -274,6 +250,8 @@ class VirtualBoxVM(object):
|
||||
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console))
|
||||
else:
|
||||
self._vboxcontroller.console = console
|
||||
|
||||
log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name,
|
||||
id=self._id,
|
||||
@ -344,13 +322,49 @@ class VirtualBoxVM(object):
|
||||
if headless:
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" headless_mode True'.format(self._name))
|
||||
else:
|
||||
self._vboxcontroller.headless = True
|
||||
log.info("VirtualBox VM {name} [id={id}] has enabled the headless mode".format(name=self._name, id=self._id))
|
||||
else:
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" headless_mode False'.format(self._name))
|
||||
else:
|
||||
self._vboxcontroller.headless = False
|
||||
log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id))
|
||||
self._headless = headless
|
||||
|
||||
@property
|
||||
def enable_console(self):
|
||||
"""
|
||||
Returns either the console is enabled or not
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._enable_console
|
||||
|
||||
@enable_console.setter
|
||||
def enable_console(self, enable_console):
|
||||
"""
|
||||
Sets either the console is enabled or not
|
||||
|
||||
:param enable_console: boolean
|
||||
"""
|
||||
|
||||
if enable_console:
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" enable_console True'.format(self._name))
|
||||
else:
|
||||
self._vboxcontroller.enable_console = True
|
||||
log.info("VirtualBox VM {name} [id={id}] has enabled the console".format(name=self._name, id=self._id))
|
||||
else:
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" enable_console False'.format(self._name))
|
||||
else:
|
||||
self._vboxcontroller.enable_console = False
|
||||
log.info("VirtualBox VM {name} [id={id}] has disabled the console".format(name=self._name, id=self._id))
|
||||
self._enable_console = enable_console
|
||||
|
||||
@property
|
||||
def vmname(self):
|
||||
"""
|
||||
@ -372,13 +386,7 @@ class VirtualBoxVM(object):
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname))
|
||||
else:
|
||||
try:
|
||||
self._machine = self._vboxmanager.vbox.findMachine(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)
|
||||
self._vboxcontroller.vmname = vmname
|
||||
|
||||
log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, vmname=vmname))
|
||||
self._vmname = vmname
|
||||
@ -402,15 +410,47 @@ class VirtualBoxVM(object):
|
||||
"""
|
||||
|
||||
self._ethernet_adapters.clear()
|
||||
for _ in range(0, adapters):
|
||||
for adapter_id in range(0, self._adapter_start_index + adapters):
|
||||
if adapter_id < self._adapter_start_index:
|
||||
self._ethernet_adapters.append(None)
|
||||
continue
|
||||
self._ethernet_adapters.append(EthernetAdapter())
|
||||
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, len(self._ethernet_adapters)))
|
||||
self._vboxwrapper.send('vbox setattr "{}" nics {}'.format(self._name, adapters))
|
||||
else:
|
||||
self._vboxcontroller.adapters = self._ethernet_adapters
|
||||
|
||||
log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
|
||||
id=self._id,
|
||||
adapters=len(self._ethernet_adapters)))
|
||||
adapters=adapters))
|
||||
|
||||
@property
|
||||
def adapter_start_index(self):
|
||||
"""
|
||||
Returns the adapter start index for this VirtualBox VM instance.
|
||||
|
||||
:returns: index
|
||||
"""
|
||||
|
||||
return self._adapter_start_index
|
||||
|
||||
@adapter_start_index.setter
|
||||
def adapter_start_index(self, adapter_start_index):
|
||||
"""
|
||||
Sets the adapter start index for this VirtualBox VM instance.
|
||||
|
||||
:param adapter_start_index: index
|
||||
"""
|
||||
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" nic_start_index {}'.format(self._name, adapter_start_index))
|
||||
|
||||
self._adapter_start_index = adapter_start_index
|
||||
self.adapters = self.adapters # this forces to recreate the adapter list with the correct index
|
||||
log.info("VirtualBox VM {name} [id={id}]: adapter start index changed to {index}".format(name=self._name,
|
||||
id=self._id,
|
||||
index=adapter_start_index))
|
||||
|
||||
@property
|
||||
def adapter_type(self):
|
||||
@ -434,6 +474,8 @@ class VirtualBoxVM(object):
|
||||
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox setattr "{}" netcard "{}"'.format(self._name, adapter_type))
|
||||
else:
|
||||
self._vboxcontroller.adapter_type = adapter_type
|
||||
|
||||
log.info("VirtualBox VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name,
|
||||
id=self._id,
|
||||
@ -445,54 +487,9 @@ class VirtualBoxVM(object):
|
||||
"""
|
||||
|
||||
if self._vboxwrapper:
|
||||
status = int(self._vboxwrapper.send('vbox status "{}"'.format(self._name))[0])
|
||||
if status == 6: # paused
|
||||
self.resume()
|
||||
return
|
||||
self._vboxwrapper.send('vbox start "{}"'.format(self._name))
|
||||
else:
|
||||
|
||||
if self._machine.state == self._vboxmanager.constants.MachineState_Paused:
|
||||
self.resume()
|
||||
return
|
||||
|
||||
self._get_session()
|
||||
self._set_network_options()
|
||||
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
|
||||
|
||||
# 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()
|
||||
self._vboxcontroller.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
@ -500,48 +497,13 @@ class VirtualBoxVM(object):
|
||||
"""
|
||||
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox stop "{}"'.format(self._name))
|
||||
try:
|
||||
self._vboxwrapper.send('vbox stop "{}"'.format(self._name))
|
||||
except VirtualBoxError:
|
||||
# probably lost the connection
|
||||
return
|
||||
else:
|
||||
|
||||
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._ethernet_adapters)):
|
||||
self._disable_adapter(adapter_id, disable=True)
|
||||
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
|
||||
self._vboxcontroller.stop()
|
||||
|
||||
def suspend(self):
|
||||
"""
|
||||
@ -551,10 +513,7 @@ class VirtualBoxVM(object):
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox suspend "{}"'.format(self._name))
|
||||
else:
|
||||
try:
|
||||
self._session.console.pause()
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
self._vboxcontroller.suspend()
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
@ -564,11 +523,7 @@ class VirtualBoxVM(object):
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox reset "{}"'.format(self._name))
|
||||
else:
|
||||
try:
|
||||
progress = self._session.console.reset()
|
||||
progress.waitForCompletion(-1)
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
self._vboxcontroller.reload()
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
@ -578,10 +533,7 @@ class VirtualBoxVM(object):
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox resume "{}"'.format(self._name))
|
||||
else:
|
||||
try:
|
||||
self._session.console.resume()
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
self._vboxcontroller.resume()
|
||||
|
||||
def port_add_nio_binding(self, adapter_id, nio):
|
||||
"""
|
||||
@ -599,12 +551,12 @@ class VirtualBoxVM(object):
|
||||
|
||||
if self._vboxwrapper:
|
||||
self._vboxwrapper.send('vbox create_udp "{}" {} {} {} {}'.format(self._name,
|
||||
adapter_id,
|
||||
nio.lport,
|
||||
nio.rhost,
|
||||
nio.rport))
|
||||
adapter_id,
|
||||
nio.lport,
|
||||
nio.rhost,
|
||||
nio.rport))
|
||||
else:
|
||||
self._create_udp(adapter_id, nio.lport, nio.rhost, nio.rport)
|
||||
self._vboxcontroller.create_udp(adapter_id, nio.lport, nio.rhost, nio.rport)
|
||||
|
||||
adapter.add_nio(0, nio)
|
||||
log.info("VirtualBox VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name,
|
||||
@ -631,7 +583,7 @@ class VirtualBoxVM(object):
|
||||
self._vboxwrapper.send('vbox delete_udp "{}" {}'.format(self._name,
|
||||
adapter_id))
|
||||
else:
|
||||
self._delete_udp(adapter_id)
|
||||
self._vboxcontroller.delete_udp(adapter_id)
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
adapter.remove_nio(0)
|
||||
@ -700,287 +652,3 @@ class VirtualBoxVM(object):
|
||||
log.info("VirtualBox VM {name} [id={id}]: stopping packet capture on adapter {adapter_id}".format(name=self._name,
|
||||
id=self._id,
|
||||
adapter_id=adapter_id))
|
||||
|
||||
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._ethernet_adapters)):
|
||||
try:
|
||||
# VirtualBox starts counting from 0
|
||||
adapter = self._session.machine.getNetworkAdapter(adapter_id)
|
||||
adapter_type = adapter.adapterType
|
||||
|
||||
if self._adapter_type == "PCnet-PCI II (Am79C970A)":
|
||||
adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C970A
|
||||
if self._adapter_type == "PCNet-FAST III (Am79C973)":
|
||||
adapter_type = self._vboxmanager.constants.NetworkAdapterType_Am79C973
|
||||
if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)":
|
||||
adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82540EM
|
||||
if self._adapter_type == "Intel PRO/1000 T Server (82543GC)":
|
||||
adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82543GC
|
||||
if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)":
|
||||
adapter_type = self._vboxmanager.constants.NetworkAdapterType_I82545EM
|
||||
if self._adapter_type == "Paravirtualized Network (virtio-net)":
|
||||
adapter_type = self._vboxmanager.constants.NetworkAdapterType_Virtio
|
||||
if self._adapter_type == "Automatic": # "Auto-guess, based on first NIC"
|
||||
adapter_type = first_adapter_type
|
||||
|
||||
adapter.adapterType = adapter_type
|
||||
|
||||
except Exception as e:
|
||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
||||
|
||||
nio = self._ethernet_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._ethernet_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):
|
||||
|
||||
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
|
||||
|
@ -63,7 +63,7 @@ class VPCS(IModule):
|
||||
vpcs_config = config.get_section_config(name.upper())
|
||||
self._vpcs = vpcs_config.get("vpcs_path")
|
||||
if not self._vpcs or not os.path.isfile(self._vpcs):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(":")
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for VPCS in the current working directory and $PATH
|
||||
for path in paths:
|
||||
try:
|
||||
@ -81,12 +81,12 @@ class VPCS(IModule):
|
||||
# a new process start when calling IModule
|
||||
IModule.__init__(self, name, *args, **kwargs)
|
||||
self._vpcs_instances = {}
|
||||
self._console_start_port_range = vpcs_config.get("console_start_port_range", 4512)
|
||||
self._console_start_port_range = vpcs_config.get("console_start_port_range", 4501)
|
||||
self._console_end_port_range = vpcs_config.get("console_end_port_range", 5000)
|
||||
self._allocated_udp_ports = []
|
||||
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 40001)
|
||||
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 40512)
|
||||
self._host = kwargs["host"]
|
||||
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 20501)
|
||||
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000)
|
||||
self._host = vpcs_config.get("host", kwargs["host"])
|
||||
self._projects_dir = kwargs["projects_dir"]
|
||||
self._tempdir = kwargs["temp_dir"]
|
||||
self._working_dir = self._projects_dir
|
||||
|
@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only).
|
||||
|
||||
class NIO_TAP(object):
|
||||
"""
|
||||
IOU TAP NIO.
|
||||
TAP NIO.
|
||||
|
||||
:param tap_device: TAP device name (e.g. tap0)
|
||||
"""
|
||||
|
@ -22,7 +22,7 @@ Interface for UDP NIOs.
|
||||
|
||||
class NIO_UDP(object):
|
||||
"""
|
||||
IOU UDP NIO.
|
||||
UDP NIO.
|
||||
|
||||
:param lport: local port number
|
||||
:param rhost: remote address/host
|
||||
|
@ -338,11 +338,10 @@ class VPCSDevice(object):
|
||||
"""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output([self._path, "-v"], stderr=subprocess.STDOUT, cwd=self._working_dir)
|
||||
output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir)
|
||||
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
|
||||
if match:
|
||||
version = match.group(1)
|
||||
print(version)
|
||||
if parse_version(version) < parse_version("0.5b1"):
|
||||
raise VPCSError("VPCS executable version must be >= 0.5b1")
|
||||
else:
|
||||
|
@ -23,5 +23,5 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "1.0beta1"
|
||||
__version__ = "1.0beta4.dev1"
|
||||
__version_info__ = (1, 0, 0, -99)
|
||||
|
12
setup.py
12
setup.py
@ -20,7 +20,7 @@ from setuptools import setup, find_packages
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
class Tox(TestCommand):
|
||||
class PyTest(TestCommand):
|
||||
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
@ -29,8 +29,8 @@ class Tox(TestCommand):
|
||||
|
||||
def run_tests(self):
|
||||
#import here, cause outside the eggs aren't loaded
|
||||
import tox
|
||||
errcode = tox.cmdline(self.test_args)
|
||||
import pytest
|
||||
errcode = pytest.main(self.test_args)
|
||||
sys.exit(errcode)
|
||||
|
||||
setup(
|
||||
@ -38,8 +38,8 @@ setup(
|
||||
version=__import__("gns3server").__version__,
|
||||
url="http://github.com/GNS3/gns3-server",
|
||||
license="GNU General Public License v3 (GPLv3)",
|
||||
tests_require=["tox"],
|
||||
cmdclass={"test": Tox},
|
||||
tests_require=["pytest"],
|
||||
cmdclass={"test": PyTest},
|
||||
author="Jeremy Grossmann",
|
||||
author_email="package-maintainer@gns3.net",
|
||||
description="GNS3 server to asynchronously manage emulators",
|
||||
@ -60,7 +60,7 @@ setup(
|
||||
include_package_data=True,
|
||||
platforms="any",
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Information Technology",
|
||||
"Topic :: System :: Networking",
|
||||
|
@ -6,10 +6,9 @@ import os
|
||||
@pytest.fixture(scope="module")
|
||||
def hypervisor(request):
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
||||
dynamips_path = '/usr/bin/dynamips'
|
||||
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
|
||||
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000)
|
||||
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1")
|
||||
hypervisor = manager.start_new_hypervisor()
|
||||
|
||||
def stop():
|
||||
|
@ -1,6 +1,5 @@
|
||||
from gns3server.modules.dynamips import Hypervisor
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
def test_is_started(hypervisor):
|
||||
@ -10,7 +9,7 @@ def test_is_started(hypervisor):
|
||||
|
||||
def test_port(hypervisor):
|
||||
|
||||
assert hypervisor.port == 9000
|
||||
assert hypervisor.port == 7200
|
||||
|
||||
|
||||
def test_host(hypervisor):
|
||||
@ -25,8 +24,7 @@ def test_working_dir(hypervisor):
|
||||
|
||||
def test_path(hypervisor):
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
||||
dynamips_path = '/usr/bin/dynamips'
|
||||
assert hypervisor.path == dynamips_path
|
||||
|
||||
|
||||
@ -34,11 +32,10 @@ def test_stdout():
|
||||
|
||||
# try to launch Dynamips on the same port
|
||||
# this will fail so that we can read its stdout/stderr
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
||||
hypervisor = Hypervisor(dynamips_path, "/tmp", "172.0.0.1", 7200)
|
||||
dynamips_path = '/usr/bin/dynamips'
|
||||
hypervisor = Hypervisor(dynamips_path, "/tmp", "127.0.0.1", 7200)
|
||||
hypervisor.start()
|
||||
# give some time for Dynamips to start
|
||||
time.sleep(0.01)
|
||||
time.sleep(0.1)
|
||||
output = hypervisor.read_stdout()
|
||||
assert output
|
||||
|
@ -7,10 +7,9 @@ import os
|
||||
@pytest.fixture(scope="module")
|
||||
def hypervisor_manager(request):
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
||||
dynamips_path = '/usr/bin/dynamips'
|
||||
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
|
||||
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1", 9000)
|
||||
manager = HypervisorManager(dynamips_path, "/tmp", "127.0.0.1")
|
||||
|
||||
#manager.start_new_hypervisor()
|
||||
|
||||
|
@ -9,7 +9,7 @@ import base64
|
||||
@pytest.fixture
|
||||
def router(request, hypervisor):
|
||||
|
||||
router = Router(hypervisor, "router", "c3725")
|
||||
router = Router(hypervisor, "router", platform="c3725")
|
||||
request.addfinalizer(router.delete)
|
||||
return router
|
||||
|
||||
@ -127,9 +127,9 @@ def test_idlepc(router):
|
||||
|
||||
def test_idlemax(router):
|
||||
|
||||
assert router.idlemax == 1500 # default value
|
||||
router.idlemax = 500
|
||||
assert router.idlemax == 500
|
||||
assert router.idlemax == 500 # default value
|
||||
router.idlemax = 1500
|
||||
assert router.idlemax == 1500
|
||||
|
||||
|
||||
def test_idlesleep(router):
|
||||
@ -172,7 +172,7 @@ def test_confreg(router):
|
||||
|
||||
def test_console(router):
|
||||
|
||||
assert router.console == router.hypervisor.baseconsole + router.id
|
||||
assert router.console == 2001
|
||||
new_console_port = router.console + 100
|
||||
router.console = new_console_port
|
||||
assert router.console == new_console_port
|
||||
@ -180,7 +180,7 @@ def test_console(router):
|
||||
|
||||
def test_aux(router):
|
||||
|
||||
assert router.aux == router.hypervisor.baseaux + router.id
|
||||
assert router.aux == 2501
|
||||
new_aux_port = router.aux + 100
|
||||
router.aux = new_aux_port
|
||||
assert router.aux == new_aux_port
|
||||
|
@ -3,17 +3,28 @@ import os
|
||||
import pytest
|
||||
|
||||
|
||||
def no_iou():
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
|
||||
|
||||
if os.path.isfile(iou_path):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def iou(request):
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
|
||||
iou_device = IOUDevice(iou_path, "/tmp")
|
||||
iou_device = IOUDevice("IOU1", iou_path, "/tmp")
|
||||
iou_device.start()
|
||||
request.addfinalizer(iou_device.delete)
|
||||
return iou_device
|
||||
|
||||
|
||||
@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
|
||||
def test_iou_is_started(iou):
|
||||
|
||||
print(iou.command())
|
||||
@ -21,6 +32,7 @@ def test_iou_is_started(iou):
|
||||
assert iou.is_running()
|
||||
|
||||
|
||||
@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
|
||||
def test_iou_restart(iou):
|
||||
|
||||
iou.stop()
|
||||
|
@ -6,9 +6,13 @@ import pytest
|
||||
@pytest.fixture(scope="session")
|
||||
def vpcs(request):
|
||||
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
vpcs_path = os.path.join(cwd, "vpcs")
|
||||
vpcs_device = VPCSDevice(vpcs_path, "/tmp")
|
||||
if os.path.isfile("/usr/bin/vpcs"):
|
||||
vpcs_path = "/usr/bin/vpcs"
|
||||
else:
|
||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||
vpcs_path = os.path.join(cwd, "vpcs")
|
||||
vpcs_device = VPCSDevice("VPCS1", vpcs_path, "/tmp")
|
||||
vpcs_device.port_add_nio_binding(0, 'nio_tap:tap0')
|
||||
vpcs_device.start()
|
||||
request.addfinalizer(vpcs_device.delete)
|
||||
return vpcs_device
|
||||
|
Loading…
Reference in New Issue
Block a user