mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-12 18:05:54 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdb23d6d3b | ||
|
|
5986849b67 | ||
|
|
76fe0cb316 | ||
|
|
4aeb451ffd | ||
|
|
7b001e4d36 | ||
|
|
b1b602db3b | ||
|
|
cf5af8a6f7 | ||
|
|
4814bc6c65 | ||
|
|
2655d426c2 | ||
|
|
f1bbacecd6 | ||
|
|
a8142a04f8 | ||
|
|
19425b1e9c | ||
|
|
1b066bef92 | ||
|
|
6bf7a6aa38 | ||
|
|
fc14deee1b | ||
|
|
270017d945 | ||
|
|
23a03a24de | ||
|
|
439e0df058 | ||
|
|
c94dc1ec07 | ||
|
|
2ccb30eeb5 | ||
|
|
389788cfdc | ||
|
|
26fc8236c9 | ||
|
|
4216c5d2ed | ||
|
|
afa4ba9b55 | ||
|
|
7980ae9ab1 | ||
|
|
3495035dbf | ||
|
|
a2d4c2427d | ||
|
|
0dae4b6930 | ||
|
|
947a732bfb | ||
|
|
d88c5648de | ||
|
|
94fbd3fac9 | ||
|
|
07eab6e766 | ||
|
|
a2833cf276 | ||
|
|
079715bc18 | ||
|
|
5a32d8a779 | ||
|
|
9f1705a4f1 | ||
|
|
02650fa490 | ||
|
|
fff3e1474f | ||
|
|
d9de1718b7 | ||
|
|
78891ae00e | ||
|
|
b344def887 | ||
|
|
a1bc815f63 | ||
|
|
668cc3f0a5 | ||
|
|
42a8c7147a | ||
|
|
a0fe9bb498 | ||
|
|
57f9d875ca | ||
|
|
a2e51ac090 | ||
|
|
887f9b298e | ||
|
|
d99047ce72 | ||
|
|
ad27fdf8b9 | ||
|
|
9df290f192 | ||
|
|
05aafb9538 | ||
|
|
da72a9501a | ||
|
|
a2dfeab315 | ||
|
|
16cad8426a | ||
|
|
0476f2932e | ||
|
|
91c0f05a4e | ||
|
|
74ee73581a | ||
|
|
a86bac4214 | ||
|
|
8abf22ef24 | ||
|
|
7cad25eb1a | ||
|
|
ecf4e91e55 | ||
|
|
ea67f4aeb9 | ||
|
|
c98bcedd39 | ||
|
|
528bb7a7c6 | ||
|
|
d31420b3e1 | ||
|
|
50d7a4f335 | ||
|
|
4216724d0b | ||
|
|
c03c66ec48 | ||
|
|
dfd18f9483 | ||
|
|
8636d3e337 | ||
|
|
c43b26d787 | ||
|
|
08f82e02a0 | ||
|
|
33bca1a85c | ||
|
|
4d50d00b3e | ||
|
|
21cc41fd16 | ||
|
|
f8d95291fa | ||
|
|
9fa873751d | ||
|
|
8c9758d16b | ||
|
|
0c5b753211 | ||
|
|
221a35baae | ||
|
|
5bb870dc0f | ||
|
|
76be91d544 |
No files matched your search
@@ -1,5 +1,60 @@
|
||||
# Change Log
|
||||
|
||||
## 1.3.8 27/07/2015
|
||||
|
||||
* Catch ProcessLookupError when updating iouyap config. Fixes #255.
|
||||
* Fixes IOS adapters and WICS cannot be removed. Fixes #282.
|
||||
* Makes sure the loop is running when closing the app.
|
||||
* Catch GeneratorExit exception. Fixes #231.
|
||||
* Fixes missing chipset info for VirtualBox VM. Fixes #254.
|
||||
* Fixes IOURC upload.
|
||||
* Restore images & projects tarballs
|
||||
* Allow users to backup projects and images.
|
||||
* Update gns3.conf.upstart.
|
||||
* Fix incorrect vboxmanage sudo command.
|
||||
* Backport from 1.4: option to drop nvram & disk files for IOS routers in order to save disk space.
|
||||
* Backport from 1.4: Remove timeout to wait for connections to finish.
|
||||
* Backport from 1.4: Fixes RuntimeError: Event loop is closed.
|
||||
* Backport from 1.4: Bind host on 0.0.0.0 when checking for a free UDP port.
|
||||
|
||||
## 1.3.7 22/06/2015
|
||||
|
||||
* Prevent install on Python 2
|
||||
|
||||
## 1.3.6 16/06/2015
|
||||
|
||||
* Fix an issue with 1.4dev compatibility
|
||||
|
||||
## 1.3.5 16/06/15
|
||||
|
||||
* Ignore invalid characters when reading the output of a process
|
||||
* Turn on / off authentication
|
||||
* Ensure no colored output on Windows
|
||||
* Do not stop saving IOS router configs when there is an exception while a project is committed.
|
||||
* Create a private config file if expected
|
||||
* Distribute our own version of netifaces working with python 3
|
||||
* Fix crash if a private config exist in IOS but no private config file
|
||||
* Basic Auth support
|
||||
* Fix crash when virtualbox list of VMS return an empty line
|
||||
|
||||
## 1.3.4 02/06/15
|
||||
|
||||
* Drop useless dependencie dateutil
|
||||
* Check if port or adapter is connected before starting/stopping a packet capture. Fixes #196.
|
||||
* Prevent users to add links to running Qemu VMs and start a capture on running VirtualBox VMs.
|
||||
* Fixes bug: couldn't set PCMCIA disk1 size for IOS routers.
|
||||
* Fix crash if you pass an invalid hostname
|
||||
* Catch VPCS kill errors
|
||||
* Raise a VirtualBox error if adapter doesn't exists
|
||||
* Ignore VirtualBox VM Name with a carriage return in name
|
||||
* Cleanup the temporary project after modules have been notified of the path change
|
||||
* Do not return error if we can't remove the old project directory
|
||||
* Catch encoding errors in windows logger
|
||||
* Use setter for the qemu_path (allow to pass only the binary name)
|
||||
* Fixes TAP connection when using VPCS.
|
||||
* Fix crash launching qemu on OSX from another location.
|
||||
* Adds NAT NIO in device schema validation so they can return an error that it is not supported.
|
||||
|
||||
## 1.3.3 14/05/15
|
||||
|
||||
* Check for empty iourc path.
|
||||
|
||||
@@ -6,4 +6,3 @@ pep8==1.5.7
|
||||
pytest-timeout
|
||||
pytest-capturelog
|
||||
pytest-cov
|
||||
python-coveralls
|
||||
@@ -0,0 +1,2 @@
|
||||
SET PYTHONPATH=.
|
||||
python.exe gns3server/main.py --debug --local
|
||||
@@ -20,6 +20,7 @@ import sys
|
||||
import struct
|
||||
import platform
|
||||
|
||||
|
||||
try:
|
||||
import raven
|
||||
RAVEN_AVAILABLE = True
|
||||
@@ -34,13 +35,23 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Dev build
|
||||
if __version__[4] != 0:
|
||||
import faulthandler
|
||||
|
||||
# Display a traceback in case of segfault crash. Usefull when frozen
|
||||
# Not enabled by default for security reason
|
||||
log.info("Enable catching segfault")
|
||||
faulthandler.enable()
|
||||
|
||||
|
||||
class CrashReport:
|
||||
|
||||
"""
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "sync+https://45147533567b4d529ca09c093758681f:12d8b456cdb34d23aba771325aa64ee6@app.getsentry.com/38482"
|
||||
DSN = "sync+https://c06a407cbe0d4036be7873b54ebfa0ef:d6129d3fe91a4ea0b96ecbcf99678b1b@app.getsentry.com/38482"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = os.path.join(os.getcwd(), "cacert.pem")
|
||||
if os.path.isfile(cacert):
|
||||
|
||||
@@ -84,9 +84,11 @@ class ProjectHandler:
|
||||
project.name = request.json.get("name", project.name)
|
||||
project_path = request.json.get("path", project.path)
|
||||
if project_path != project.path:
|
||||
old_path = project.path
|
||||
project.path = project_path
|
||||
for module in MODULES:
|
||||
yield from module.instance().project_moved(project)
|
||||
yield from project.clean_old_path(old_path)
|
||||
# Very important we need to remove temporary flag after moving the project
|
||||
project.temporary = request.json.get("temporary", project.temporary)
|
||||
response.json(project)
|
||||
|
||||
@@ -344,7 +344,7 @@ class VirtualBoxHandler:
|
||||
vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
|
||||
adapter_number = int(request.match_info["adapter_number"])
|
||||
pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"])
|
||||
vm.start_capture(adapter_number, pcap_file_path)
|
||||
yield from vm.start_capture(adapter_number, pcap_file_path)
|
||||
response.json({"pcap_file_path": pcap_file_path})
|
||||
|
||||
@Route.post(
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
import os
|
||||
import aiohttp
|
||||
import stat
|
||||
import io
|
||||
import tarfile
|
||||
import asyncio
|
||||
|
||||
from ..config import Config
|
||||
from ..web.route import Route
|
||||
@@ -59,29 +62,97 @@ class UploadHandler:
|
||||
response.redirect("/upload")
|
||||
return
|
||||
|
||||
if data["type"] not in ["IOU", "IOURC", "QEMU", "IOS"]:
|
||||
raise aiohttp.web.HTTPForbidden("You are not authorized to upload this kind of image {}".format(data["type"]))
|
||||
if data["type"] not in ["IOU", "IOURC", "QEMU", "IOS", "IMAGES", "PROJECTS"]:
|
||||
raise aiohttp.web.HTTPForbidden(text="You are not authorized to upload this kind of image {}".format(data["type"]))
|
||||
|
||||
if data["type"] == "IOURC":
|
||||
destination_dir = os.path.expanduser("~/")
|
||||
destination_path = os.path.join(destination_dir, ".iourc")
|
||||
else:
|
||||
destination_dir = os.path.join(UploadHandler.image_directory(), data["type"])
|
||||
destination_path = os.path.join(destination_dir, data["file"].filename)
|
||||
try:
|
||||
os.makedirs(destination_dir, exist_ok=True)
|
||||
with open(destination_path, "wb+") as f:
|
||||
chunk = data["file"].file.read()
|
||||
f.write(chunk)
|
||||
st = os.stat(destination_path)
|
||||
os.chmod(destination_path, st.st_mode | stat.S_IXUSR)
|
||||
if data["type"] == "IMAGES":
|
||||
UploadHandler._restore_directory(data["file"], UploadHandler.image_directory())
|
||||
elif data["type"] == "PROJECTS":
|
||||
UploadHandler._restore_directory(data["file"], UploadHandler.project_directory())
|
||||
else:
|
||||
if data["type"] == "IOURC":
|
||||
destination_dir = os.path.expanduser("~/")
|
||||
destination_path = os.path.join(destination_dir, ".iourc")
|
||||
else:
|
||||
destination_dir = os.path.join(UploadHandler.image_directory(), data["type"])
|
||||
destination_path = os.path.join(destination_dir, data["file"].filename)
|
||||
os.makedirs(destination_dir, exist_ok=True)
|
||||
with open(destination_path, "wb+") as f:
|
||||
chunk = data["file"].file.read()
|
||||
f.write(chunk)
|
||||
st = os.stat(destination_path)
|
||||
os.chmod(destination_path, st.st_mode | stat.S_IXUSR)
|
||||
except OSError as e:
|
||||
response.html("Could not upload file: {}".format(e))
|
||||
response.set_status(200)
|
||||
return
|
||||
response.redirect("/upload")
|
||||
|
||||
@classmethod
|
||||
@Route.get(
|
||||
r"/backup/images.tar",
|
||||
description="Backup GNS3 images",
|
||||
api_version=None
|
||||
)
|
||||
def backup_images(request, response):
|
||||
yield from UploadHandler._backup_directory(request, response, UploadHandler.image_directory())
|
||||
|
||||
@classmethod
|
||||
@Route.get(
|
||||
r"/backup/projects.tar",
|
||||
description="Backup GNS3 projects",
|
||||
api_version=None
|
||||
)
|
||||
def backup_images(request, response):
|
||||
yield from UploadHandler._backup_directory(request, response, UploadHandler.project_directory())
|
||||
|
||||
@staticmethod
|
||||
def _restore_directory(file, directory):
|
||||
"""
|
||||
Extract from HTTP stream the content of a tar
|
||||
"""
|
||||
destination_path = os.path.join(directory, "archive.tar")
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
with open(destination_path, "wb+") as f:
|
||||
chunk = file.file.read()
|
||||
f.write(chunk)
|
||||
t = tarfile.open(destination_path)
|
||||
t.extractall(directory)
|
||||
t.close()
|
||||
os.remove(destination_path)
|
||||
|
||||
@staticmethod
|
||||
@asyncio.coroutine
|
||||
def _backup_directory(request, response, directory):
|
||||
"""
|
||||
Return a tar archive from a directory
|
||||
"""
|
||||
response.content_type = 'application/x-gtar'
|
||||
response.set_status(200)
|
||||
response.enable_chunked_encoding()
|
||||
# Very important: do not send a content length otherwise QT close the connection but curl can consume the Feed
|
||||
response.content_length = None
|
||||
response.start(request)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
with tarfile.open('arch.tar', 'w', fileobj=buffer) as tar:
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for file in files:
|
||||
path = os.path.join(root, file)
|
||||
tar.add(os.path.join(root, file), arcname=os.path.relpath(path, directory))
|
||||
response.write(buffer.getvalue())
|
||||
yield from response.drain()
|
||||
buffer.truncate(0)
|
||||
buffer.seek(0)
|
||||
yield from response.write_eof()
|
||||
|
||||
@staticmethod
|
||||
def image_directory():
|
||||
server_config = Config.instance().get_section_config("Server")
|
||||
return os.path.expanduser(server_config.get("images_path", "~/GNS3/images"))
|
||||
|
||||
@staticmethod
|
||||
def project_directory():
|
||||
server_config = Config.instance().get_section_config("Server")
|
||||
return os.path.expanduser(server_config.get("projects_path", "~/GNS3/projects"))
|
||||
+7
-1
@@ -186,7 +186,13 @@ def main():
|
||||
Project.clean_project_directory()
|
||||
|
||||
CrashReport.instance()
|
||||
host = server_config["host"]
|
||||
|
||||
try:
|
||||
host = server_config["host"].encode("idna").decode()
|
||||
except UnicodeError:
|
||||
log.critical("Invalid hostname %s", server_config["host"])
|
||||
return
|
||||
|
||||
port = int(server_config["port"])
|
||||
server = Server.instance(host, port)
|
||||
try:
|
||||
|
||||
@@ -113,7 +113,7 @@ class BaseManager:
|
||||
for future in done:
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
except (Exception, GeneratorExit) as e:
|
||||
log.error("Could not close VM {}".format(e), exc_info=1)
|
||||
continue
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class Dynamips(BaseManager):
|
||||
for future in done:
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
except (Exception, GeneratorExit) as e:
|
||||
log.error("Could not stop device hypervisor {}".format(e), exc_info=1)
|
||||
continue
|
||||
|
||||
@@ -154,7 +154,7 @@ class Dynamips(BaseManager):
|
||||
for future in done:
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
except (Exception, GeneratorExit) as e:
|
||||
log.error("Could not delete device {}".format(e), exc_info=1)
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -171,8 +171,11 @@ class Dynamips(BaseManager):
|
||||
files = glob.glob(os.path.join(project_dir, "*.ghost"))
|
||||
files += glob.glob(os.path.join(project_dir, "*_lock"))
|
||||
files += glob.glob(os.path.join(project_dir, "ilt_*"))
|
||||
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_rommon_vars"))
|
||||
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_*_ssa"))
|
||||
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_rommon_vars"))
|
||||
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_ssa"))
|
||||
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_log.txt"))
|
||||
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_rom"))
|
||||
files += glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i[0-9]*_bootflash"))
|
||||
for file in files:
|
||||
try:
|
||||
log.debug("Deleting file {}".format(file))
|
||||
@@ -210,7 +213,11 @@ class Dynamips(BaseManager):
|
||||
# save the configs when the project is committed
|
||||
for vm in self._vms.copy().values():
|
||||
if vm.project.id == project.id:
|
||||
yield from vm.save_configs()
|
||||
try:
|
||||
yield from vm.save_configs()
|
||||
except DynamipsError as e:
|
||||
log.warning(e)
|
||||
continue
|
||||
|
||||
@property
|
||||
def dynamips_path(self):
|
||||
|
||||
@@ -298,6 +298,9 @@ class EthernetSwitch(Device):
|
||||
|
||||
nio = self._nios[port_number]
|
||||
|
||||
if not nio:
|
||||
raise DynamipsError("Port {} is not connected".format(port_number))
|
||||
|
||||
data_link_type = data_link_type.lower()
|
||||
if data_link_type.startswith("dlt_"):
|
||||
data_link_type = data_link_type[4:]
|
||||
@@ -324,6 +327,10 @@ class EthernetSwitch(Device):
|
||||
raise DynamipsError("Port {} is not allocated".format(port_number))
|
||||
|
||||
nio = self._nios[port_number]
|
||||
|
||||
if not nio:
|
||||
raise DynamipsError("Port {} is not connected".format(port_number))
|
||||
|
||||
yield from nio.unbind_filter("both")
|
||||
log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on port {port}'.format(name=self._name,
|
||||
id=self._id,
|
||||
|
||||
@@ -87,6 +87,7 @@ class Router(BaseVM):
|
||||
self._exec_area = 64 # 64 MB on other systems
|
||||
self._disk0 = 0 # Megabytes
|
||||
self._disk1 = 0 # Megabytes
|
||||
self._auto_delete_disks = False
|
||||
self._aux = aux
|
||||
self._mac_addr = ""
|
||||
self._system_id = "FTX0945W0MY" # processor board ID in IOS
|
||||
@@ -145,6 +146,7 @@ class Router(BaseVM):
|
||||
"exec_area": self._exec_area,
|
||||
"disk0": self._disk0,
|
||||
"disk1": self._disk1,
|
||||
"auto_delete_disks": self._auto_delete_disks,
|
||||
"console": self._console,
|
||||
"aux": self._aux,
|
||||
"mac_addr": self._mac_addr,
|
||||
@@ -158,7 +160,7 @@ class Router(BaseVM):
|
||||
for slot in self._slots:
|
||||
if slot:
|
||||
slot = str(slot)
|
||||
router_info["slot" + str(slot_number)] = slot
|
||||
router_info["slot" + str(slot_number)] = slot
|
||||
slot_number += 1
|
||||
|
||||
# add the wics
|
||||
@@ -166,6 +168,8 @@ class Router(BaseVM):
|
||||
for wic_slot_number in range(0, len(self._slots[0].wics)):
|
||||
if self._slots[0].wics[wic_slot_number]:
|
||||
router_info["wic" + str(wic_slot_number)] = str(self._slots[0].wics[wic_slot_number])
|
||||
else:
|
||||
router_info["wic" + str(wic_slot_number)] = None
|
||||
|
||||
return router_info
|
||||
|
||||
@@ -182,6 +186,16 @@ class Router(BaseVM):
|
||||
@asyncio.coroutine
|
||||
def create(self):
|
||||
|
||||
# delete any previous file with same Dynamips identifier
|
||||
project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower()))
|
||||
for file in glob.glob(os.path.join(project_dir, "c[0-9][0-9][0-9][0-9]_i{}_*".format(self._dynamips_id))):
|
||||
try:
|
||||
log.debug("Deleting file {}".format(file))
|
||||
yield from wait_run_in_executor(os.remove, file)
|
||||
except OSError as e:
|
||||
log.warn("Could not delete file {}: {}".format(file, e))
|
||||
continue
|
||||
|
||||
if not self._hypervisor:
|
||||
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
|
||||
self._hypervisor = yield from self.manager.start_new_hypervisor(working_dir=module_workdir)
|
||||
@@ -342,6 +356,20 @@ class Router(BaseVM):
|
||||
pass
|
||||
yield from self.hypervisor.stop()
|
||||
|
||||
if self._auto_delete_disks:
|
||||
# delete nvram and disk files
|
||||
project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower()))
|
||||
files = glob.glob(os.path.join(project_dir, "{}_i{}_disk[0-1]".format(self.platform, self.dynamips_id)))
|
||||
files += glob.glob(os.path.join(project_dir, "{}_i{}_slot[0-1]".format(self.platform, self.dynamips_id)))
|
||||
files += glob.glob(os.path.join(project_dir, "{}_i{}_nvram".format(self.platform, self.dynamips_id)))
|
||||
files += glob.glob(os.path.join(project_dir, "{}_i{}_flash[0-1]".format(self.platform, self.dynamips_id)))
|
||||
for file in files:
|
||||
try:
|
||||
log.debug("Deleting file {}".format(file))
|
||||
yield from wait_run_in_executor(os.remove, file)
|
||||
except OSError as e:
|
||||
log.warn("Could not delete file {}: {}".format(file, e))
|
||||
continue
|
||||
self._closed = True
|
||||
|
||||
@property
|
||||
@@ -836,7 +864,7 @@ class Router(BaseVM):
|
||||
return self._disk1
|
||||
|
||||
@asyncio.coroutine
|
||||
def disk1(self, disk1):
|
||||
def set_disk1(self, disk1):
|
||||
"""
|
||||
Sets the size (MB) for PCMCIA disk1.
|
||||
|
||||
@@ -851,6 +879,30 @@ class Router(BaseVM):
|
||||
new_disk1=disk1))
|
||||
self._disk1 = disk1
|
||||
|
||||
@property
|
||||
def auto_delete_disks(self):
|
||||
"""
|
||||
Returns True if auto delete disks is enabled on this router.
|
||||
|
||||
:returns: boolean either auto delete disks is activated or not
|
||||
"""
|
||||
|
||||
return self._auto_delete_disks
|
||||
|
||||
@asyncio.coroutine
|
||||
def set_auto_delete_disks(self, auto_delete_disks):
|
||||
"""
|
||||
Enable/disable use of auto delete disks
|
||||
|
||||
:param auto_delete_disks: activate/deactivate auto delete disks (boolean)
|
||||
"""
|
||||
|
||||
if auto_delete_disks:
|
||||
log.info('Router "{name}" [{id}]: auto delete disks enabled'.format(name=self._name, id=self._id))
|
||||
else:
|
||||
log.info('Router "{name}" [{id}]: auto delete disks disabled'.format(name=self._name, id=self._id))
|
||||
self._auto_delete_disks = auto_delete_disks
|
||||
|
||||
@asyncio.coroutine
|
||||
def set_console(self, console):
|
||||
"""
|
||||
@@ -1178,11 +1230,20 @@ class Router(BaseVM):
|
||||
if not adapter.port_exists(port_number):
|
||||
raise DynamipsError("Port {port_number} does not exist in adapter {adapter}".format(adapter=adapter,
|
||||
port_number=port_number))
|
||||
|
||||
yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name,
|
||||
slot_number=slot_number,
|
||||
port_number=port_number,
|
||||
nio=nio))
|
||||
try:
|
||||
yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name,
|
||||
slot_number=slot_number,
|
||||
port_number=port_number,
|
||||
nio=nio))
|
||||
except DynamipsError:
|
||||
# in case of error try to remove and add the nio binding
|
||||
yield from self._hypervisor.send('vm slot_remove_nio_binding "{name}" {slot_number} {port_number}'.format(name=self._name,
|
||||
slot_number=slot_number,
|
||||
port_number=port_number))
|
||||
yield from self._hypervisor.send('vm slot_add_nio_binding "{name}" {slot_number} {port_number} {nio}'.format(name=self._name,
|
||||
slot_number=slot_number,
|
||||
port_number=port_number,
|
||||
nio=nio))
|
||||
|
||||
log.info('Router "{name}" [{id}]: NIO {nio_name} bound to port {slot_number}/{port_number}'.format(name=self._name,
|
||||
id=self._id,
|
||||
@@ -1303,6 +1364,10 @@ class Router(BaseVM):
|
||||
|
||||
nio = adapter.get_nio(port_number)
|
||||
|
||||
if not nio:
|
||||
raise DynamipsError("Port {slot_number}/{port_number} is not connected".format(slot_number=slot_number,
|
||||
port_number=port_number))
|
||||
|
||||
if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
|
||||
raise DynamipsError("Port {port_number} has already a filter applied on {adapter}".format(adapter=adapter,
|
||||
port_number=port_number))
|
||||
@@ -1335,6 +1400,11 @@ class Router(BaseVM):
|
||||
port_number=port_number))
|
||||
|
||||
nio = adapter.get_nio(port_number)
|
||||
|
||||
if not nio:
|
||||
raise DynamipsError("Port {slot_number}/{port_number} is not connected".format(slot_number=slot_number,
|
||||
port_number=port_number))
|
||||
|
||||
yield from nio.unbind_filter("both")
|
||||
|
||||
log.info('Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format(name=self._name,
|
||||
@@ -1490,6 +1560,9 @@ class Router(BaseVM):
|
||||
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
|
||||
startup_config_base64, private_config_base64 = yield from self.extract_config()
|
||||
if startup_config_base64:
|
||||
if not self.startup_config:
|
||||
self._startup_config = os.path.join("configs", "i{}_startup-config.cfg".format(self._dynamips_id))
|
||||
|
||||
try:
|
||||
config = base64.b64decode(startup_config_base64).decode("utf-8", errors="replace")
|
||||
config = "!\n" + config.replace("\r", "")
|
||||
@@ -1501,6 +1574,9 @@ class Router(BaseVM):
|
||||
raise DynamipsError("Could not save the startup configuration {}: {}".format(config_path, e))
|
||||
|
||||
if private_config_base64:
|
||||
if not self.private_config:
|
||||
self._private_config = os.path.join("configs", "i{}_private-config.cfg".format(self._dynamips_id))
|
||||
|
||||
try:
|
||||
config = base64.b64decode(private_config_base64).decode("utf-8", errors="replace")
|
||||
config = "!\n" + config.replace("\r", "")
|
||||
|
||||
@@ -862,7 +862,10 @@ class IOUVM(BaseVM):
|
||||
port_number=port_number))
|
||||
if self.is_iouyap_running():
|
||||
self._update_iouyap_config()
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
try:
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
except ProcessLookupError:
|
||||
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
|
||||
|
||||
def adapter_remove_nio_binding(self, adapter_number, port_number):
|
||||
"""
|
||||
@@ -894,8 +897,10 @@ class IOUVM(BaseVM):
|
||||
port_number=port_number))
|
||||
if self.is_iouyap_running():
|
||||
self._update_iouyap_config()
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
|
||||
try:
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
except ProcessLookupError:
|
||||
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
|
||||
return nio
|
||||
|
||||
@property
|
||||
@@ -1052,7 +1057,10 @@ class IOUVM(BaseVM):
|
||||
|
||||
if self.is_iouyap_running():
|
||||
self._update_iouyap_config()
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
try:
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
except ProcessLookupError:
|
||||
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop_capture(self, adapter_number, port_number):
|
||||
@@ -1085,4 +1093,7 @@ class IOUVM(BaseVM):
|
||||
port_number=port_number))
|
||||
if self.is_iouyap_running():
|
||||
self._update_iouyap_config()
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
try:
|
||||
os.kill(self._iouyap_process.pid, signal.SIGHUP)
|
||||
except ProcessLookupError:
|
||||
log.error("Could not update iouyap configuration: process (PID={}) not found".format(self._iouyap_process.pid))
|
||||
@@ -34,8 +34,9 @@ class PortManager:
|
||||
def __init__(self, host="127.0.0.1"):
|
||||
|
||||
self._console_host = host
|
||||
self._udp_host = host
|
||||
|
||||
# UDP host must be 0.0.0.0, reason: https://github.com/GNS3/gns3-server/issues/265
|
||||
self._udp_host = "0.0.0.0"
|
||||
self._used_tcp_ports = set()
|
||||
self._used_udp_ports = set()
|
||||
|
||||
|
||||
@@ -146,13 +146,17 @@ class Project:
|
||||
self._path = path
|
||||
self._update_temporary_file()
|
||||
|
||||
# The order of operation is important because we want to avoid losing
|
||||
# data
|
||||
if old_path:
|
||||
@asyncio.coroutine
|
||||
def clean_old_path(self, old_path):
|
||||
"""
|
||||
Called after a project location change. All the modules should
|
||||
have been notified before
|
||||
"""
|
||||
if self._temporary:
|
||||
try:
|
||||
shutil.rmtree(old_path)
|
||||
yield from wait_run_in_executor(shutil.rmtree, old_path)
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPConflict(text="Can't remove temporary directory {}: {}".format(old_path, e))
|
||||
log.warn("Can't remove temporary directory {}: {}".format(old_path, e))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -359,7 +363,7 @@ class Project:
|
||||
for future in done:
|
||||
try:
|
||||
future.result()
|
||||
except Exception as e:
|
||||
except (Exception, GeneratorExit) as e:
|
||||
log.error("Could not close VM or device {}".format(e), exc_info=1)
|
||||
|
||||
if cleanup and os.path.exists(self.path):
|
||||
|
||||
@@ -47,13 +47,13 @@ class Qemu(BaseManager):
|
||||
"""
|
||||
|
||||
qemus = []
|
||||
paths = []
|
||||
paths = set()
|
||||
try:
|
||||
paths.append(os.getcwd())
|
||||
paths.add(os.getcwd())
|
||||
except FileNotFoundError:
|
||||
log.warning("The current working directory doesn't exist")
|
||||
if "PATH" in os.environ:
|
||||
paths.extend(os.environ["PATH"].split(os.pathsep))
|
||||
paths.update(os.environ["PATH"].split(os.pathsep))
|
||||
else:
|
||||
log.warning("The PATH environment variable doesn't exist")
|
||||
# look for Qemu binaries in the current working directory and $PATH
|
||||
@@ -64,17 +64,21 @@ class Qemu(BaseManager):
|
||||
exec_dir = os.path.dirname(os.path.abspath(sys.executable))
|
||||
for f in os.listdir(exec_dir):
|
||||
if f.lower().startswith("qemu"):
|
||||
paths.append(os.path.join(exec_dir, f))
|
||||
paths.add(os.path.join(exec_dir, f))
|
||||
|
||||
if "PROGRAMFILES(X86)" in os.environ and os.path.exists(os.environ["PROGRAMFILES(X86)"]):
|
||||
paths.append(os.path.join(os.environ["PROGRAMFILES(X86)"], "qemu"))
|
||||
paths.add(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"))
|
||||
paths.add(os.path.join(os.environ["PROGRAMFILES"], "qemu"))
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# add specific locations on Mac OS X regardless of what's in $PATH
|
||||
paths.extend(["/usr/local/bin", "/opt/local/bin"])
|
||||
paths.update(["/usr/local/bin", "/opt/local/bin"])
|
||||
if hasattr(sys, "frozen"):
|
||||
paths.append(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/")))
|
||||
try:
|
||||
paths.add(os.path.abspath(os.path.join(os.getcwd(), "../../../qemu/bin/")))
|
||||
# If the user run the server by hand from outside
|
||||
except FileNotFoundError:
|
||||
paths.add("/Applications/GNS3.app/Contents/Resources/qemu/bin")
|
||||
for path in paths:
|
||||
try:
|
||||
for f in os.listdir(path):
|
||||
|
||||
@@ -69,7 +69,7 @@ class QemuVM(BaseVM):
|
||||
self._stdout_file = ""
|
||||
|
||||
# QEMU VM settings
|
||||
self._qemu_path = qemu_path
|
||||
self.qemu_path = qemu_path
|
||||
self._hda_disk_image = ""
|
||||
self._hdb_disk_image = ""
|
||||
self._hdc_disk_image = ""
|
||||
@@ -741,6 +741,8 @@ class QemuVM(BaseVM):
|
||||
adapter_number=adapter_number))
|
||||
|
||||
if self.is_running():
|
||||
raise QemuError("Sorry, adding a link to a started Qemu VM is not supported.")
|
||||
# FIXME: does the code below work? very undocumented feature...
|
||||
# dynamically configure an UDP tunnel on the QEMU VM adapter
|
||||
if nio and isinstance(nio, NIOUDP):
|
||||
if self._legacy_networking:
|
||||
@@ -751,7 +753,6 @@ class QemuVM(BaseVM):
|
||||
nio.rport,
|
||||
nio.rhost))
|
||||
else:
|
||||
# FIXME: does it work? very undocumented feature...
|
||||
# Apparently there is a bug in Qemu...
|
||||
# netdev_add [user|tap|socket|hubport|netmap],id=str[,prop=value][,...] -- add host network device
|
||||
# netdev_del id -- remove host network device
|
||||
@@ -785,6 +786,7 @@ class QemuVM(BaseVM):
|
||||
adapter_number=adapter_number))
|
||||
|
||||
if self.is_running():
|
||||
# FIXME: does the code below work? very undocumented feature...
|
||||
# dynamically disable the QEMU VM adapter
|
||||
yield from self._control_vm("host_net_remove {} gns3-{}".format(adapter_number, adapter_number))
|
||||
yield from self._control_vm("host_net_add user vlan={},name=gns3-{}".format(adapter_number, adapter_number))
|
||||
|
||||
@@ -97,7 +97,7 @@ class VirtualBox(BaseManager):
|
||||
vbox_user = self.config.get_section_config("VirtualBox").get("vbox_user")
|
||||
if vbox_user:
|
||||
# TODO: test & review this part
|
||||
sudo_command = "sudo -i -u {}".format(vbox_user) + " ".join(command)
|
||||
sudo_command = "sudo -i -u {} ".format(vbox_user) + " ".join(command)
|
||||
process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
else:
|
||||
process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
||||
@@ -168,6 +168,8 @@ class VirtualBox(BaseManager):
|
||||
vms = []
|
||||
result = yield from self.execute("list", ["vms"])
|
||||
for line in result:
|
||||
if len(line) == 0 or line[0] != '"' or line[-1:] != "}":
|
||||
continue # Broken output (perhaps a carriage return in VM name
|
||||
vmname, _ = line.rsplit(' ', 1)
|
||||
vmname = vmname.strip('"')
|
||||
if vmname == "<inaccessible>":
|
||||
|
||||
@@ -574,10 +574,11 @@ class VirtualBoxVM(BaseVM):
|
||||
|
||||
# check the maximum number of adapters supported by the VM
|
||||
vm_info = yield from self._get_vm_info()
|
||||
chipset = vm_info["chipset"]
|
||||
maximum_adapters = 8
|
||||
if chipset == "ich9":
|
||||
maximum_adapters = int(self._system_properties["Maximum ICH9 Network Adapter count"])
|
||||
if "chipset" in vm_info:
|
||||
chipset = vm_info["chipset"]
|
||||
if chipset == "ich9":
|
||||
maximum_adapters = int(self._system_properties["Maximum ICH9 Network Adapter count"])
|
||||
return maximum_adapters
|
||||
|
||||
def _get_pipe_name(self):
|
||||
@@ -799,18 +800,22 @@ class VirtualBoxVM(BaseVM):
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
except KeyError:
|
||||
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
|
||||
vm_state = yield from self._get_vm_state()
|
||||
if vm_state == "running":
|
||||
# dynamically configure an UDP tunnel on the VirtualBox adapter
|
||||
yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_number + 1))
|
||||
yield from self._control_vm("nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
|
||||
yield from self._control_vm("nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
|
||||
yield from self._control_vm("nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
|
||||
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
|
||||
if isinstance(nio, NIOUDP):
|
||||
# dynamically configure an UDP tunnel on the VirtualBox adapter
|
||||
yield from self._control_vm("nic{} generic UDPTunnel".format(adapter_number + 1))
|
||||
yield from self._control_vm("nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
|
||||
yield from self._control_vm("nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
|
||||
yield from self._control_vm("nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
|
||||
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
|
||||
elif isinstance(nio, NIONAT):
|
||||
yield from self._control_vm("nic{} nat".format(adapter_number + 1))
|
||||
yield from self._control_vm("setlinkstate{} on".format(adapter_number + 1))
|
||||
|
||||
adapter.add_nio(0, nio)
|
||||
log.info("VirtualBox VM '{name}' [{id}]: {nio} added to adapter {adapter_number}".format(name=self.name,
|
||||
@@ -830,7 +835,7 @@ class VirtualBoxVM(BaseVM):
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
except KeyError:
|
||||
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
|
||||
@@ -851,6 +856,7 @@ class VirtualBoxVM(BaseVM):
|
||||
adapter_number=adapter_number))
|
||||
return nio
|
||||
|
||||
@asyncio.coroutine
|
||||
def start_capture(self, adapter_number, output_file):
|
||||
"""
|
||||
Starts a packet capture.
|
||||
@@ -861,11 +867,19 @@ class VirtualBoxVM(BaseVM):
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
except KeyError:
|
||||
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
|
||||
vm_state = yield from self._get_vm_state()
|
||||
if vm_state == "running" or vm_state == "paused" or vm_state == "stuck":
|
||||
raise VirtualBoxError("Sorry, packet capturing on a started VirtualBox VM is not supported.")
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
|
||||
if not nio:
|
||||
raise VirtualBoxError("Adapter {} is not connected".format(adapter_number))
|
||||
|
||||
if nio.capturing:
|
||||
raise VirtualBoxError("Packet capture is already activated on adapter {adapter_number}".format(adapter_number=adapter_number))
|
||||
|
||||
@@ -883,11 +897,15 @@ class VirtualBoxVM(BaseVM):
|
||||
|
||||
try:
|
||||
adapter = self._ethernet_adapters[adapter_number]
|
||||
except IndexError:
|
||||
except KeyError:
|
||||
raise VirtualBoxError("Adapter {adapter_number} doesn't exist on VirtualBox VM '{name}'".format(name=self.name,
|
||||
adapter_number=adapter_number))
|
||||
|
||||
nio = adapter.get_nio(0)
|
||||
|
||||
if not nio:
|
||||
raise VirtualBoxError("Adapter {} is not connected".format(adapter_number))
|
||||
|
||||
nio.stopPacketCapture()
|
||||
|
||||
log.info("VirtualBox VM '{name}' [{id}]: stopping packet capture on adapter {adapter_number}".format(name=self.name,
|
||||
|
||||
@@ -254,7 +254,10 @@ class VPCSVM(BaseVM):
|
||||
except asyncio.TimeoutError:
|
||||
if self._process.returncode is None:
|
||||
log.warn("VPCS process {} is still running... killing it".format(self._process.pid))
|
||||
self._process.kill()
|
||||
try:
|
||||
self._process.kill()
|
||||
except OSError as e:
|
||||
raise VPCSError("Can not stop the VPCS process: {}".format(e))
|
||||
|
||||
self._process = None
|
||||
self._started = False
|
||||
@@ -390,6 +393,9 @@ class VPCSVM(BaseVM):
|
||||
|
||||
command = [self.vpcs_path]
|
||||
command.extend(["-p", str(self._console)]) # listen to console port
|
||||
command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset
|
||||
command.extend(["-i", "1"]) # option to start only one VPC instance
|
||||
command.extend(["-F"]) # option to avoid the daemonization of VPCS
|
||||
|
||||
nio = self._ethernet_adapter.get_nio(0)
|
||||
if nio:
|
||||
@@ -404,10 +410,6 @@ class VPCSVM(BaseVM):
|
||||
command.extend(["-e"])
|
||||
command.extend(["-d", nio.tap_device])
|
||||
|
||||
command.extend(["-m", str(self._manager.get_mac_id(self.id))]) # the unique ID is used to set the MAC address offset
|
||||
command.extend(["-i", "1"]) # option to start only one VPC instance
|
||||
command.extend(["-F"]) # option to avoid the daemonization of VPCS
|
||||
|
||||
if self.script_file:
|
||||
command.extend([os.path.basename(self.script_file)])
|
||||
return command
|
||||
|
||||
@@ -218,6 +218,16 @@ DEVICE_NIO_SCHEMA = {
|
||||
"required": ["type", "ethernet_device"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"NAT": {
|
||||
"description": "NAT Network Input/Output",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["nio_nat"]
|
||||
},
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"TAP": {
|
||||
"description": "TAP Network Input/Output",
|
||||
"properties": {
|
||||
@@ -291,6 +301,7 @@ DEVICE_NIO_SCHEMA = {
|
||||
{"$ref": "#/definitions/UDP"},
|
||||
{"$ref": "#/definitions/Ethernet"},
|
||||
{"$ref": "#/definitions/LinuxEthernet"},
|
||||
{"$ref": "#/definitions/NAT"},
|
||||
{"$ref": "#/definitions/TAP"},
|
||||
{"$ref": "#/definitions/UNIX"},
|
||||
{"$ref": "#/definitions/VDE"},
|
||||
|
||||
@@ -120,6 +120,10 @@ VM_CREATE_SCHEMA = {
|
||||
"description": "disk1 size in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"auto_delete_disks": {
|
||||
"description": "automatically delete nvram and disk files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"type": "integer",
|
||||
@@ -337,6 +341,10 @@ VM_UPDATE_SCHEMA = {
|
||||
"description": "disk1 size in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"auto_delete_disks": {
|
||||
"description": "automatically delete nvram and disk files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"type": "integer",
|
||||
@@ -591,6 +599,10 @@ VM_OBJECT_SCHEMA = {
|
||||
"description": "disk1 size in MB",
|
||||
"type": "integer"
|
||||
},
|
||||
"auto_delete_disks": {
|
||||
"description": "automatically delete nvram and disk files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"console": {
|
||||
"description": "console TCP port",
|
||||
"type": "integer",
|
||||
|
||||
+13
-1
@@ -86,6 +86,7 @@ class Server:
|
||||
|
||||
if self._handler:
|
||||
yield from self._handler.finish_connections()
|
||||
self._handler = None
|
||||
|
||||
for module in MODULES:
|
||||
log.debug("Unloading module {}".format(module.__name__))
|
||||
@@ -175,6 +176,10 @@ class Server:
|
||||
yield from embed(globals(), locals(), return_asyncio_coroutine=True, patch_stdout=True)
|
||||
|
||||
def _exit_handling(self):
|
||||
"""
|
||||
Makes sure the asyncio loop is closed.
|
||||
"""
|
||||
|
||||
def close_asyncio_loop():
|
||||
loop = None
|
||||
try:
|
||||
@@ -226,7 +231,8 @@ class Server:
|
||||
|
||||
log.info("Starting server on {}:{}".format(self._host, self._port))
|
||||
self._handler = app.make_handler(handler=RequestHandler)
|
||||
self._loop.run_until_complete(self._run_application(self._handler, ssl_context))
|
||||
server = self._run_application(self._handler, ssl_context)
|
||||
self._loop.run_until_complete(server)
|
||||
self._signal_handling()
|
||||
|
||||
self._exit_handling()
|
||||
@@ -245,3 +251,9 @@ class Server:
|
||||
# on Windows when the process gets the SIGBREAK signal
|
||||
# TypeError: async() takes 1 positional argument but 3 were given
|
||||
log.warning("TypeError exception in the loop {}".format(e))
|
||||
finally:
|
||||
if self._handler and self._loop.is_running():
|
||||
self._loop.run_until_complete(self._handler.finish_connections())
|
||||
server.close()
|
||||
if self._loop.is_running():
|
||||
self._loop.run_until_complete(app.finish())
|
||||
@@ -6,6 +6,6 @@
|
||||
<ul>
|
||||
<li><a href="http://community.gns3.com">Community</a></li>
|
||||
<li><a href="http://api.gns3.net">API documentation</a></li>
|
||||
<li><a href="/upload">Upload images</a></li>
|
||||
<li><a href="/upload">Upload images & backup</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -4,6 +4,15 @@
|
||||
<title>GNS3 Server</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<a href="/">Home</a>
|
||||
|
|
||||
<a href="/upload">Upload</a>
|
||||
|
|
||||
<a href="/backup/images.tar">Backup images</a>
|
||||
|
|
||||
<a href="/backup/projects.tar">Backup projects</a>
|
||||
</div>
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
<small>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<option value="IOURC">IOU licence (iourc)</option>
|
||||
<option value="IOS">IOS</option>
|
||||
<option value="QEMU">Qemu</option>
|
||||
<option value="IMAGES">GNS3 images backup (.tar)</option>
|
||||
<option value="PROJECTS">GNS3 projects backup (.tar)</option>
|
||||
</select>
|
||||
<br />
|
||||
<br />
|
||||
|
||||
@@ -51,7 +51,10 @@ def subprocess_check_output(*args, cwd=None, env=None):
|
||||
output = yield from proc.stdout.read()
|
||||
if output is None:
|
||||
return ""
|
||||
return output.decode("utf-8")
|
||||
# If we received garbage we ignore invalid characters
|
||||
# it should happend only when user try to use another binary
|
||||
# and the code of VPCS, dynamips... Will detect it's not the correct binary
|
||||
return output.decode("utf-8", errors="ignore")
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "1.3.3"
|
||||
__version_info__ = (1, 3, 3, 0)
|
||||
__version__ = "1.3.8"
|
||||
__version_info__ = (1, 3, 8, 0)
|
||||
@@ -34,7 +34,7 @@ class ColouredFormatter(logging.Formatter):
|
||||
|
||||
message = super().format(record)
|
||||
|
||||
if not colour:
|
||||
if not colour or sys.platform.startswith("win"):
|
||||
return message.replace("#RESET#", "")
|
||||
|
||||
level_no = record.levelno
|
||||
@@ -81,12 +81,30 @@ class ColouredStreamHandler(logging.StreamHandler):
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
class WinStreamHandler(logging.StreamHandler):
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
if sys.stdin.encoding != "utf-8":
|
||||
record = record
|
||||
|
||||
stream = self.stream
|
||||
try:
|
||||
msg = self.formatter.format(record, stream.isatty())
|
||||
stream.write(msg.encode(stream.encoding, errors="replace").decode(stream.encoding))
|
||||
stream.write(self.terminator)
|
||||
self.flush()
|
||||
pass
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def init_logger(level, logfile=None, quiet=False):
|
||||
if logfile and len(logfile) > 0:
|
||||
stream_handler = logging.FileHandler(logfile)
|
||||
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
elif sys.platform.startswith("win"):
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
stream_handler = WinStreamHandler(sys.stdout)
|
||||
stream_handler.formatter = ColouredFormatter("{asctime} {levelname} {filename}:{lineno} {message}", "%Y-%m-%d %H:%M:%S", "{")
|
||||
else:
|
||||
stream_handler = ColouredStreamHandler(sys.stdout)
|
||||
|
||||
+32
-1
@@ -83,6 +83,31 @@ class Route(object):
|
||||
def delete(cls, path, *args, **kw):
|
||||
return cls._route('DELETE', path, *args, **kw)
|
||||
|
||||
@classmethod
|
||||
def authenticate(cls, request, route, server_config):
|
||||
"""
|
||||
Ask user for authentication
|
||||
|
||||
:returns: Response if you need to auth the user otherwise None
|
||||
"""
|
||||
if not server_config.getboolean("auth", False):
|
||||
return
|
||||
|
||||
user = server_config.get("user", "").strip()
|
||||
password = server_config.get("password", "").strip()
|
||||
|
||||
if len(user) == 0:
|
||||
return
|
||||
|
||||
if "AUTHORIZATION" in request.headers:
|
||||
if request.headers["AUTHORIZATION"] == aiohttp.helpers.BasicAuth(user, password).encode():
|
||||
return
|
||||
|
||||
response = Response(request=request, route=route)
|
||||
response.set_status(401)
|
||||
response.headers["WWW-Authenticate"] = 'Basic realm="GNS3 server"'
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def _route(cls, method, path, *args, **kw):
|
||||
# This block is executed only the first time
|
||||
@@ -118,6 +143,13 @@ class Route(object):
|
||||
def control_schema(request):
|
||||
# This block is executed at each method call
|
||||
|
||||
server_config = Config.instance().get_section_config("Server")
|
||||
|
||||
# Authenticate
|
||||
response = cls.authenticate(request, route, server_config)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# Non API call
|
||||
if api_version is None:
|
||||
response = Response(request=request, route=route, output_schema=output_schema)
|
||||
@@ -127,7 +159,6 @@ class Route(object):
|
||||
# API call
|
||||
try:
|
||||
request = yield from parse_request(request, input_schema)
|
||||
server_config = Config.instance().get_section_config("Server")
|
||||
record_file = server_config.get("record")
|
||||
if record_file:
|
||||
try:
|
||||
|
||||
@@ -6,7 +6,7 @@ stop on shutdown
|
||||
|
||||
script
|
||||
echo $$ > /var/run/gns3.pid
|
||||
exec start-stop-daemon --start -c gns3 --exec /usr/local/bin/gns3server --log /var/log/gns3.log
|
||||
exec start-stop-daemon --start -c gns3 --exec /usr/local/bin/gns3server -- --log /var/log/gns3.log
|
||||
end script
|
||||
|
||||
pre-start script
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
netifaces==0.10.4
|
||||
gns3-netifaces==0.10.4.1
|
||||
jsonschema==2.4.0
|
||||
python-dateutil==2.3
|
||||
aiohttp==0.14.4
|
||||
Jinja2==2.7.3
|
||||
raven==5.2.0
|
||||
@@ -19,6 +19,10 @@ import sys
|
||||
from setuptools import setup, find_packages
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
# we only support Python 3 version >= 3.3
|
||||
if sys.version_info < (3, 3):
|
||||
raise SystemExit("Python 3.3 or higher is required")
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
|
||||
@@ -34,12 +38,14 @@ class PyTest(TestCommand):
|
||||
sys.exit(errcode)
|
||||
|
||||
|
||||
dependencies = ["aiohttp>=0.14.4",
|
||||
"jsonschema>=2.4.0",
|
||||
"Jinja2>=2.7.3",
|
||||
"raven>=5.2.0"]
|
||||
dependencies = [
|
||||
# "gns3-netifaces>=0.10.4.1",
|
||||
"aiohttp>=0.14.4",
|
||||
"jsonschema>=2.4.0",
|
||||
"Jinja2>=2.7.3",
|
||||
"raven>=5.2.0"]
|
||||
|
||||
#if not sys.platform.startswith("win"):
|
||||
# if not sys.platform.startswith("win"):
|
||||
# dependencies.append("netifaces==0.10.4")
|
||||
|
||||
if sys.version_info == (3, 3):
|
||||
|
||||
@@ -136,6 +136,7 @@ def run_around_tests(monkeypatch):
|
||||
config = Config.instance()
|
||||
config.clear()
|
||||
config.set("Server", "project_directory", tmppath)
|
||||
config.set("Server", "auth", False)
|
||||
|
||||
# Prevent exectuions of the VM if we forgot to mock something
|
||||
config.set("VirtualBox", "vboxmanage_path", tmppath)
|
||||
|
||||
@@ -88,7 +88,10 @@ class Query:
|
||||
except ValueError:
|
||||
response.json = None
|
||||
else:
|
||||
response.html = response.body.decode("utf-8")
|
||||
try:
|
||||
response.html = response.body.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
response.html = None
|
||||
else:
|
||||
response.json = {}
|
||||
response.html = ""
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import pytest
|
||||
|
||||
|
||||
def test_udp_allocation(server, project):
|
||||
response = server.post('/projects/{}/ports/udp'.format(project.id), {}, example=True)
|
||||
@@ -22,6 +25,8 @@ def test_udp_allocation(server, project):
|
||||
assert response.json == {'udp_port': 10000}
|
||||
|
||||
|
||||
# Netfifaces is not available on Travis
|
||||
@pytest.mark.skipif(os.environ.get("TRAVIS", False) is not False, reason="Not supported on Travis")
|
||||
def test_interfaces(server):
|
||||
response = server.get('/interfaces', example=True)
|
||||
assert response.status == 200
|
||||
|
||||
@@ -20,6 +20,7 @@ This test suite check /project endpoint
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from tests.utils import asyncio_patch
|
||||
|
||||
@@ -85,18 +86,43 @@ def test_update_temporary_project(server):
|
||||
assert response.json["temporary"] is False
|
||||
|
||||
|
||||
def test_update_path_project(server, tmpdir):
|
||||
def test_update_path_project_temporary(server, tmpdir):
|
||||
|
||||
os.makedirs(str(tmpdir / "a"))
|
||||
os.makedirs(str(tmpdir / "b"))
|
||||
|
||||
with patch("gns3server.modules.project.Project.is_local", return_value=True):
|
||||
response = server.post("/projects", {"name": "first_name"})
|
||||
response = server.post("/projects", {"name": "first_name", "path": str(tmpdir / "a"), "temporary": True})
|
||||
assert response.status == 201
|
||||
assert response.json["name"] == "first_name"
|
||||
query = {"name": "second_name", "path": str(tmpdir)}
|
||||
query = {"name": "second_name", "path": str(tmpdir / "b")}
|
||||
response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True)
|
||||
assert response.status == 200
|
||||
assert response.json["path"] == str(tmpdir)
|
||||
assert response.json["path"] == str(tmpdir / "b")
|
||||
assert response.json["name"] == "second_name"
|
||||
|
||||
assert not os.path.exists(str(tmpdir / "a"))
|
||||
assert os.path.exists(str(tmpdir / "b"))
|
||||
|
||||
|
||||
def test_update_path_project_non_temporary(server, tmpdir):
|
||||
|
||||
os.makedirs(str(tmpdir / "a"))
|
||||
os.makedirs(str(tmpdir / "b"))
|
||||
|
||||
with patch("gns3server.modules.project.Project.is_local", return_value=True):
|
||||
response = server.post("/projects", {"name": "first_name", "path": str(tmpdir / "a")})
|
||||
assert response.status == 201
|
||||
assert response.json["name"] == "first_name"
|
||||
query = {"name": "second_name", "path": str(tmpdir / "b")}
|
||||
response = server.put("/projects/{project_id}".format(project_id=response.json["project_id"]), query, example=True)
|
||||
assert response.status == 200
|
||||
assert response.json["path"] == str(tmpdir / "b")
|
||||
assert response.json["name"] == "second_name"
|
||||
|
||||
assert os.path.exists(str(tmpdir / "a"))
|
||||
assert os.path.exists(str(tmpdir / "b"))
|
||||
|
||||
|
||||
def test_update_path_project_non_local(server, tmpdir):
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ def test_vpcs_get(server, project, vm):
|
||||
assert response.route == "/projects/{project_id}/vpcs/vms/{vm_id}"
|
||||
assert response.json["name"] == "PC TEST 1"
|
||||
assert response.json["project_id"] == project.id
|
||||
assert response.json["startup_script_path"] == None
|
||||
assert response.json["startup_script_path"] is None
|
||||
|
||||
|
||||
def test_vpcs_create_startup_script(server, project):
|
||||
@@ -51,7 +51,7 @@ def test_vpcs_create_startup_script(server, project):
|
||||
assert response.route == "/projects/{project_id}/vpcs/vms"
|
||||
assert response.json["name"] == "PC TEST 1"
|
||||
assert response.json["project_id"] == project.id
|
||||
assert response.json["startup_script"] == "ip 192.168.1.2\necho TEST"
|
||||
assert response.json["startup_script"] == os.linesep.join(["ip 192.168.1.2", "echo TEST"])
|
||||
assert response.json["startup_script_path"] == "startup.vpc"
|
||||
|
||||
|
||||
|
||||
@@ -17,10 +17,15 @@
|
||||
|
||||
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import os
|
||||
import tarfile
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
from gns3server.config import Config
|
||||
|
||||
|
||||
def test_index_upload(server):
|
||||
response = server.get('/upload', api_version=None)
|
||||
assert response.status == 200
|
||||
@@ -37,10 +42,141 @@ def test_upload(server, tmpdir):
|
||||
body.add_field("type", "QEMU")
|
||||
body.add_field("file", open(str(tmpdir / "test"), "rb"), content_type="application/iou", filename="test2")
|
||||
|
||||
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
|
||||
response = server.post('/upload', api_version=None, body=body, raw=True)
|
||||
Config.instance().set("Server", "images_path", str(tmpdir))
|
||||
response = server.post('/upload', api_version=None, body=body, raw=True)
|
||||
|
||||
with open(str(tmpdir / "QEMU" / "test2")) as f:
|
||||
assert f.read() == "TEST"
|
||||
|
||||
assert "test2" in response.body.decode("utf-8")
|
||||
|
||||
|
||||
def test_upload_images_backup(server, tmpdir):
|
||||
Config.instance().set("Server", "images_path", str(tmpdir / 'images'))
|
||||
os.makedirs(str(tmpdir / 'images' / 'IOU'))
|
||||
# An old IOU image that we need to replace
|
||||
with open(str(tmpdir / 'images' / 'IOU' / 'b.img'), 'w+') as f:
|
||||
f.write('bad')
|
||||
|
||||
os.makedirs(str(tmpdir / 'old' / 'QEMU'))
|
||||
with open(str(tmpdir / 'old' / 'QEMU' / 'a.img'), 'w+') as f:
|
||||
f.write('hello')
|
||||
os.makedirs(str(tmpdir / 'old' / 'IOU'))
|
||||
with open(str(tmpdir / 'old' / 'IOU' / 'b.img'), 'w+') as f:
|
||||
f.write('world')
|
||||
|
||||
os.chdir(str(tmpdir / 'old'))
|
||||
with tarfile.open(str(tmpdir / 'test.tar'), 'w') as tar:
|
||||
tar.add('.', recursive=True)
|
||||
|
||||
body = aiohttp.FormData()
|
||||
body.add_field('type', 'IMAGES')
|
||||
body.add_field('file', open(str(tmpdir / 'test.tar'), 'rb'), content_type='application/x-gtar', filename='test.tar')
|
||||
response = server.post('/upload', api_version=None, body=body, raw=True)
|
||||
assert response.status == 200
|
||||
|
||||
with open(str(tmpdir / 'images' / 'QEMU' / 'a.img')) as f:
|
||||
assert f.read() == 'hello'
|
||||
with open(str(tmpdir / 'images' / 'IOU' / 'b.img')) as f:
|
||||
assert f.read() == 'world'
|
||||
|
||||
assert 'a.img' in response.body.decode('utf-8')
|
||||
assert 'b.img' in response.body.decode('utf-8')
|
||||
assert not os.path.exists(str(tmpdir / 'images' / 'archive.tar'))
|
||||
|
||||
|
||||
def test_upload_projects_backup(server, tmpdir):
|
||||
Config.instance().set("Server", "projects_path", str(tmpdir / 'projects'))
|
||||
os.makedirs(str(tmpdir / 'projects' / 'b'))
|
||||
# An old b image that we need to replace
|
||||
with open(str(tmpdir / 'projects' / 'b' / 'b.img'), 'w+') as f:
|
||||
f.write('bad')
|
||||
|
||||
os.makedirs(str(tmpdir / 'old' / 'a'))
|
||||
with open(str(tmpdir / 'old' / 'a' / 'a.img'), 'w+') as f:
|
||||
f.write('hello')
|
||||
os.makedirs(str(tmpdir / 'old' / 'b'))
|
||||
with open(str(tmpdir / 'old' / 'b' / 'b.img'), 'w+') as f:
|
||||
f.write('world')
|
||||
|
||||
os.chdir(str(tmpdir / 'old'))
|
||||
with tarfile.open(str(tmpdir / 'test.tar'), 'w') as tar:
|
||||
tar.add('.', recursive=True)
|
||||
|
||||
body = aiohttp.FormData()
|
||||
body.add_field('type', 'PROJECTS')
|
||||
body.add_field('file', open(str(tmpdir / 'test.tar'), 'rb'), content_type='application/x-gtar', filename='test.tar')
|
||||
response = server.post('/upload', api_version=None, body=body, raw=True)
|
||||
assert response.status == 200
|
||||
|
||||
with open(str(tmpdir / 'projects' / 'a' / 'a.img')) as f:
|
||||
assert f.read() == 'hello'
|
||||
with open(str(tmpdir / 'projects' / 'b' / 'b.img')) as f:
|
||||
assert f.read() == 'world'
|
||||
|
||||
assert 'a.img' not in response.body.decode('utf-8')
|
||||
assert 'b.img' not in response.body.decode('utf-8')
|
||||
assert not os.path.exists(str(tmpdir / 'projects' / 'archive.tar'))
|
||||
|
||||
|
||||
def test_backup_images(server, tmpdir, loop):
|
||||
Config.instance().set('Server', 'images_path', str(tmpdir))
|
||||
|
||||
os.makedirs(str(tmpdir / 'QEMU'))
|
||||
with open(str(tmpdir / 'QEMU' / 'a.img'), 'w+') as f:
|
||||
f.write('hello')
|
||||
with open(str(tmpdir / 'QEMU' / 'b.img'), 'w+') as f:
|
||||
f.write('world')
|
||||
|
||||
response = server.get('/backup/images.tar', api_version=None, raw=True)
|
||||
assert response.status == 200
|
||||
assert response.headers['CONTENT-TYPE'] == 'application/x-gtar'
|
||||
|
||||
with open(str(tmpdir / 'images.tar'), 'wb+') as f:
|
||||
print(len(response.body))
|
||||
f.write(response.body)
|
||||
|
||||
tar = tarfile.open(str(tmpdir / 'images.tar'), 'r')
|
||||
os.makedirs(str(tmpdir / 'extract'))
|
||||
os.chdir(str(tmpdir / 'extract'))
|
||||
# Extract to current working directory
|
||||
tar.extractall()
|
||||
tar.close()
|
||||
|
||||
assert os.path.exists(os.path.join('QEMU', 'a.img'))
|
||||
open(os.path.join('QEMU', 'a.img')).read() == 'hello'
|
||||
|
||||
assert os.path.exists(os.path.join('QEMU', 'b.img'))
|
||||
open(os.path.join('QEMU', 'b.img')).read() == 'world'
|
||||
|
||||
|
||||
def test_backup_projects(server, tmpdir, loop):
|
||||
Config.instance().set('Server', 'projects_path', str(tmpdir))
|
||||
|
||||
os.makedirs(str(tmpdir / 'a'))
|
||||
with open(str(tmpdir / 'a' / 'a.gns3'), 'w+') as f:
|
||||
f.write('hello')
|
||||
os.makedirs(str(tmpdir / 'b'))
|
||||
with open(str(tmpdir / 'b' / 'b.gns3'), 'w+') as f:
|
||||
f.write('world')
|
||||
|
||||
response = server.get('/backup/projects.tar', api_version=None, raw=True)
|
||||
assert response.status == 200
|
||||
assert response.headers['CONTENT-TYPE'] == 'application/x-gtar'
|
||||
|
||||
with open(str(tmpdir / 'projects.tar'), 'wb+') as f:
|
||||
print(len(response.body))
|
||||
f.write(response.body)
|
||||
|
||||
tar = tarfile.open(str(tmpdir / 'projects.tar'), 'r')
|
||||
os.makedirs(str(tmpdir / 'extract'))
|
||||
os.chdir(str(tmpdir / 'extract'))
|
||||
# Extract to current working directory
|
||||
tar.extractall()
|
||||
tar.close()
|
||||
|
||||
assert os.path.exists(os.path.join('a', 'a.gns3'))
|
||||
open(os.path.join('a', 'a.gns3')).read() == 'hello'
|
||||
|
||||
assert os.path.exists(os.path.join('b', 'b.gns3'))
|
||||
open(os.path.join('b', 'b.gns3')).read() == 'world'
|
||||
@@ -73,10 +73,6 @@ def test_changing_path_temporary_flag(tmpdir):
|
||||
assert os.path.exists(os.path.join(p.path, ".gns3_temporary"))
|
||||
|
||||
p.path = str(tmpdir)
|
||||
p.temporary = False
|
||||
assert not os.path.exists(os.path.join(p.path, ".gns3_temporary"))
|
||||
assert not os.path.exists(os.path.join(str(tmpdir), ".gns3_temporary"))
|
||||
assert not os.path.exists(original_path)
|
||||
|
||||
|
||||
def test_temporary_path():
|
||||
|
||||
@@ -20,10 +20,14 @@ import pytest
|
||||
import tempfile
|
||||
import os
|
||||
import stat
|
||||
import asyncio
|
||||
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from gns3server.modules.virtualbox import VirtualBox
|
||||
from gns3server.modules.virtualbox.virtualbox_error import VirtualBoxError
|
||||
from unittest.mock import patch
|
||||
from tests.utils import asyncio_patch
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
@@ -65,3 +69,31 @@ def test_vboxmanage_path(manager, tmpdir):
|
||||
tmpfile = tempfile.NamedTemporaryFile()
|
||||
with patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": path}):
|
||||
assert manager.find_vboxmanage() == path
|
||||
|
||||
|
||||
def test_get_list(manager, loop):
|
||||
vm_list = ['"Windows 8.1" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}',
|
||||
'"Carriage',
|
||||
'Return" {27b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}',
|
||||
'',
|
||||
'"<inaccessible>" {42b4d095-ff5f-4ac4-bb9d-5f2c7861c1f1}',
|
||||
'"Linux Microcore 4.7.1" {ccd8c50b-c172-457d-99fa-dd69371ede0e}']
|
||||
|
||||
@asyncio.coroutine
|
||||
def execute_mock(cmd, args):
|
||||
if cmd == "list":
|
||||
return vm_list
|
||||
else:
|
||||
if args[0] == "Windows 8.1":
|
||||
return ["memory=512"]
|
||||
elif args[0] == "Linux Microcore 4.7.1":
|
||||
return ["memory=256"]
|
||||
assert False, "Unknow {} {}".format(cmd, args)
|
||||
|
||||
with asyncio_patch("gns3server.modules.virtualbox.VirtualBox.execute") as mock:
|
||||
mock.side_effect = execute_mock
|
||||
vms = loop.run_until_complete(asyncio.async(manager.get_list()))
|
||||
assert vms == [
|
||||
{"vmname": "Windows 8.1", "ram": 512},
|
||||
{"vmname": "Linux Microcore 4.7.1", "ram": 256}
|
||||
]
|
||||
@@ -54,3 +54,9 @@ def test_vm_invalid_virtualbox_api_version(loop, project, manager):
|
||||
with pytest.raises(VirtualBoxError):
|
||||
vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False)
|
||||
loop.run_until_complete(asyncio.async(vm.create()))
|
||||
|
||||
|
||||
def test_vm_adapter_add_nio_binding_adapter_not_exist(loop, vm, manager, free_console_port):
|
||||
nio = manager.create_nio(manager.vboxmanage_path, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "192.168.1.2"})
|
||||
with pytest.raises(VirtualBoxError):
|
||||
loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(15, nio)))
|
||||
@@ -190,9 +190,9 @@ def test_update_startup_script_h(vm):
|
||||
|
||||
|
||||
def test_get_startup_script(vm):
|
||||
content = "echo GNS3 VPCS\nip 192.168.1.2\n"
|
||||
content = "echo GNS3 VPCS\nip 192.168.1.2"
|
||||
vm.startup_script = content
|
||||
assert vm.startup_script == content
|
||||
assert vm.startup_script == os.linesep.join(["echo GNS3 VPCS","ip 192.168.1.2"])
|
||||
|
||||
|
||||
def test_get_startup_script_using_default_script(vm):
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- 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 gns3server.utils.interfaces import interfaces
|
||||
|
||||
|
||||
def test_interfaces():
|
||||
# This test should pass on all platforms without crash
|
||||
assert isinstance(interfaces(), list)
|
||||
Reference in new issue
Block a user