Base QEMU support.

This commit is contained in:
grossmj 2014-09-18 15:47:43 -06:00
parent b483f87c2f
commit d1715baae1
21 changed files with 2005 additions and 17 deletions

View File

@ -20,8 +20,9 @@ from .base import IModule
from .dynamips import Dynamips
from .vpcs import VPCS
from .virtualbox import VirtualBox
from .qemu import Qemu
MODULES = [Dynamips, VPCS, VirtualBox]
MODULES = [Dynamips, VPCS, VirtualBox, Qemu]
if sys.platform.startswith("linux"):
# IOU runs only on Linux

View File

@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
IOU NIO.
Network Input/Output.
"""
def __init__(self):

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_GenericEthernet(NIO):
"""
NIO generic Ethernet NIO.
Generic Ethernet NIO.
:param ethernet_device: Ethernet device name (e.g. eth0)
"""

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_TAP(NIO):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -0,0 +1,684 @@
# -*- 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 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
QEMU_BINARIES = ["qemu.exe",
"qemu-system-arm",
"qemu-system-mips",
"qemu-system-ppc",
"qemu-system-sparc",
"qemu-system-x86",
"qemu-system-i386",
"qemu-system-x86_64"]
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):
# get the qemu-img location
config = Config.instance()
qemu_config = config.get_section_config(name.upper())
self._qemu_img_path = qemu_config.get("qemu_img_path")
if not self._qemu_img_path or not os.path.isfile(self._qemu_img_path):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
# look for qemu-img in the current working directory and $PATH
for path in paths:
try:
if "qemu-img" in os.listdir(path) and os.access(os.path.join(path, "qemu-img"), os.X_OK):
self._qemu_img_path = os.path.join(path, "qemu-img")
break
except OSError:
continue
if not self._qemu_img_path:
log.warning("qemu-img couldn't be found!")
elif not os.access(self._qemu_img_path, os.X_OK):
log.warning("qemu-img is not executable")
# 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 "qemu_img_path" in request and request["qemu_img_path"]:
self._qemu_img_path = request["qemu_img_path"]
log.info("QEMU image utility path set to {}".format(self._qemu_img_path))
for qemu_id in self._qemu_instances:
qemu_instance = self._qemu_instances[qemu_id]
qemu_instance.qemu_img_path = self._qemu_img_path
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._qemu_img_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
"""
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(":")
# look for Qemu binaries in the current working directory and $PATH
for path in paths:
for qemu_binary in QEMU_BINARIES:
try:
if qemu_binary in os.listdir(path) and os.access(os.path.join(path, qemu_binary), os.X_OK):
qemu_path = os.path.join(path, qemu_binary)
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)

View File

@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class Adapter(object):
"""
Base class for adapters.
:param interfaces: number of interfaces supported by this adapter.
"""
def __init__(self, interfaces=1):
self._interfaces = interfaces
self._ports = {}
for port_id in range(0, interfaces):
self._ports[port_id] = None
def removable(self):
"""
Returns True if the adapter can be removed from a slot
and False if not.
:returns: boolean
"""
return True
def port_exists(self, port_id):
"""
Checks if a port exists on this adapter.
:returns: True is the port exists,
False otherwise.
"""
if port_id in self._ports:
return True
return False
def add_nio(self, port_id, nio):
"""
Adds a NIO to a port on this adapter.
:param port_id: port ID (integer)
:param nio: NIO instance
"""
self._ports[port_id] = nio
def remove_nio(self, port_id):
"""
Removes a NIO from a port on this adapter.
:param port_id: port ID (integer)
"""
self._ports[port_id] = None
def get_nio(self, port_id):
"""
Returns the NIO assigned to a port.
:params port_id: port ID (integer)
:returns: NIO instance
"""
return self._ports[port_id]
@property
def ports(self):
"""
Returns port to NIO mapping
:returns: dictionary port -> NIO
"""
return self._ports
@property
def interfaces(self):
"""
Returns the number of interfaces supported by this adapter.
:returns: number of interfaces
"""
return self._interfaces

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .adapter import Adapter
class EthernetAdapter(Adapter):
"""
QEMU Ethernet adapter.
"""
def __init__(self):
Adapter.__init__(self, interfaces=1)
def __str__(self):
return "QEMU Ethernet adapter"

View File

View 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

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

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Custom exceptions for 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

View File

