mirror of
https://github.com/GNS3/gns3-server.git
synced 2025-01-31 05:13:49 +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
|
language: python
|
||||||
|
|
||||||
python:
|
env:
|
||||||
- "3.3"
|
- TOX_ENV=py33
|
||||||
- "3.4"
|
- TOX_ENV=py34
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- sudo add-apt-repository ppa:gns3/ppa -y
|
||||||
|
- sudo apt-get update -q
|
||||||
|
|
||||||
install:
|
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:
|
branches:
|
||||||
only:
|
only:
|
||||||
|
19
README.rst
19
README.rst
@ -1,7 +1,7 @@
|
|||||||
GNS3-server
|
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.
|
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.
|
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
|
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._session_id = str(uuid.uuid4())
|
||||||
self.zmq_router = zmq_router
|
self.zmq_router = zmq_router
|
||||||
|
|
||||||
|
def check_origin(self, origin):
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session_id(self):
|
def session_id(self):
|
||||||
"""
|
"""
|
||||||
|
@ -17,12 +17,13 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from .base import IModule
|
from .base import IModule
|
||||||
|
from .deadman import DeadMan
|
||||||
from .dynamips import Dynamips
|
from .dynamips import Dynamips
|
||||||
|
from .qemu import Qemu
|
||||||
from .vpcs import VPCS
|
from .vpcs import VPCS
|
||||||
from .virtualbox import VirtualBox
|
from .virtualbox import VirtualBox
|
||||||
from .deadman import DeadMan
|
|
||||||
|
|
||||||
MODULES = [Dynamips, VPCS, VirtualBox, DeadMan]
|
MODULES = [DeadMan, Dynamips, VPCS, VirtualBox, Qemu]
|
||||||
|
|
||||||
if sys.platform.startswith("linux"):
|
if sys.platform.startswith("linux"):
|
||||||
# IOU runs only on Linux
|
# IOU runs only on Linux
|
||||||
|
@ -111,7 +111,7 @@ class Dynamips(IModule):
|
|||||||
dynamips_config = config.get_section_config(name.upper())
|
dynamips_config = config.get_section_config(name.upper())
|
||||||
self._dynamips = dynamips_config.get("dynamips_path")
|
self._dynamips = dynamips_config.get("dynamips_path")
|
||||||
if not self._dynamips or not os.path.isfile(self._dynamips):
|
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
|
# look for Dynamips in the current working directory and $PATH
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
@ -137,7 +137,7 @@ class Dynamips(IModule):
|
|||||||
self._projects_dir = kwargs["projects_dir"]
|
self._projects_dir = kwargs["projects_dir"]
|
||||||
self._tempdir = kwargs["temp_dir"]
|
self._tempdir = kwargs["temp_dir"]
|
||||||
self._working_dir = self._projects_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"):
|
if not sys.platform.startswith("win32"):
|
||||||
#FIXME: pickle issues Windows
|
#FIXME: pickle issues Windows
|
||||||
|
@ -158,7 +158,8 @@ class VM(object):
|
|||||||
router = PLATFORMS[platform](hypervisor, name, router_id)
|
router = PLATFORMS[platform](hypervisor, name, router_id)
|
||||||
router.ram = ram
|
router.ram = ram
|
||||||
router.image = image
|
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
|
router.mmap = self._hypervisor_manager.mmap_support
|
||||||
if "console" in request:
|
if "console" in request:
|
||||||
router.console = request["console"]
|
router.console = request["console"]
|
||||||
|
@ -51,6 +51,7 @@ class C1700(Router):
|
|||||||
self._chassis = chassis
|
self._chassis = chassis
|
||||||
self._iomem = 15 # percentage
|
self._iomem = 15 # percentage
|
||||||
self._clock_divisor = 8
|
self._clock_divisor = 8
|
||||||
|
self._sparsemem = False
|
||||||
|
|
||||||
if chassis != "1720":
|
if chassis != "1720":
|
||||||
self.chassis = chassis
|
self.chassis = chassis
|
||||||
@ -72,7 +73,8 @@ class C1700(Router):
|
|||||||
"disk1": self._disk1,
|
"disk1": self._disk1,
|
||||||
"chassis": self._chassis,
|
"chassis": self._chassis,
|
||||||
"iomem": self._iomem,
|
"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
|
# update the router defaults with the platform specific defaults
|
||||||
router_defaults.update(platform_defaults)
|
router_defaults.update(platform_defaults)
|
||||||
|
@ -66,6 +66,7 @@ class C2600(Router):
|
|||||||
self._chassis = chassis
|
self._chassis = chassis
|
||||||
self._iomem = 15 # percentage
|
self._iomem = 15 # percentage
|
||||||
self._clock_divisor = 8
|
self._clock_divisor = 8
|
||||||
|
self._sparsemem = False
|
||||||
|
|
||||||
if chassis != "2610":
|
if chassis != "2610":
|
||||||
self.chassis = chassis
|
self.chassis = chassis
|
||||||
@ -87,7 +88,8 @@ class C2600(Router):
|
|||||||
"disk1": self._disk1,
|
"disk1": self._disk1,
|
||||||
"iomem": self._iomem,
|
"iomem": self._iomem,
|
||||||
"chassis": self._chassis,
|
"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
|
# update the router defaults with the platform specific defaults
|
||||||
router_defaults.update(platform_defaults)
|
router_defaults.update(platform_defaults)
|
||||||
|
@ -24,7 +24,6 @@ from ..dynamips_error import DynamipsError
|
|||||||
from .router import Router
|
from .router import Router
|
||||||
from ..adapters.c7200_io_2fe import C7200_IO_2FE
|
from ..adapters.c7200_io_2fe import C7200_IO_2FE
|
||||||
from ..adapters.c7200_io_ge_e import C7200_IO_GE_E
|
from ..adapters.c7200_io_ge_e import C7200_IO_GE_E
|
||||||
from pkg_resources import parse_version
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@ -55,10 +54,6 @@ class C7200(Router):
|
|||||||
if npe != "npe-400":
|
if npe != "npe-400":
|
||||||
self.npe = npe
|
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:
|
# 4 sensors with a default temperature of 22C:
|
||||||
# sensor 1 = I/0 controller inlet
|
# sensor 1 = I/0 controller inlet
|
||||||
# sensor 2 = I/0 controller outlet
|
# sensor 2 = I/0 controller outlet
|
||||||
|
@ -68,7 +68,7 @@ class IOU(IModule):
|
|||||||
iou_config = config.get_section_config(name.upper())
|
iou_config = config.get_section_config(name.upper())
|
||||||
self._iouyap = iou_config.get("iouyap_path")
|
self._iouyap = iou_config.get("iouyap_path")
|
||||||
if not self._iouyap or not os.path.isfile(self._iouyap):
|
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
|
# look for iouyap in the current working directory and $PATH
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
@ -87,11 +87,11 @@ class IOU(IModule):
|
|||||||
IModule.__init__(self, name, *args, **kwargs)
|
IModule.__init__(self, name, *args, **kwargs)
|
||||||
self._iou_instances = {}
|
self._iou_instances = {}
|
||||||
self._console_start_port_range = iou_config.get("console_start_port_range", 4001)
|
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._allocated_udp_ports = []
|
||||||
self._udp_start_port_range = iou_config.get("udp_start_port_range", 30001)
|
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._udp_end_port_range = iou_config.get("udp_end_port_range", 35000)
|
||||||
self._host = kwargs["host"]
|
self._host = iou_config.get("host", kwargs["host"])
|
||||||
self._projects_dir = kwargs["projects_dir"]
|
self._projects_dir = kwargs["projects_dir"]
|
||||||
self._tempdir = kwargs["temp_dir"]
|
self._tempdir = kwargs["temp_dir"]
|
||||||
self._working_dir = self._projects_dir
|
self._working_dir = self._projects_dir
|
||||||
|
@ -22,7 +22,7 @@ Base interface for NIOs.
|
|||||||
|
|
||||||
class NIO(object):
|
class NIO(object):
|
||||||
"""
|
"""
|
||||||
IOU NIO.
|
Network Input/Output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
|||||||
|
|
||||||
class NIO_GenericEthernet(NIO):
|
class NIO_GenericEthernet(NIO):
|
||||||
"""
|
"""
|
||||||
NIO generic Ethernet NIO.
|
Generic Ethernet NIO.
|
||||||
|
|
||||||
:param ethernet_device: Ethernet device name (e.g. eth0)
|
:param ethernet_device: Ethernet device name (e.g. eth0)
|
||||||
"""
|
"""
|
||||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
|||||||
|
|
||||||
class NIO_TAP(NIO):
|
class NIO_TAP(NIO):
|
||||||
"""
|
"""
|
||||||
IOU TAP NIO.
|
TAP NIO.
|
||||||
|
|
||||||
:param tap_device: TAP device name (e.g. tap0)
|
:param tap_device: TAP device name (e.g. tap0)
|
||||||
"""
|
"""
|
||||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
|||||||
|
|
||||||
class NIO_UDP(NIO):
|
class NIO_UDP(NIO):
|
||||||
"""
|
"""
|
||||||
IOU UDP NIO.
|
UDP NIO.
|
||||||
|
|
||||||
:param lport: local port number
|
:param lport: local port number
|
||||||
:param rhost: remote address/host
|
: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 socket
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from pkg_resources import parse_version
|
||||||
from gns3server.modules import IModule
|
from gns3server.modules import IModule
|
||||||
from gns3server.config import Config
|
from gns3server.config import Config
|
||||||
from .virtualbox_vm import VirtualBoxVM
|
from .virtualbox_vm import VirtualBoxVM
|
||||||
@ -60,25 +61,26 @@ class VirtualBox(IModule):
|
|||||||
|
|
||||||
def __init__(self, name, *args, **kwargs):
|
def __init__(self, name, *args, **kwargs):
|
||||||
|
|
||||||
# get the vboxwrapper location
|
# get the vboxwrapper location (only non-Windows platforms)
|
||||||
config = Config.instance()
|
if not sys.platform.startswith("win"):
|
||||||
vbox_config = config.get_section_config(name.upper())
|
config = Config.instance()
|
||||||
self._vboxwrapper_path = vbox_config.get("vboxwrapper_path")
|
vbox_config = config.get_section_config(name.upper())
|
||||||
if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path):
|
self._vboxwrapper_path = vbox_config.get("vboxwrapper_path")
|
||||||
paths = [os.getcwd()] + os.environ["PATH"].split(":")
|
if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path):
|
||||||
# look for iouyap in the current working directory and $PATH
|
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||||
for path in paths:
|
# look for vboxwrapper in the current working directory and $PATH
|
||||||
try:
|
for path in paths:
|
||||||
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
|
try:
|
||||||
self._vboxwrapper_path = os.path.join(path, "vboxwrapper")
|
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
|
||||||
break
|
self._vboxwrapper_path = os.path.join(path, "vboxwrapper")
|
||||||
except OSError:
|
break
|
||||||
continue
|
except OSError:
|
||||||
|
continue
|
||||||
|
|
||||||
if not self._vboxwrapper_path:
|
if not self._vboxwrapper_path:
|
||||||
log.warning("vboxwrapper couldn't be found!")
|
log.warning("vboxwrapper couldn't be found!")
|
||||||
elif not os.access(self._vboxwrapper_path, os.X_OK):
|
elif not os.access(self._vboxwrapper_path, os.X_OK):
|
||||||
log.warning("vboxwrapper is not executable")
|
log.warning("vboxwrapper is not executable")
|
||||||
|
|
||||||
# a new process start when calling IModule
|
# a new process start when calling IModule
|
||||||
IModule.__init__(self, name, *args, **kwargs)
|
IModule.__init__(self, name, *args, **kwargs)
|
||||||
@ -91,7 +93,7 @@ class VirtualBox(IModule):
|
|||||||
self._allocated_udp_ports = []
|
self._allocated_udp_ports = []
|
||||||
self._udp_start_port_range = vbox_config.get("udp_start_port_range", 35001)
|
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._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._projects_dir = kwargs["projects_dir"]
|
||||||
self._tempdir = kwargs["temp_dir"]
|
self._tempdir = kwargs["temp_dir"]
|
||||||
self._working_dir = self._projects_dir
|
self._working_dir = self._projects_dir
|
||||||
@ -105,18 +107,30 @@ class VirtualBox(IModule):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
|
import pywintypes
|
||||||
import win32com.client
|
import win32com.client
|
||||||
if win32com.client.gencache.is_readonly is True:
|
|
||||||
# dynamically generate the cache
|
try:
|
||||||
# http://www.py2exe.org/index.cgi/IncludingTypelibs
|
if win32com.client.gencache.is_readonly is True:
|
||||||
# http://www.py2exe.org/index.cgi/UsingEnsureDispatch
|
# dynamically generate the cache
|
||||||
win32com.client.gencache.is_readonly = False
|
# http://www.py2exe.org/index.cgi/IncludingTypelibs
|
||||||
#win32com.client.gencache.Rebuild()
|
# http://www.py2exe.org/index.cgi/UsingEnsureDispatch
|
||||||
win32com.client.gencache.GetGeneratePath()
|
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:
|
try:
|
||||||
from .vboxapi_py3 import VirtualBoxManager
|
from .vboxapi_py3 import VirtualBoxManager
|
||||||
self._vboxmanager = VirtualBoxManager(None, None)
|
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:
|
except Exception as e:
|
||||||
|
self._vboxmanager = None
|
||||||
raise VirtualBoxError("Could not initialize the VirtualBox Manager: {}".format(e))
|
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,
|
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 = VboxWrapperClient(self._vboxwrapper_path, self._tempdir, "127.0.0.1")
|
||||||
#self._vboxwrapper.connect()
|
#self._vboxwrapper.connect()
|
||||||
self._vboxwrapper.start()
|
try:
|
||||||
|
self._vboxwrapper.start()
|
||||||
|
except VirtualBoxError:
|
||||||
|
self._vboxwrapper = None
|
||||||
|
raise
|
||||||
|
|
||||||
def stop(self, signum=None):
|
def stop(self, signum=None):
|
||||||
"""
|
"""
|
||||||
@ -154,9 +172,9 @@ class VirtualBox(IModule):
|
|||||||
"""
|
"""
|
||||||
Returns a VirtualBox VM instance.
|
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:
|
if vbox_id not in self._vbox_instances:
|
||||||
@ -253,6 +271,7 @@ class VirtualBox(IModule):
|
|||||||
|
|
||||||
Mandatory request parameters:
|
Mandatory request parameters:
|
||||||
- name (VirtualBox VM name)
|
- name (VirtualBox VM name)
|
||||||
|
- vmname (VirtualBox VM name in VirtualBox)
|
||||||
|
|
||||||
Optional request parameters:
|
Optional request parameters:
|
||||||
- console (VirtualBox VM console port)
|
- console (VirtualBox VM console port)
|
||||||
@ -399,7 +418,10 @@ class VirtualBox(IModule):
|
|||||||
try:
|
try:
|
||||||
vbox_instance.start()
|
vbox_instance.start()
|
||||||
except VirtualBoxError as e:
|
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
|
return
|
||||||
self.send_response(True)
|
self.send_response(True)
|
||||||
|
|
||||||
@ -632,7 +654,7 @@ class VirtualBox(IModule):
|
|||||||
Deletes an NIO (Network Input/Output).
|
Deletes an NIO (Network Input/Output).
|
||||||
|
|
||||||
Mandatory request parameters:
|
Mandatory request parameters:
|
||||||
- id (VPCS instance identifier)
|
- id (VirtualBox instance identifier)
|
||||||
- port (port identifier)
|
- port (port identifier)
|
||||||
|
|
||||||
Response parameters:
|
Response parameters:
|
||||||
@ -667,7 +689,7 @@ class VirtualBox(IModule):
|
|||||||
Starts a packet capture.
|
Starts a packet capture.
|
||||||
|
|
||||||
Mandatory request parameters:
|
Mandatory request parameters:
|
||||||
- id (vm identifier)
|
- id (VirtualBox VM identifier)
|
||||||
- port (port number)
|
- port (port number)
|
||||||
- port_id (port identifier)
|
- port_id (port identifier)
|
||||||
- capture_file_name
|
- capture_file_name
|
||||||
@ -708,7 +730,7 @@ class VirtualBox(IModule):
|
|||||||
Stops a packet capture.
|
Stops a packet capture.
|
||||||
|
|
||||||
Mandatory request parameters:
|
Mandatory request parameters:
|
||||||
- id (vm identifier)
|
- id (VirtualBox VM identifier)
|
||||||
- port (port number)
|
- port (port number)
|
||||||
- port_id (port identifier)
|
- port_id (port identifier)
|
||||||
|
|
||||||
@ -748,7 +770,11 @@ class VirtualBox(IModule):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._vboxwrapper and not self._vboxmanager:
|
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:
|
if self._vboxwrapper:
|
||||||
vms = self._vboxwrapper.get_vm_list()
|
vms = self._vboxwrapper.get_vm_list()
|
||||||
|
@ -22,7 +22,7 @@ Base interface for NIOs.
|
|||||||
|
|
||||||
class NIO(object):
|
class NIO(object):
|
||||||
"""
|
"""
|
||||||
IOU NIO.
|
Network Input/Output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -24,7 +24,7 @@ from .nio import NIO
|
|||||||
|
|
||||||
class NIO_UDP(NIO):
|
class NIO_UDP(NIO):
|
||||||
"""
|
"""
|
||||||
IOU UDP NIO.
|
UDP NIO.
|
||||||
|
|
||||||
:param lport: local port number
|
:param lport: local port number
|
||||||
:param rhost: remote address/host
|
:param rhost: remote address/host
|
||||||
|
@ -82,9 +82,15 @@ VBOX_UPDATE_SCHEMA = {
|
|||||||
"adapters": {
|
"adapters": {
|
||||||
"description": "number of adapters",
|
"description": "number of adapters",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 0,
|
"minimum": 1,
|
||||||
"maximum": 36, # maximum given by the ICH9 chipset in VirtualBox
|
"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": {
|
"adapter_type": {
|
||||||
"description": "VirtualBox adapter type",
|
"description": "VirtualBox adapter type",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -96,6 +102,10 @@ VBOX_UPDATE_SCHEMA = {
|
|||||||
"maximum": 65535,
|
"maximum": 65535,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"enable_console": {
|
||||||
|
"description": "enable the console",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"headless": {
|
"headless": {
|
||||||
"description": "headless mode",
|
"description": "headless mode",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
@ -26,6 +26,7 @@ import tempfile
|
|||||||
import socket
|
import socket
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from pkg_resources import parse_version
|
||||||
from ..attic import wait_socket_is_ready
|
from ..attic import wait_socket_is_ready
|
||||||
from .virtualbox_error import VirtualBoxError
|
from .virtualbox_error import VirtualBoxError
|
||||||
|
|
||||||
@ -53,7 +54,7 @@ class VboxWrapperClient(object):
|
|||||||
self._command = []
|
self._command = []
|
||||||
self._process = None
|
self._process = None
|
||||||
self._working_dir = working_dir
|
self._working_dir = working_dir
|
||||||
self._stdout_file = ""
|
self._stderr_file = ""
|
||||||
self._started = False
|
self._started = False
|
||||||
self._host = host
|
self._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
@ -139,19 +140,31 @@ class VboxWrapperClient(object):
|
|||||||
try:
|
try:
|
||||||
log.info("starting VirtualBox wrapper: {}".format(self._command))
|
log.info("starting VirtualBox wrapper: {}".format(self._command))
|
||||||
with tempfile.NamedTemporaryFile(delete=False) as fd:
|
with tempfile.NamedTemporaryFile(delete=False) as fd:
|
||||||
self._stdout_file = fd.name
|
with open(os.devnull, "w") as null:
|
||||||
log.info("VirtualBox wrapper process logging to {}".format(fd.name))
|
self._stderr_file = fd.name
|
||||||
self._process = subprocess.Popen(self._command,
|
log.info("VirtualBox wrapper process logging to {}".format(fd.name))
|
||||||
stdout=fd,
|
self._process = subprocess.Popen(self._command,
|
||||||
stderr=subprocess.STDOUT,
|
stdout=null,
|
||||||
cwd=self._working_dir)
|
stderr=fd,
|
||||||
|
cwd=self._working_dir)
|
||||||
log.info("VirtualBox wrapper started PID={}".format(self._process.pid))
|
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.wait_for_vboxwrapper(self._host, self._port)
|
||||||
self.connect()
|
self.connect()
|
||||||
self._started = True
|
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:
|
except OSError as e:
|
||||||
log.error("could not start VirtualBox wrapper: {}".format(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):
|
def wait_for_vboxwrapper(self, host, port):
|
||||||
"""
|
"""
|
||||||
@ -198,26 +211,26 @@ class VboxWrapperClient(object):
|
|||||||
if self._process.poll() is None:
|
if self._process.poll() is None:
|
||||||
log.warn("VirtualBox wrapper process {} is still running".format(self._process.pid))
|
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:
|
try:
|
||||||
os.remove(self._stdout_file)
|
os.remove(self._stderr_file)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
log.warning("could not delete temporary VirtualBox wrapper log file: {}".format(e))
|
log.warning("could not delete temporary VirtualBox wrapper log file: {}".format(e))
|
||||||
self._started = False
|
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.
|
Only use when the process has been stopped or has crashed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output = ""
|
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:
|
try:
|
||||||
with open(self._stdout_file, errors="replace") as file:
|
with open(self._stderr_file, errors="replace") as file:
|
||||||
output = file.read()
|
output = file.read()
|
||||||
except OSError as e:
|
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
|
return output
|
||||||
|
|
||||||
def is_running(self):
|
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.
|
VirtualBox VM instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import socket
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .pipe_proxy import PipeProxy
|
|
||||||
from .virtualbox_error import VirtualBoxError
|
from .virtualbox_error import VirtualBoxError
|
||||||
|
from .virtualbox_controller import VirtualBoxController
|
||||||
from .adapters.ethernet_adapter import EthernetAdapter
|
from .adapters.ethernet_adapter import EthernetAdapter
|
||||||
from ..attic import find_unused_port
|
from ..attic import find_unused_port
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
import msvcrt
|
|
||||||
import win32file
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -97,21 +87,13 @@ class VirtualBoxVM(object):
|
|||||||
self._console_start_port_range = console_start_port_range
|
self._console_start_port_range = console_start_port_range
|
||||||
self._console_end_port_range = console_end_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
|
# VirtualBox settings
|
||||||
self._console = console
|
self._console = console
|
||||||
self._ethernet_adapters = []
|
self._ethernet_adapters = []
|
||||||
self._headless = False
|
self._headless = False
|
||||||
|
self._enable_console = True
|
||||||
self._vmname = vmname
|
self._vmname = vmname
|
||||||
|
self._adapter_start_index = 0
|
||||||
self._adapter_type = "Automatic"
|
self._adapter_type = "Automatic"
|
||||||
|
|
||||||
working_dir_path = os.path.join(working_dir, "vbox", "vm-{}".format(self._id))
|
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 create vbox "{}"'.format(self._name))
|
||||||
self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname))
|
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 {}'.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:
|
else:
|
||||||
try:
|
self._vboxcontroller = VirtualBoxController(self._vmname, vboxmanager, self._host)
|
||||||
self._machine = self._vboxmanager.vbox.findMachine(self._vmname)
|
self._vboxcontroller.console = self._console
|
||||||
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.adapters = 2 # creates 2 adapters by default
|
||||||
log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name,
|
log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name,
|
||||||
id=self._id))
|
id=self._id))
|
||||||
|
|
||||||
@ -165,9 +139,11 @@ class VirtualBoxVM(object):
|
|||||||
|
|
||||||
vbox_defaults = {"name": self._name,
|
vbox_defaults = {"name": self._name,
|
||||||
"vmname": self._vmname,
|
"vmname": self._vmname,
|
||||||
"adapters": len(self._ethernet_adapters),
|
"adapters": self.adapters,
|
||||||
|
"adapter_start_index": self._adapter_start_index,
|
||||||
"adapter_type": "Automatic",
|
"adapter_type": "Automatic",
|
||||||
"console": self._console,
|
"console": self._console,
|
||||||
|
"enable_console": self._enable_console,
|
||||||
"headless": self._headless}
|
"headless": self._headless}
|
||||||
|
|
||||||
return vbox_defaults
|
return vbox_defaults
|
||||||
@ -274,6 +250,8 @@ class VirtualBoxVM(object):
|
|||||||
|
|
||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox setattr "{}" console {}'.format(self._name, self._console))
|
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,
|
log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name,
|
||||||
id=self._id,
|
id=self._id,
|
||||||
@ -344,13 +322,49 @@ class VirtualBoxVM(object):
|
|||||||
if headless:
|
if headless:
|
||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox setattr "{}" headless_mode True'.format(self._name))
|
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))
|
log.info("VirtualBox VM {name} [id={id}] has enabled the headless mode".format(name=self._name, id=self._id))
|
||||||
else:
|
else:
|
||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox setattr "{}" headless_mode False'.format(self._name))
|
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))
|
log.info("VirtualBox VM {name} [id={id}] has disabled the headless mode".format(name=self._name, id=self._id))
|
||||||
self._headless = headless
|
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
|
@property
|
||||||
def vmname(self):
|
def vmname(self):
|
||||||
"""
|
"""
|
||||||
@ -372,13 +386,7 @@ class VirtualBoxVM(object):
|
|||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname))
|
self._vboxwrapper.send('vbox setattr "{}" image "{}"'.format(self._name, vmname))
|
||||||
else:
|
else:
|
||||||
try:
|
self._vboxcontroller.vmname = vmname
|
||||||
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)
|
|
||||||
|
|
||||||
log.info("VirtualBox VM {name} [id={id}] has set the VM name to {vmname}".format(name=self._name, id=self._id, 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
|
self._vmname = vmname
|
||||||
@ -402,15 +410,47 @@ class VirtualBoxVM(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self._ethernet_adapters.clear()
|
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())
|
self._ethernet_adapters.append(EthernetAdapter())
|
||||||
|
|
||||||
if self._vboxwrapper:
|
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,
|
log.info("VirtualBox VM {name} [id={id}]: number of Ethernet adapters changed to {adapters}".format(name=self._name,
|
||||||
id=self._id,
|
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
|
@property
|
||||||
def adapter_type(self):
|
def adapter_type(self):
|
||||||
@ -434,6 +474,8 @@ class VirtualBoxVM(object):
|
|||||||
|
|
||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox setattr "{}" netcard "{}"'.format(self._name, adapter_type))
|
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,
|
log.info("VirtualBox VM {name} [id={id}]: adapter type changed to {adapter_type}".format(name=self._name,
|
||||||
id=self._id,
|
id=self._id,
|
||||||
@ -445,54 +487,9 @@ class VirtualBoxVM(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self._vboxwrapper:
|
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))
|
self._vboxwrapper.send('vbox start "{}"'.format(self._name))
|
||||||
else:
|
else:
|
||||||
|
self._vboxcontroller.start()
|
||||||
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()
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
@ -500,48 +497,13 @@ class VirtualBoxVM(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if self._vboxwrapper:
|
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:
|
else:
|
||||||
|
self._vboxcontroller.stop()
|
||||||
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
|
|
||||||
|
|
||||||
def suspend(self):
|
def suspend(self):
|
||||||
"""
|
"""
|
||||||
@ -551,10 +513,7 @@ class VirtualBoxVM(object):
|
|||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox suspend "{}"'.format(self._name))
|
self._vboxwrapper.send('vbox suspend "{}"'.format(self._name))
|
||||||
else:
|
else:
|
||||||
try:
|
self._vboxcontroller.suspend()
|
||||||
self._session.console.pause()
|
|
||||||
except Exception as e:
|
|
||||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
"""
|
"""
|
||||||
@ -564,11 +523,7 @@ class VirtualBoxVM(object):
|
|||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox reset "{}"'.format(self._name))
|
self._vboxwrapper.send('vbox reset "{}"'.format(self._name))
|
||||||
else:
|
else:
|
||||||
try:
|
self._vboxcontroller.reload()
|
||||||
progress = self._session.console.reset()
|
|
||||||
progress.waitForCompletion(-1)
|
|
||||||
except Exception as e:
|
|
||||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
"""
|
"""
|
||||||
@ -578,10 +533,7 @@ class VirtualBoxVM(object):
|
|||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox resume "{}"'.format(self._name))
|
self._vboxwrapper.send('vbox resume "{}"'.format(self._name))
|
||||||
else:
|
else:
|
||||||
try:
|
self._vboxcontroller.resume()
|
||||||
self._session.console.resume()
|
|
||||||
except Exception as e:
|
|
||||||
raise VirtualBoxError("VirtualBox error: {}".format(e))
|
|
||||||
|
|
||||||
def port_add_nio_binding(self, adapter_id, nio):
|
def port_add_nio_binding(self, adapter_id, nio):
|
||||||
"""
|
"""
|
||||||
@ -599,12 +551,12 @@ class VirtualBoxVM(object):
|
|||||||
|
|
||||||
if self._vboxwrapper:
|
if self._vboxwrapper:
|
||||||
self._vboxwrapper.send('vbox create_udp "{}" {} {} {} {}'.format(self._name,
|
self._vboxwrapper.send('vbox create_udp "{}" {} {} {} {}'.format(self._name,
|
||||||
adapter_id,
|
adapter_id,
|
||||||
nio.lport,
|
nio.lport,
|
||||||
nio.rhost,
|
nio.rhost,
|
||||||
nio.rport))
|
nio.rport))
|
||||||
else:
|
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)
|
adapter.add_nio(0, nio)
|
||||||
log.info("VirtualBox VM {name} [id={id}]: {nio} added to adapter {adapter_id}".format(name=self._name,
|
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,
|
self._vboxwrapper.send('vbox delete_udp "{}" {}'.format(self._name,
|
||||||
adapter_id))
|
adapter_id))
|
||||||
else:
|
else:
|
||||||
self._delete_udp(adapter_id)
|
self._vboxcontroller.delete_udp(adapter_id)
|
||||||
|
|
||||||
nio = adapter.get_nio(0)
|
nio = adapter.get_nio(0)
|
||||||
adapter.remove_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,
|
log.info("VirtualBox VM {name} [id={id}]: stopping packet capture on adapter {adapter_id}".format(name=self._name,
|
||||||
id=self._id,
|
id=self._id,
|
||||||
adapter_id=adapter_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())
|
vpcs_config = config.get_section_config(name.upper())
|
||||||
self._vpcs = vpcs_config.get("vpcs_path")
|
self._vpcs = vpcs_config.get("vpcs_path")
|
||||||
if not self._vpcs or not os.path.isfile(self._vpcs):
|
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
|
# look for VPCS in the current working directory and $PATH
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
@ -81,12 +81,12 @@ class VPCS(IModule):
|
|||||||
# a new process start when calling IModule
|
# a new process start when calling IModule
|
||||||
IModule.__init__(self, name, *args, **kwargs)
|
IModule.__init__(self, name, *args, **kwargs)
|
||||||
self._vpcs_instances = {}
|
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._console_end_port_range = vpcs_config.get("console_end_port_range", 5000)
|
||||||
self._allocated_udp_ports = []
|
self._allocated_udp_ports = []
|
||||||
self._udp_start_port_range = vpcs_config.get("udp_start_port_range", 40001)
|
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", 40512)
|
self._udp_end_port_range = vpcs_config.get("udp_end_port_range", 21000)
|
||||||
self._host = kwargs["host"]
|
self._host = vpcs_config.get("host", kwargs["host"])
|
||||||
self._projects_dir = kwargs["projects_dir"]
|
self._projects_dir = kwargs["projects_dir"]
|
||||||
self._tempdir = kwargs["temp_dir"]
|
self._tempdir = kwargs["temp_dir"]
|
||||||
self._working_dir = self._projects_dir
|
self._working_dir = self._projects_dir
|
||||||
|
@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only).
|
|||||||
|
|
||||||
class NIO_TAP(object):
|
class NIO_TAP(object):
|
||||||
"""
|
"""
|
||||||
IOU TAP NIO.
|
TAP NIO.
|
||||||
|
|
||||||
:param tap_device: TAP device name (e.g. tap0)
|
:param tap_device: TAP device name (e.g. tap0)
|
||||||
"""
|
"""
|
||||||
|
@ -22,7 +22,7 @@ Interface for UDP NIOs.
|
|||||||
|
|
||||||
class NIO_UDP(object):
|
class NIO_UDP(object):
|
||||||
"""
|
"""
|
||||||
IOU UDP NIO.
|
UDP NIO.
|
||||||
|
|
||||||
:param lport: local port number
|
:param lport: local port number
|
||||||
:param rhost: remote address/host
|
:param rhost: remote address/host
|
||||||
|
@ -338,11 +338,10 @@ class VPCSDevice(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
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"))
|
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
|
||||||
if match:
|
if match:
|
||||||
version = match.group(1)
|
version = match.group(1)
|
||||||
print(version)
|
|
||||||
if parse_version(version) < parse_version("0.5b1"):
|
if parse_version(version) < parse_version("0.5b1"):
|
||||||
raise VPCSError("VPCS executable version must be >= 0.5b1")
|
raise VPCSError("VPCS executable version must be >= 0.5b1")
|
||||||
else:
|
else:
|
||||||
|
@ -23,5 +23,5 @@
|
|||||||
# or negative for a release candidate or beta (after the base version
|
# or negative for a release candidate or beta (after the base version
|
||||||
# number has been incremented)
|
# number has been incremented)
|
||||||
|
|
||||||
__version__ = "1.0beta1"
|
__version__ = "1.0beta4.dev1"
|
||||||
__version_info__ = (1, 0, 0, -99)
|
__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
|
from setuptools.command.test import test as TestCommand
|
||||||
|
|
||||||
|
|
||||||
class Tox(TestCommand):
|
class PyTest(TestCommand):
|
||||||
|
|
||||||
def finalize_options(self):
|
def finalize_options(self):
|
||||||
TestCommand.finalize_options(self)
|
TestCommand.finalize_options(self)
|
||||||
@ -29,8 +29,8 @@ class Tox(TestCommand):
|
|||||||
|
|
||||||
def run_tests(self):
|
def run_tests(self):
|
||||||
#import here, cause outside the eggs aren't loaded
|
#import here, cause outside the eggs aren't loaded
|
||||||
import tox
|
import pytest
|
||||||
errcode = tox.cmdline(self.test_args)
|
errcode = pytest.main(self.test_args)
|
||||||
sys.exit(errcode)
|
sys.exit(errcode)
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
@ -38,8 +38,8 @@ setup(
|
|||||||
version=__import__("gns3server").__version__,
|
version=__import__("gns3server").__version__,
|
||||||
url="http://github.com/GNS3/gns3-server",
|
url="http://github.com/GNS3/gns3-server",
|
||||||
license="GNU General Public License v3 (GPLv3)",
|
license="GNU General Public License v3 (GPLv3)",
|
||||||
tests_require=["tox"],
|
tests_require=["pytest"],
|
||||||
cmdclass={"test": Tox},
|
cmdclass={"test": PyTest},
|
||||||
author="Jeremy Grossmann",
|
author="Jeremy Grossmann",
|
||||||
author_email="package-maintainer@gns3.net",
|
author_email="package-maintainer@gns3.net",
|
||||||
description="GNS3 server to asynchronously manage emulators",
|
description="GNS3 server to asynchronously manage emulators",
|
||||||
@ -60,7 +60,7 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
platforms="any",
|
platforms="any",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 4 - Beta",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: Information Technology",
|
"Intended Audience :: Information Technology",
|
||||||
"Topic :: System :: Networking",
|
"Topic :: System :: Networking",
|
||||||
|
@ -6,10 +6,9 @@ import os
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def hypervisor(request):
|
def hypervisor(request):
|
||||||
|
|
||||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
dynamips_path = '/usr/bin/dynamips'
|
||||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
|
||||||
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
|
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()
|
hypervisor = manager.start_new_hypervisor()
|
||||||
|
|
||||||
def stop():
|
def stop():
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from gns3server.modules.dynamips import Hypervisor
|
from gns3server.modules.dynamips import Hypervisor
|
||||||
import time
|
import time
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_started(hypervisor):
|
def test_is_started(hypervisor):
|
||||||
@ -10,7 +9,7 @@ def test_is_started(hypervisor):
|
|||||||
|
|
||||||
def test_port(hypervisor):
|
def test_port(hypervisor):
|
||||||
|
|
||||||
assert hypervisor.port == 9000
|
assert hypervisor.port == 7200
|
||||||
|
|
||||||
|
|
||||||
def test_host(hypervisor):
|
def test_host(hypervisor):
|
||||||
@ -25,8 +24,7 @@ def test_working_dir(hypervisor):
|
|||||||
|
|
||||||
def test_path(hypervisor):
|
def test_path(hypervisor):
|
||||||
|
|
||||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
dynamips_path = '/usr/bin/dynamips'
|
||||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
|
||||||
assert hypervisor.path == dynamips_path
|
assert hypervisor.path == dynamips_path
|
||||||
|
|
||||||
|
|
||||||
@ -34,11 +32,10 @@ def test_stdout():
|
|||||||
|
|
||||||
# try to launch Dynamips on the same port
|
# try to launch Dynamips on the same port
|
||||||
# this will fail so that we can read its stdout/stderr
|
# this will fail so that we can read its stdout/stderr
|
||||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
dynamips_path = '/usr/bin/dynamips'
|
||||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
hypervisor = Hypervisor(dynamips_path, "/tmp", "127.0.0.1", 7200)
|
||||||
hypervisor = Hypervisor(dynamips_path, "/tmp", "172.0.0.1", 7200)
|
|
||||||
hypervisor.start()
|
hypervisor.start()
|
||||||
# give some time for Dynamips to start
|
# give some time for Dynamips to start
|
||||||
time.sleep(0.01)
|
time.sleep(0.1)
|
||||||
output = hypervisor.read_stdout()
|
output = hypervisor.read_stdout()
|
||||||
assert output
|
assert output
|
||||||
|
@ -7,10 +7,9 @@ import os
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def hypervisor_manager(request):
|
def hypervisor_manager(request):
|
||||||
|
|
||||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
dynamips_path = '/usr/bin/dynamips'
|
||||||
dynamips_path = os.path.join(cwd, "dynamips.stable")
|
|
||||||
print("\nStarting Dynamips Hypervisor: {}".format(dynamips_path))
|
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()
|
#manager.start_new_hypervisor()
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import base64
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def router(request, hypervisor):
|
def router(request, hypervisor):
|
||||||
|
|
||||||
router = Router(hypervisor, "router", "c3725")
|
router = Router(hypervisor, "router", platform="c3725")
|
||||||
request.addfinalizer(router.delete)
|
request.addfinalizer(router.delete)
|
||||||
return router
|
return router
|
||||||
|
|
||||||
@ -127,9 +127,9 @@ def test_idlepc(router):
|
|||||||
|
|
||||||
def test_idlemax(router):
|
def test_idlemax(router):
|
||||||
|
|
||||||
assert router.idlemax == 1500 # default value
|
assert router.idlemax == 500 # default value
|
||||||
router.idlemax = 500
|
router.idlemax = 1500
|
||||||
assert router.idlemax == 500
|
assert router.idlemax == 1500
|
||||||
|
|
||||||
|
|
||||||
def test_idlesleep(router):
|
def test_idlesleep(router):
|
||||||
@ -172,7 +172,7 @@ def test_confreg(router):
|
|||||||
|
|
||||||
def test_console(router):
|
def test_console(router):
|
||||||
|
|
||||||
assert router.console == router.hypervisor.baseconsole + router.id
|
assert router.console == 2001
|
||||||
new_console_port = router.console + 100
|
new_console_port = router.console + 100
|
||||||
router.console = new_console_port
|
router.console = new_console_port
|
||||||
assert router.console == new_console_port
|
assert router.console == new_console_port
|
||||||
@ -180,7 +180,7 @@ def test_console(router):
|
|||||||
|
|
||||||
def test_aux(router):
|
def test_aux(router):
|
||||||
|
|
||||||
assert router.aux == router.hypervisor.baseaux + router.id
|
assert router.aux == 2501
|
||||||
new_aux_port = router.aux + 100
|
new_aux_port = router.aux + 100
|
||||||
router.aux = new_aux_port
|
router.aux = new_aux_port
|
||||||
assert router.aux == new_aux_port
|
assert router.aux == new_aux_port
|
||||||
|
@ -3,17 +3,28 @@ import os
|
|||||||
import pytest
|
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")
|
@pytest.fixture(scope="session")
|
||||||
def iou(request):
|
def iou(request):
|
||||||
|
|
||||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||||
iou_path = os.path.join(cwd, "i86bi_linux-ipbase-ms-12.4.bin")
|
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()
|
iou_device.start()
|
||||||
request.addfinalizer(iou_device.delete)
|
request.addfinalizer(iou_device.delete)
|
||||||
return iou_device
|
return iou_device
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
|
||||||
def test_iou_is_started(iou):
|
def test_iou_is_started(iou):
|
||||||
|
|
||||||
print(iou.command())
|
print(iou.command())
|
||||||
@ -21,6 +32,7 @@ def test_iou_is_started(iou):
|
|||||||
assert iou.is_running()
|
assert iou.is_running()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(no_iou(), reason="IOU Image not available")
|
||||||
def test_iou_restart(iou):
|
def test_iou_restart(iou):
|
||||||
|
|
||||||
iou.stop()
|
iou.stop()
|
||||||
|
@ -6,9 +6,13 @@ import pytest
|
|||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def vpcs(request):
|
def vpcs(request):
|
||||||
|
|
||||||
cwd = os.path.dirname(os.path.abspath(__file__))
|
if os.path.isfile("/usr/bin/vpcs"):
|
||||||
vpcs_path = os.path.join(cwd, "vpcs")
|
vpcs_path = "/usr/bin/vpcs"
|
||||||
vpcs_device = VPCSDevice(vpcs_path, "/tmp")
|
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()
|
vpcs_device.start()
|
||||||
request.addfinalizer(vpcs_device.delete)
|
request.addfinalizer(vpcs_device.delete)
|
||||||
return vpcs_device
|
return vpcs_device
|
||||||
|
Loading…
Reference in New Issue
Block a user