@ -0,0 +1,616 @@
# -*- 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
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 qemu_img_path: path to the QEMU IMG 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,
qemu_img_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._qemu_img_path = qemu_img_path
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._disk_image = ""
self._options = ""
self._ram = 256
self._console = console
self._ethernet_adapters = []
self._adapter_type = "e1000"
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,
"disk_image": self._disk_image,
"options": self._options,
"adapters": self.adapters,
"adapter_type": self._adapter_type,
"console": self._console}
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 qemu_img_path(self):
"""
Returns the QEMU IMG binary path for this QEMU VM.
:returns: QEMU IMG path
"""
return self._qemu_img_path
@qemu_img_path.setter
def qemu_img_path(self, qemu_img_path):
"""
Sets the QEMU IMG binary path this QEMU VM.
:param qemu_img_path: QEMU IMG path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU IMG path to {qemu_img_path}".format(name=self._name,
id=self._id,
qemu_img_path=qemu_img_path))
self._qemu_img_path = qemu_img_path
@property
def disk_image(self):
"""
Returns the disk image path for this QEMU VM.
:returns: QEMU disk image path
"""
return self._disk_image
@disk_image.setter
def disk_image(self, disk_image):
"""
Sets the disk image for this QEMU VM.
:param disk_image: QEMU disk image path
"""
log.info("QEMU VM {name} [id={id}] has set the QEMU disk image path to {disk_image}".format(name=self._name,
id=self._id,
disk_image=disk_image))
self._disk_image = 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))
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))
#TODO: check binary image is valid?
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):
hda_disk = os.path.join(self._working_dir, "hda.disk")
if not os.path.exists(hda_disk):
try:
retcode = subprocess.call([self._qemu_img_path, "create", "-o",
"backing_file={}".format(self._disk_image),
"-f", "qcow2", hda_disk])
log.info("{} returned with {}".format(self._qemu_img_path, retcode))
except OSError as e:
raise QemuError("Could not create disk image {}".format(e))
return ["-hda", hda_disk]
def _network_options(self):
network_options = []
adapter_id = 0
for adapter in self._ethernet_adapters:
nio = adapter.get_nio(0)
if nio:
#TODO: let users specific the 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)])
if 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(["-device", "{}".format(self._adapter_type)])
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._serial_options())
command.extend(self._network_options())
return command

View File

@ -0,0 +1,373 @@
# -*- 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,
},
"disk_image": {
"description": "QEMU 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"
},
"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"]
}

View File

@ -68,7 +68,7 @@ class VirtualBox(IModule):
self._vboxwrapper_path = vbox_config.get("vboxwrapper_path")
if not self._vboxwrapper_path or not os.path.isfile(self._vboxwrapper_path):
paths = [os.getcwd()] + os.environ["PATH"].split(":")
# look for iouyap in the current working directory and $PATH
# look for vboxwrapper in the current working directory and $PATH
for path in paths:
try:
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
@ -172,9 +172,9 @@ class VirtualBox(IModule):
"""
Returns a VirtualBox VM instance.
:param vbox_id: VirtualBox device identifier
:param vbox_id: VirtualBox VM identifier
:returns: VBoxDevice instance
:returns: VirtualBoxVM instance
"""
if vbox_id not in self._vbox_instances:
@ -271,6 +271,7 @@ class VirtualBox(IModule):
Mandatory request parameters:
- name (VirtualBox VM name)
- vmname (VirtualBox VM name in VirtualBox)
Optional request parameters:
- console (VirtualBox VM console port)
@ -653,7 +654,7 @@ class VirtualBox(IModule):
Deletes an NIO (Network Input/Output).
Mandatory request parameters:
- id (VPCS instance identifier)
- id (VirtualBox instance identifier)
- port (port identifier)
Response parameters:
@ -688,7 +689,7 @@ class VirtualBox(IModule):
Starts a packet capture.
Mandatory request parameters:
- id (vm identifier)
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)
- capture_file_name
@ -729,7 +730,7 @@ class VirtualBox(IModule):
Stops a packet capture.
Mandatory request parameters:
- id (vm identifier)
- id (VirtualBox VM identifier)
- port (port number)
- port_id (port identifier)

View File

@ -22,7 +22,7 @@ Base interface for NIOs.
class NIO(object):
"""
IOU NIO.
Network Input/Output.
"""
def __init__(self):

View File

@ -24,7 +24,7 @@ from .nio import NIO
class NIO_UDP(NIO):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -22,7 +22,7 @@ Interface for TAP NIOs (UNIX based OSes only).
class NIO_TAP(object):
"""
IOU TAP NIO.
TAP NIO.
:param tap_device: TAP device name (e.g. tap0)
"""

View File

@ -22,7 +22,7 @@ Interface for UDP NIOs.
class NIO_UDP(object):
"""
IOU UDP NIO.
UDP NIO.
:param lport: local port number
:param rhost: remote address/host

View File

@ -338,11 +338,10 @@ class VPCSDevice(object):
"""
try:
output = subprocess.check_output([self._path, "-v"], stderr=subprocess.STDOUT, cwd=self._working_dir)
output = subprocess.check_output([self._path, "-v"], cwd=self._working_dir)
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
if match:
version = match.group(1)
print(version)
if parse_version(version) < parse_version("0.5b1"):
raise VPCSError("VPCS executable version must be >= 0.5b1")
else: