mirror of
https://github.com/GNS3/gns3-server.git
synced 2025-06-24 17:55:15 +00:00
Compare commits
113 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
078b72cafd | |||
b2457e0b3b | |||
2531a05adc | |||
dd9f62158f | |||
a3c0f0754e | |||
157bc18ebd | |||
3704911c2d | |||
1e38b11f34 | |||
bebdadc465 | |||
b0ce091a4c | |||
d21469a916 | |||
b57a023394 | |||
a929dfea38 | |||
fcff2d0813 | |||
ed39afbf3d | |||
3ba4789ba6 | |||
17b93e6a89 | |||
4b21135ba7 | |||
a3f00e1f45 | |||
b7dac1bec4 | |||
18c4154376 | |||
22efc7488f | |||
9eeb8910fb | |||
71e2586e17 | |||
ee2dada88b | |||
c4054cf810 | |||
d2d91ebdea | |||
0dea63c9ea | |||
3467b42ab5 | |||
65103e9332 | |||
f6bc823b58 | |||
151788e48a | |||
6b70fa9794 | |||
359abb0286 | |||
d18293ae7c | |||
929c337e8b | |||
b9bc73fd01 | |||
e75fbc9d73 | |||
0311a0086e | |||
461e3ce53f | |||
ee1e5b8204 | |||
8f6e5b4ad8 | |||
14cc01bb8b | |||
40ce22222e | |||
7e991cc404 | |||
83f2509cfe | |||
fac0f5ecd9 | |||
2e39265da1 | |||
bf618d321c | |||
8b879c0614 | |||
cf0adf56a8 | |||
dfdc18b20c | |||
bca90bc563 | |||
5a4ffae6a2 | |||
789e24795e | |||
1d5dc2ecf0 |
65
CHANGELOG
65
CHANGELOG
@ -1,5 +1,70 @@
|
||||
# Change Log
|
||||
|
||||
## 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.
|
||||
* Fixes bugs with IOS router configs. Fixes #354.
|
||||
* Use a temporary directory as egg cache
|
||||
* Catch crash error in IOU in case of permission denied
|
||||
|
||||
## 1.3.3rc1 07/05/2015
|
||||
|
||||
* Return an error if an adapter slot doesn't exist on an IOS router.
|
||||
* NIO NAT support for VirtualBox VMs.
|
||||
* NIO NAT support for QEMU VMs (user mode back-end is used).
|
||||
* Throw an error if user put an invalid port range in config file
|
||||
* Turn off configuration parser interpolation
|
||||
* Catch configuration file parsing errors
|
||||
* Force closing the event loop to avoid warning with Python 3.4.3
|
||||
* Catch error when you can't mark a project as no longer temporary
|
||||
* Catch BrokenPipeError for OSX frozen server
|
||||
* Match how IOU initial-config is set for VPCS VM.
|
||||
* Refactors how startup-config and private-config are handled for IOS routers.
|
||||
* Catch the "WinError 0 The operation completed successfully" exception at a higher level.
|
||||
* Fix temporary project not cleanup with save as
|
||||
* If image is not found in VM directory look in images folder
|
||||
* Ordered MAC addresses for QEMU based VMs.
|
||||
* Merge remote-tracking branch 'origin/master'
|
||||
* Force utf-8 configuraton files reading
|
||||
* Do not list file starting with a . in upload handler
|
||||
* Do not crash when closing a project if VirtualBox is not accessible
|
||||
* Catch connection reset errors
|
||||
|
||||
|
||||
## 1.3.2 28/04/2015
|
||||
|
||||
* Cleanup the VirtualBox Media Manager after closing a project.
|
||||
|
@ -6,4 +6,3 @@ pep8==1.5.7
|
||||
pytest-timeout
|
||||
pytest-capturelog
|
||||
pytest-cov
|
||||
python-coveralls
|
||||
|
2
gns3server.bat
Normal file
2
gns3server.bat
Normal file
@ -0,0 +1,2 @@
|
||||
SET PYTHONPATH=.
|
||||
python.exe gns3server/main.py --debug --local
|
@ -92,7 +92,7 @@ class Config(object):
|
||||
|
||||
def clear(self):
|
||||
"""Restart with a clean config"""
|
||||
self._config = configparser.ConfigParser()
|
||||
self._config = configparser.RawConfigParser()
|
||||
# Override config from command line even if we modify the config file and live reload it.
|
||||
self._override_config = {}
|
||||
|
||||
@ -135,7 +135,11 @@ class Config(object):
|
||||
Read the configuration files.
|
||||
"""
|
||||
|
||||
parsed_files = self._config.read(self._files)
|
||||
try:
|
||||
parsed_files = self._config.read(self._files, encoding="utf-8")
|
||||
except configparser.Error as e:
|
||||
log.error("Can't parse configuration file: %s", str(e))
|
||||
return
|
||||
if not parsed_files:
|
||||
log.warning("No configuration file could be found or read")
|
||||
else:
|
||||
|
@ -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://22979234ab4749ceabce08e6da4c1476:1432c8c7a43d410b9b5bb33f8e55b2a6@app.getsentry.com/38482"
|
||||
DSN = "sync+https://41286c30323843fd93a8f3f313c09dd0:eedbc6d7eed442a3bd5ab00d646a7fad@app.getsentry.com/38482"
|
||||
if hasattr(sys, "frozen"):
|
||||
cacert = os.path.join(os.getcwd(), "cacert.pem")
|
||||
if os.path.isfile(cacert):
|
||||
|
@ -26,6 +26,7 @@ from ...schemas.dynamips_vm import VM_CAPTURE_SCHEMA
|
||||
from ...schemas.dynamips_vm import VM_OBJECT_SCHEMA
|
||||
from ...schemas.dynamips_vm import VM_CONFIGS_SCHEMA
|
||||
from ...modules.dynamips import Dynamips
|
||||
from ...modules.dynamips.dynamips_error import DynamipsError
|
||||
from ...modules.project_manager import ProjectManager
|
||||
|
||||
DEFAULT_CHASSIS = {
|
||||
@ -358,13 +359,41 @@ class DynamipsVMHandler:
|
||||
project_id=request.match_info["project_id"])
|
||||
|
||||
startup_config_base64, private_config_base64 = yield from vm.extract_config()
|
||||
module_workdir = vm.project.module_working_directory(dynamips_manager.module_name.lower())
|
||||
result = {}
|
||||
if startup_config_base64:
|
||||
startup_config_content = base64.b64decode(startup_config_base64).decode(errors='replace')
|
||||
startup_config_content = base64.b64decode(startup_config_base64).decode("utf-8", errors='replace')
|
||||
result["startup_config_content"] = startup_config_content
|
||||
else:
|
||||
# nvram doesn't contain anything if the router has not been started at least once
|
||||
# in this case just use the startup-config file
|
||||
if vm.startup_config:
|
||||
startup_config_path = os.path.join(module_workdir, vm.startup_config)
|
||||
if os.path.isfile(startup_config_path):
|
||||
try:
|
||||
with open(startup_config_path, "rb") as f:
|
||||
content = f.read().decode("utf-8", errors='replace')
|
||||
if content:
|
||||
result["startup_config_content"] = content
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not read the startup-config {}: {}".format(startup_config_path, e))
|
||||
|
||||
if private_config_base64:
|
||||
private_config_content = base64.b64decode(private_config_base64).decode(errors='replace')
|
||||
private_config_content = base64.b64decode(private_config_base64).decode("utf-8", errors='replace')
|
||||
result["private_config_content"] = private_config_content
|
||||
else:
|
||||
# nvram doesn't contain anything if the router has not been started at least once
|
||||
# in this case just use the private-config file
|
||||
if vm.private_config:
|
||||
private_config_path = os.path.join(module_workdir, vm.private_config)
|
||||
if os.path.isfile(private_config_path):
|
||||
try:
|
||||
with open(private_config_path, "rb") as f:
|
||||
content = f.read().decode("utf-8", errors='replace')
|
||||
if content:
|
||||
result["private_config_content"] = content
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not read the private-config {}: {}".format(private_config_path, e))
|
||||
|
||||
response.set_status(200)
|
||||
response.json(result)
|
||||
|
@ -81,13 +81,16 @@ class ProjectHandler:
|
||||
|
||||
pm = ProjectManager.instance()
|
||||
project = pm.get_project(request.match_info["project_id"])
|
||||
project.temporary = request.json.get("temporary", project.temporary)
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
|
@ -247,7 +247,7 @@ class QEMUHandler:
|
||||
qemu_manager = Qemu.instance()
|
||||
vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
|
||||
nio_type = request.json["type"]
|
||||
if nio_type not in ("nio_udp", "nio_tap"):
|
||||
if nio_type not in ("nio_udp", "nio_tap", "nio_nat"):
|
||||
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
|
||||
nio = qemu_manager.create_nio(vm.qemu_path, request.json)
|
||||
yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio)
|
||||
|
@ -294,7 +294,7 @@ class VirtualBoxHandler:
|
||||
vbox_manager = VirtualBox.instance()
|
||||
vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"])
|
||||
nio_type = request.json["type"]
|
||||
if nio_type != "nio_udp":
|
||||
if nio_type not in ("nio_udp", "nio_nat"):
|
||||
raise HTTPConflict(text="NIO of type {} is not supported".format(nio_type))
|
||||
nio = vbox_manager.create_nio(vbox_manager.vboxmanage_path, request.json)
|
||||
yield from vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), nio)
|
||||
@ -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(
|
||||
|
@ -36,8 +36,9 @@ class UploadHandler:
|
||||
try:
|
||||
for root, _, files in os.walk(UploadHandler.image_directory()):
|
||||
for filename in files:
|
||||
image_file = os.path.join(root, filename)
|
||||
uploaded_files.append(image_file)
|
||||
if not filename.startswith("."):
|
||||
image_file = os.path.join(root, filename)
|
||||
uploaded_files.append(image_file)
|
||||
except OSError:
|
||||
pass
|
||||
iourc_path = os.path.join(os.path.expanduser("~/"), ".iourc")
|
||||
|
@ -16,6 +16,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# WARNING
|
||||
# Due to buggy user machines we choose to put this as the first loading modules
|
||||
# otherwise the egg cache is initialized in his standard location and
|
||||
# if is not writetable the application crash. It's the user fault
|
||||
# because one day the user as used sudo to run an egg and break his
|
||||
# filesystem permissions, but it's a common mistake.
|
||||
import gns3server.utils.get_resource
|
||||
|
||||
|
||||
import os
|
||||
import datetime
|
||||
import sys
|
||||
@ -176,11 +186,21 @@ 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:
|
||||
server.run()
|
||||
except OSError as e:
|
||||
# This is to ignore OSError: [WinError 0] The operation completed successfully exception on Windows.
|
||||
if not sys.platform.startswith("win") and not e.winerror == 0:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.critical("Critical error while running the server: {}".format(e), exc_info=1)
|
||||
CrashReport.instance().capture_exception()
|
||||
|
@ -34,6 +34,7 @@ from .project_manager import ProjectManager
|
||||
|
||||
from .nios.nio_udp import NIOUDP
|
||||
from .nios.nio_tap import NIOTAP
|
||||
from .nios.nio_nat import NIONAT
|
||||
from .nios.nio_generic_ethernet import NIOGenericEthernet
|
||||
|
||||
|
||||
@ -370,6 +371,8 @@ class BaseManager:
|
||||
nio = NIOTAP(tap_device)
|
||||
elif nio_settings["type"] == "nio_generic_ethernet":
|
||||
nio = NIOGenericEthernet(nio_settings["ethernet_device"])
|
||||
elif nio_settings["type"] == "nio_nat":
|
||||
nio = NIONAT()
|
||||
assert nio is not None
|
||||
return nio
|
||||
|
||||
@ -386,7 +389,16 @@ class BaseManager:
|
||||
img_directory = self.get_images_directory()
|
||||
if not os.path.isabs(path):
|
||||
s = os.path.split(path)
|
||||
return os.path.normpath(os.path.join(img_directory, *s))
|
||||
path = os.path.normpath(os.path.join(img_directory, *s))
|
||||
|
||||
# Compatibility with old topologies we look in parent directory
|
||||
# We look at first in new location
|
||||
if not os.path.exists(path):
|
||||
old_path = os.path.normpath(os.path.join(img_directory, '..', *s))
|
||||
if os.path.exists(old_path):
|
||||
return old_path
|
||||
|
||||
return path
|
||||
return path
|
||||
|
||||
def get_relative_image_path(self, path):
|
||||
|
@ -210,7 +210,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):
|
||||
@ -410,6 +414,8 @@ class Dynamips(BaseManager):
|
||||
nio = NIOVDE(node.hypervisor, control_file, local_file)
|
||||
elif nio_settings["type"] == "nio_null":
|
||||
nio = NIONull(node.hypervisor)
|
||||
else:
|
||||
raise aiohttp.web.HTTPConflict(text="NIO of type {} is not supported".format(nio_settings["type"]))
|
||||
|
||||
yield from nio.create()
|
||||
return nio
|
||||
@ -471,31 +477,42 @@ class Dynamips(BaseManager):
|
||||
if hasattr(vm, "set_{}".format(name)):
|
||||
setter = getattr(vm, "set_{}".format(name))
|
||||
yield from setter(value)
|
||||
|
||||
elif name.startswith("slot") and value in ADAPTER_MATRIX:
|
||||
slot_id = int(name[-1])
|
||||
adapter_name = value
|
||||
adapter = ADAPTER_MATRIX[adapter_name]()
|
||||
if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)):
|
||||
yield from vm.slot_remove_binding(slot_id)
|
||||
if not isinstance(vm.slots[slot_id], type(adapter)):
|
||||
yield from vm.slot_add_binding(slot_id, adapter)
|
||||
try:
|
||||
if vm.slots[slot_id] and not isinstance(vm.slots[slot_id], type(adapter)):
|
||||
yield from vm.slot_remove_binding(slot_id)
|
||||
if not isinstance(vm.slots[slot_id], type(adapter)):
|
||||
yield from vm.slot_add_binding(slot_id, adapter)
|
||||
except IndexError:
|
||||
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
|
||||
elif name.startswith("slot") and value is None:
|
||||
slot_id = int(name[-1])
|
||||
if vm.slots[slot_id]:
|
||||
yield from vm.slot_remove_binding(slot_id)
|
||||
try:
|
||||
if vm.slots[slot_id]:
|
||||
yield from vm.slot_remove_binding(slot_id)
|
||||
except IndexError:
|
||||
raise DynamipsError("Slot {} doesn't exist on this router".format(slot_id))
|
||||
elif name.startswith("wic") and value in WIC_MATRIX:
|
||||
wic_slot_id = int(name[-1])
|
||||
wic_name = value
|
||||
wic = WIC_MATRIX[wic_name]()
|
||||
if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
|
||||
yield from vm.uninstall_wic(wic_slot_id)
|
||||
if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
|
||||
yield from vm.install_wic(wic_slot_id, wic)
|
||||
try:
|
||||
if vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
|
||||
yield from vm.uninstall_wic(wic_slot_id)
|
||||
if not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
|
||||
yield from vm.install_wic(wic_slot_id, wic)
|
||||
except IndexError:
|
||||
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
|
||||
elif name.startswith("wic") and value is None:
|
||||
wic_slot_id = int(name[-1])
|
||||
if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]:
|
||||
yield from vm.uninstall_wic(wic_slot_id)
|
||||
try:
|
||||
if vm.slots[0].wics and vm.slots[0].wics[wic_slot_id]:
|
||||
yield from vm.uninstall_wic(wic_slot_id)
|
||||
except IndexError:
|
||||
raise DynamipsError("WIC slot {} doesn't exist on this router".format(wic_slot_id))
|
||||
|
||||
mmap_support = self.config.get_section_config("Dynamips").getboolean("mmap_support", True)
|
||||
if mmap_support is False:
|
||||
@ -521,38 +538,34 @@ class Dynamips(BaseManager):
|
||||
default_startup_config_path = os.path.join(module_workdir, "configs", "i{}_startup-config.cfg".format(vm.dynamips_id))
|
||||
default_private_config_path = os.path.join(module_workdir, "configs", "i{}_private-config.cfg".format(vm.dynamips_id))
|
||||
|
||||
startup_config_path = settings.get("startup_config")
|
||||
startup_config_content = settings.get("startup_config_content")
|
||||
if startup_config_content:
|
||||
startup_config_path = self._create_config(vm, startup_config_content, default_startup_config_path)
|
||||
if startup_config_path:
|
||||
yield from vm.set_configs(startup_config_path)
|
||||
elif startup_config_content:
|
||||
startup_config_path = self._create_config(vm, default_startup_config_path, startup_config_content)
|
||||
yield from vm.set_configs(startup_config_path)
|
||||
else:
|
||||
startup_config_path = settings.get("startup_config")
|
||||
if startup_config_path:
|
||||
yield from vm.set_configs(startup_config_path)
|
||||
|
||||
private_config_path = settings.get("private_config")
|
||||
private_config_content = settings.get("private_config_content")
|
||||
if private_config_content:
|
||||
private_config_path = self._create_config(vm, private_config_content, default_private_config_path)
|
||||
if private_config_path:
|
||||
yield from vm.set_configs(vm.startup_config, private_config_path)
|
||||
elif private_config_content:
|
||||
private_config_path = self._create_config(vm, default_private_config_path, private_config_content)
|
||||
yield from vm.set_configs(vm.startup_config, private_config_path)
|
||||
else:
|
||||
private_config_path = settings.get("private_config")
|
||||
if private_config_path:
|
||||
yield from vm.set_configs(vm.startup_config, private_config_path)
|
||||
|
||||
def _create_config(self, vm, content, path):
|
||||
def _create_config(self, vm, path, content=None):
|
||||
"""
|
||||
Creates a config file.
|
||||
|
||||
:param vm: VM instance
|
||||
:param content: config content
|
||||
:param path: path to the destination config file
|
||||
:param content: config content
|
||||
|
||||
:returns: relative path to the created config file
|
||||
"""
|
||||
|
||||
log.info("Creating config file {}".format(path))
|
||||
content = "!\n" + content.replace("\r", "")
|
||||
content = content.replace('%h', vm.name)
|
||||
config_dir = os.path.dirname(path)
|
||||
try:
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
@ -561,7 +574,10 @@ class Dynamips(BaseManager):
|
||||
|
||||
try:
|
||||
with open(path, "wb") as f:
|
||||
f.write(content.encode("utf-8"))
|
||||
if content:
|
||||
content = "!\n" + content.replace("\r", "")
|
||||
content = content.replace('%h', vm.name)
|
||||
f.write(content.encode("utf-8"))
|
||||
except OSError as e:
|
||||
raise DynamipsError("Could not create config file {}: {}".format(path, e))
|
||||
|
||||
|
@ -276,15 +276,16 @@ class DynamipsHypervisor:
|
||||
while True:
|
||||
try:
|
||||
try:
|
||||
line = yield from self._reader.readline()
|
||||
#line = yield from self._reader.readline() # this can lead to ValueError: Line is too long
|
||||
chunk = yield from self._reader.read(1024) # match to Dynamips' buffer size
|
||||
except asyncio.CancelledError:
|
||||
# task has been canceled but continue to read
|
||||
# any remaining data sent by the hypervisor
|
||||
continue
|
||||
if not line:
|
||||
if not chunk:
|
||||
raise DynamipsError("No data returned from {host}:{port}, Dynamips process running: {run}"
|
||||
.format(host=self._host, port=self._port, run=self.is_running()))
|
||||
buf += line.decode("utf-8")
|
||||
buf += chunk.decode("utf-8")
|
||||
except OSError as e:
|
||||
raise DynamipsError("Lost communication with {host}:{port} :{error}, Dynamips process running: {run}"
|
||||
.format(host=self._host, port=self._port, error=e, run=self.is_running()))
|
||||
|
@ -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,
|
||||
|
@ -836,7 +836,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.
|
||||
|
||||
@ -1303,6 +1303,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 +1339,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,
|
||||
@ -1436,6 +1445,17 @@ class Router(BaseVM):
|
||||
private_config = private_config.replace("\\", '/')
|
||||
|
||||
if self._startup_config != startup_config or self._private_config != private_config:
|
||||
self._startup_config = startup_config
|
||||
self._private_config = private_config
|
||||
|
||||
module_workdir = self.project.module_working_directory(self.manager.module_name.lower())
|
||||
private_config_path = os.path.join(module_workdir, private_config)
|
||||
try:
|
||||
if not os.path.getsize(private_config_path):
|
||||
# an empty private-config can prevent a router to boot.
|
||||
private_config = ''
|
||||
except OSError as e:
|
||||
raise DynamipsError("Cannot access the private-config {}: {}".format(private_config_path, e))
|
||||
|
||||
yield from self._hypervisor.send('vm set_config "{name}" "{startup}" "{private}"'.format(name=self._name,
|
||||
startup=startup_config,
|
||||
@ -1445,15 +1465,11 @@ class Router(BaseVM):
|
||||
id=self._id,
|
||||
startup=startup_config))
|
||||
|
||||
self._startup_config = startup_config
|
||||
|
||||
if private_config:
|
||||
log.info('Router "{name}" [{id}]: has a new private-config set: "{private}"'.format(name=self._name,
|
||||
id=self._id,
|
||||
private=private_config))
|
||||
|
||||
self._private_config = private_config
|
||||
|
||||
@asyncio.coroutine
|
||||
def extract_config(self):
|
||||
"""
|
||||
@ -1483,6 +1499,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", "")
|
||||
@ -1494,6 +1513,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", "")
|
||||
|
@ -433,11 +433,14 @@ class IOUVM(BaseVM):
|
||||
|
||||
yield from self._library_check()
|
||||
|
||||
self._rename_nvram_file()
|
||||
try:
|
||||
self._rename_nvram_file()
|
||||
except OSError as e:
|
||||
raise IOUError("Could not rename nvram files: {}".format(e))
|
||||
|
||||
iourc_path = self.iourc_path
|
||||
if iourc_path is None:
|
||||
raise IOUError("Could not find a iourc file (IOU license)")
|
||||
if not iourc_path:
|
||||
raise IOUError("Could not find an iourc file (IOU license)")
|
||||
if not os.path.isfile(iourc_path):
|
||||
raise IOUError("The iourc path '{}' is not a regular file".format(iourc_path))
|
||||
|
||||
|
41
gns3server/modules/nios/nio_nat.py
Normal file
41
gns3server/modules/nios/nio_nat.py
Normal file
@ -0,0 +1,41 @@
|
||||
# -*- 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 NAT NIOs.
|
||||
"""
|
||||
|
||||
from .nio import NIO
|
||||
|
||||
|
||||
class NIONAT(NIO):
|
||||
|
||||
"""
|
||||
NAT NIO.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super().__init__()
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "NIO TAP"
|
||||
|
||||
def __json__(self):
|
||||
|
||||
return {"type": "nio_nat"}
|
@ -140,7 +140,7 @@ class PortManager:
|
||||
"""
|
||||
|
||||
if end_port < start_port:
|
||||
raise Exception("Invalid port range {}-{}".format(start_port, end_port))
|
||||
raise HTTPConflict(text="Invalid port range {}-{}".format(start_port, end_port))
|
||||
|
||||
if socket_type == "UDP":
|
||||
socket_type = socket.SOCK_DGRAM
|
||||
|
@ -139,9 +139,25 @@ class Project:
|
||||
if path != self._path and self.is_local() is False:
|
||||
raise aiohttp.web.HTTPForbidden(text="You are not allowed to modify the project directory location")
|
||||
|
||||
old_path = None
|
||||
if hasattr(self, "_path"):
|
||||
old_path = self._path
|
||||
|
||||
self._path = path
|
||||
self._update_temporary_file()
|
||||
|
||||
@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:
|
||||
yield from wait_run_in_executor(shutil.rmtree, old_path)
|
||||
except OSError as e:
|
||||
log.warn("Can't remove temporary directory {}: {}".format(old_path, e))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@ -228,7 +244,10 @@ class Project:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not create temporary project: {}".format(e))
|
||||
else:
|
||||
if os.path.exists(os.path.join(self._path, ".gns3_temporary")):
|
||||
os.remove(os.path.join(self._path, ".gns3_temporary"))
|
||||
try:
|
||||
os.remove(os.path.join(self._path, ".gns3_temporary"))
|
||||
except OSError as e:
|
||||
raise aiohttp.web.HTTPInternalServerError(text="Could not mark project as no longer temporary: {}".format(e))
|
||||
|
||||
def module_working_directory(self, module_name):
|
||||
"""
|
||||
|
@ -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):
|
||||
|
@ -23,7 +23,6 @@ order to run a QEMU VM.
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
import subprocess
|
||||
import shlex
|
||||
import asyncio
|
||||
@ -33,6 +32,7 @@ from .qemu_error import QemuError
|
||||
from ..adapters.ethernet_adapter import EthernetAdapter
|
||||
from ..nios.nio_udp import NIOUDP
|
||||
from ..nios.nio_tap import NIOTAP
|
||||
from ..nios.nio_nat import NIONAT
|
||||
from ..base_vm import BaseVM
|
||||
from ...schemas.qemu import QEMU_OBJECT_SCHEMA
|
||||
|
||||
@ -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))
|
||||
@ -981,46 +983,47 @@ class QemuVM(BaseVM):
|
||||
|
||||
return options
|
||||
|
||||
def _get_random_mac(self, adapter_number):
|
||||
# TODO: let users specify a base mac address
|
||||
return "00:00:ab:%02x:%02x:%02d" % (random.randint(0x00, 0xff), random.randint(0x00, 0xff), adapter_number)
|
||||
|
||||
def _network_options(self):
|
||||
|
||||
network_options = []
|
||||
adapter_number = 0
|
||||
for adapter in self._ethernet_adapters:
|
||||
mac = self._get_random_mac(adapter_number)
|
||||
if self._legacy_networking:
|
||||
network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)])
|
||||
else:
|
||||
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_number)])
|
||||
network_options.extend(["-net", "none"]) # we do not want any user networking back-end if no adapter is connected.
|
||||
for adapter_number, adapter in enumerate(self._ethernet_adapters):
|
||||
# TODO: let users specify a base mac address
|
||||
mac = "00:00:ab:%s:%s:%02x" % (self.id[-4:-2], self.id[-2:], adapter_number)
|
||||
nio = adapter.get_nio(0)
|
||||
if nio:
|
||||
if isinstance(nio, NIOUDP):
|
||||
if self._legacy_networking:
|
||||
if self._legacy_networking:
|
||||
# legacy QEMU networking syntax (-net)
|
||||
if nio:
|
||||
network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)])
|
||||
if isinstance(nio, NIOUDP):
|
||||
network_options.extend(["-net", "udp,vlan={},name=gns3-{},sport={},dport={},daddr={}".format(adapter_number,
|
||||
adapter_number,
|
||||
nio.lport,
|
||||
nio.rport,
|
||||
nio.rhost)])
|
||||
else:
|
||||
elif isinstance(nio, NIOTAP):
|
||||
network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)])
|
||||
elif isinstance(nio, NIONAT):
|
||||
network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_number, adapter_number)])
|
||||
else:
|
||||
network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)])
|
||||
|
||||
else:
|
||||
# newer QEMU networking syntax
|
||||
if nio:
|
||||
network_options.extend(["-device", "{},mac={},netdev=gns3-{}".format(self._adapter_type, mac, adapter_number)])
|
||||
if isinstance(nio, NIOUDP):
|
||||
network_options.extend(["-netdev", "socket,id=gns3-{},udp={}:{},localaddr={}:{}".format(adapter_number,
|
||||
nio.rhost,
|
||||
nio.rport,
|
||||
self._host,
|
||||
nio.lport)])
|
||||
elif isinstance(nio, NIOTAP):
|
||||
if self._legacy_networking:
|
||||
network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)])
|
||||
else:
|
||||
elif isinstance(nio, NIOTAP):
|
||||
network_options.extend(["-netdev", "tap,id=gns3-{},ifname={}".format(adapter_number, nio.tap_device)])
|
||||
else:
|
||||
if self._legacy_networking:
|
||||
network_options.extend(["-net", "user,vlan={},name=gns3-{}".format(adapter_number, adapter_number)])
|
||||
elif isinstance(nio, NIONAT):
|
||||
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_number)])
|
||||
else:
|
||||
network_options.extend(["-netdev", "user,id=gns3-{}".format(adapter_number)])
|
||||
adapter_number += 1
|
||||
network_options.extend(["-device", "{},mac={}".format(self._adapter_type, mac)])
|
||||
|
||||
return network_options
|
||||
|
||||
|
@ -122,7 +122,12 @@ class VirtualBox(BaseManager):
|
||||
"""
|
||||
|
||||
hdds = []
|
||||
properties = yield from self.execute("list", ["hdds"])
|
||||
try:
|
||||
properties = yield from self.execute("list", ["hdds"])
|
||||
# If VirtualBox is not available we have no inaccessible hdd
|
||||
except VirtualBoxError:
|
||||
return hdds
|
||||
|
||||
flag_inaccessible = False
|
||||
for prop in properties:
|
||||
try:
|
||||
@ -151,7 +156,7 @@ class VirtualBox(BaseManager):
|
||||
try:
|
||||
yield from self.execute("closemedium", ["disk", hdd_file])
|
||||
except VirtualBoxError as e:
|
||||
log.warning("Could not close VirtualBox VM disk file {}: {error}".format(os.path.basename(hdd_file), e))
|
||||
log.warning("Could not close VirtualBox VM disk file {}: {}".format(os.path.basename(hdd_file), e))
|
||||
continue
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -163,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>":
|
||||
|
@ -31,6 +31,7 @@ import asyncio
|
||||
from pkg_resources import parse_version
|
||||
from .virtualbox_error import VirtualBoxError
|
||||
from ..nios.nio_udp import NIOUDP
|
||||
from ..nios.nio_nat import NIONAT
|
||||
from ..adapters.ethernet_adapter import EthernetAdapter
|
||||
from .telnet_server import TelnetServer # TODO: port TelnetServer to asyncio
|
||||
from ..base_vm import BaseVM
|
||||
@ -659,12 +660,12 @@ class VirtualBoxVM(BaseVM):
|
||||
yield from self._modify_vm("--cableconnected{} off".format(adapter_number + 1))
|
||||
nio = self._ethernet_adapters[adapter_number].get_nio(0)
|
||||
if nio:
|
||||
if not self._use_any_adapter and attachment not in ("none", "null", "generic"):
|
||||
if not isinstance(nio, NIONAT) and not self._use_any_adapter and attachment not in ("none", "null", "generic"):
|
||||
raise VirtualBoxError("Attachment ({}) already configured on adapter {}. "
|
||||
"Please set it to 'Not attached' to allow GNS3 to use it.".format(attachment,
|
||||
adapter_number + 1))
|
||||
yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1))
|
||||
|
||||
yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1))
|
||||
vbox_adapter_type = "82540EM"
|
||||
if self._adapter_type == "PCnet-PCI II (Am79C970A)":
|
||||
vbox_adapter_type = "Am79C970A"
|
||||
@ -681,13 +682,17 @@ class VirtualBoxVM(BaseVM):
|
||||
args = [self._vmname, "--nictype{}".format(adapter_number + 1), vbox_adapter_type]
|
||||
yield from self.manager.execute("modifyvm", args)
|
||||
|
||||
log.debug("setting UDP params on adapter {}".format(adapter_number))
|
||||
yield from self._modify_vm("--nic{} generic".format(adapter_number + 1))
|
||||
yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1))
|
||||
yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
|
||||
yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
|
||||
yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
|
||||
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
|
||||
if isinstance(nio, NIOUDP):
|
||||
log.debug("setting UDP params on adapter {}".format(adapter_number))
|
||||
yield from self._modify_vm("--nic{} generic".format(adapter_number + 1))
|
||||
yield from self._modify_vm("--nicgenericdrv{} UDPTunnel".format(adapter_number + 1))
|
||||
yield from self._modify_vm("--nicproperty{} sport={}".format(adapter_number + 1, nio.lport))
|
||||
yield from self._modify_vm("--nicproperty{} dest={}".format(adapter_number + 1, nio.rhost))
|
||||
yield from self._modify_vm("--nicproperty{} dport={}".format(adapter_number + 1, nio.rport))
|
||||
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
|
||||
elif isinstance(nio, NIONAT):
|
||||
yield from self._modify_vm("--nic{} nat".format(adapter_number + 1))
|
||||
yield from self._modify_vm("--cableconnected{} on".format(adapter_number + 1))
|
||||
|
||||
if nio.capturing:
|
||||
yield from self._modify_vm("--nictrace{} on".format(adapter_number + 1))
|
||||
@ -794,18 +799,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,
|
||||
@ -825,7 +834,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))
|
||||
|
||||
@ -846,6 +855,7 @@ class VirtualBoxVM(BaseVM):
|
||||
adapter_number=adapter_number))
|
||||
return nio
|
||||
|
||||
@asyncio.coroutine
|
||||
def start_capture(self, adapter_number, output_file):
|
||||
"""
|
||||
Starts a packet capture.
|
||||
@ -856,11 +866,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))
|
||||
|
||||
@ -878,11 +896,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,
|
||||
|
@ -181,15 +181,15 @@ class VPCSVM(BaseVM):
|
||||
"""
|
||||
|
||||
try:
|
||||
script_file = os.path.join(self.working_dir, 'startup.vpc')
|
||||
with open(script_file, "wb+") as f:
|
||||
startup_script_path = os.path.join(self.working_dir, 'startup.vpc')
|
||||
with open(startup_script_path, "w+", encoding='utf-8') as f:
|
||||
if startup_script is None:
|
||||
f.write(b'')
|
||||
f.write('')
|
||||
else:
|
||||
startup_script = startup_script.replace("%h", self._name)
|
||||
f.write(startup_script.encode("utf-8"))
|
||||
f.write(startup_script)
|
||||
except OSError as e:
|
||||
raise VPCSError('Cannot write the startup script file "{}": {}'.format(self.script_file, e))
|
||||
raise VPCSError('Cannot write the startup script file "{}": {}'.format(startup_script_path, e))
|
||||
|
||||
@asyncio.coroutine
|
||||
def _check_vpcs_version(self):
|
||||
@ -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"},
|
||||
|
@ -745,5 +745,4 @@ VM_CONFIGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"required": ["startup_config_content", "private_config_content"]
|
||||
}
|
||||
|
@ -78,6 +78,16 @@ 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": {
|
||||
@ -148,6 +158,7 @@ NIO_SCHEMA = {
|
||||
{"$ref": "#/definitions/UDP"},
|
||||
{"$ref": "#/definitions/Ethernet"},
|
||||
{"$ref": "#/definitions/LinuxEthernet"},
|
||||
{"$ref": "#/definitions/NAT"},
|
||||
{"$ref": "#/definitions/TAP"},
|
||||
{"$ref": "#/definitions/UNIX"},
|
||||
{"$ref": "#/definitions/VDE"},
|
||||
|
@ -27,6 +27,7 @@ import aiohttp
|
||||
import functools
|
||||
import types
|
||||
import time
|
||||
import atexit
|
||||
|
||||
from .web.route import Route
|
||||
from .web.request_handler import RequestHandler
|
||||
@ -173,6 +174,18 @@ class Server:
|
||||
return
|
||||
yield from embed(globals(), locals(), return_asyncio_coroutine=True, patch_stdout=True)
|
||||
|
||||
def _exit_handling(self):
|
||||
def close_asyncio_loop():
|
||||
loop = None
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except AttributeError:
|
||||
pass
|
||||
if loop is not None:
|
||||
loop.close()
|
||||
|
||||
atexit.register(close_asyncio_loop)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Starts the server.
|
||||
@ -216,6 +229,8 @@ class Server:
|
||||
self._loop.run_until_complete(self._run_application(self._handler, ssl_context))
|
||||
self._signal_handling()
|
||||
|
||||
self._exit_handling()
|
||||
|
||||
if server_config.getboolean("live"):
|
||||
log.info("Code live reload is enabled, watching for file changes")
|
||||
self._loop.call_later(1, self._reload_hook)
|
||||
@ -225,11 +240,6 @@ class Server:
|
||||
|
||||
try:
|
||||
self._loop.run_forever()
|
||||
except OSError as e:
|
||||
# This is to ignore OSError: [WinError 0] The operation completed successfully
|
||||
# exception on Windows.
|
||||
if not sys.platform.startswith("win") and not e.winerror == 0:
|
||||
raise
|
||||
except TypeError as e:
|
||||
# This is to ignore an asyncio.windows_events exception
|
||||
# on Windows when the process gets the SIGBREAK signal
|
||||
|
@ -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
|
||||
|
41
gns3server/utils/get_resource.py
Normal file
41
gns3server/utils/get_resource.py
Normal file
@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2015 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/>.
|
||||
|
||||
import tempfile
|
||||
import pkg_resources
|
||||
import atexit
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
egg_cache_dir = tempfile.mkdtemp()
|
||||
pkg_resources.set_extraction_path(egg_cache_dir)
|
||||
except ValueError:
|
||||
# If the path is already set the module throw an error
|
||||
pass
|
||||
|
||||
|
||||
@atexit.register
|
||||
def clean_egg_cache():
|
||||
try:
|
||||
import shutil
|
||||
log.debug("Clean egg cache %s", egg_cache_dir)
|
||||
shutil.rmtree(egg_cache_dir)
|
||||
except Exception:
|
||||
# We don't care if we can not cleanup
|
||||
pass
|
@ -23,5 +23,5 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "1.3.2"
|
||||
__version_info__ = (1, 3, 2, 0)
|
||||
__version__ = "1.3.6"
|
||||
__version_info__ = (1, 3, 6, 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
|
||||
@ -74,6 +74,27 @@ class ColouredStreamHandler(logging.StreamHandler):
|
||||
stream.write(msg)
|
||||
stream.write(self.terminator)
|
||||
self.flush()
|
||||
# On OSX when frozen flush raise a BrokenPipeError
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
except Exception:
|
||||
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)
|
||||
|
||||
@ -83,7 +104,7 @@ def init_logger(level, logfile=None, quiet=False):
|
||||
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)
|
||||
|
@ -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:
|
||||
@ -161,6 +192,11 @@ class Route(object):
|
||||
response = Response(request=request, route=route)
|
||||
response.set_status(408)
|
||||
response.json({"message": "Client disconnected", "status": 408})
|
||||
except ConnectionResetError:
|
||||
log.error("Client connection reset")
|
||||
response = Response(request=request, route=route)
|
||||
response.set_status(408)
|
||||
response.json({"message": "Connection reset", "status": 408})
|
||||
except Exception as e:
|
||||
log.error("Uncaught exception detected: {type}".format(type=type(e)), exc_info=1)
|
||||
response = Response(request=request, route=route)
|
||||
|
@ -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
|
||||
|
4
setup.py
4
setup.py
@ -34,7 +34,9 @@ class PyTest(TestCommand):
|
||||
sys.exit(errcode)
|
||||
|
||||
|
||||
dependencies = ["aiohttp>=0.14.4",
|
||||
dependencies = [
|
||||
#"gns3-netifaces>=0.10.4.1",
|
||||
"aiohttp>=0.14.4",
|
||||
"jsonschema>=2.4.0",
|
||||
"Jinja2>=2.7.3",
|
||||
"raven>=5.2.0"]
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
import aiohttp
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from gns3server.config import Config
|
||||
|
||||
def test_index_upload(server):
|
||||
response = server.get('/upload', api_version=None)
|
||||
@ -37,8 +37,8 @@ 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"
|
||||
|
@ -260,24 +260,24 @@ def test_control_vm_expect_text(vm, loop, running_subprocess_mock):
|
||||
def test_build_command(vm, loop, fake_qemu_binary, port_manager):
|
||||
|
||||
os.environ["DISPLAY"] = "0:0"
|
||||
with patch("gns3server.modules.qemu.qemu_vm.QemuVM._get_random_mac", return_value="00:00:ab:7e:b5:00"):
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
|
||||
cmd = loop.run_until_complete(asyncio.async(vm._build_command()))
|
||||
assert cmd == [
|
||||
fake_qemu_binary,
|
||||
"-name",
|
||||
"test",
|
||||
"-m",
|
||||
"256",
|
||||
"-hda",
|
||||
os.path.join(vm.working_dir, "flash.qcow2"),
|
||||
"-serial",
|
||||
"telnet:127.0.0.1:{},server,nowait".format(vm.console),
|
||||
"-device",
|
||||
"e1000,mac=00:00:ab:7e:b5:00,netdev=gns3-0",
|
||||
"-netdev",
|
||||
"user,id=gns3-0"
|
||||
]
|
||||
with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process:
|
||||
cmd = loop.run_until_complete(asyncio.async(vm._build_command()))
|
||||
assert cmd == [
|
||||
fake_qemu_binary,
|
||||
"-name",
|
||||
"test",
|
||||
"-m",
|
||||
"256",
|
||||
"-hda",
|
||||
os.path.join(vm.working_dir, "flash.qcow2"),
|
||||
"-serial",
|
||||
"telnet:127.0.0.1:{},server,nowait".format(vm.console),
|
||||
"-net",
|
||||
"none",
|
||||
"-device",
|
||||
"e1000,mac=00:00:ab:0e:0f:00"
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform.startswith("win"), reason="Not supported on Windows")
|
||||
def test_build_command_without_display(vm, loop, fake_qemu_binary):
|
||||
@ -314,6 +314,7 @@ def test_hdb_disk_image(vm, tmpdir):
|
||||
vm.hdb_disk_image = "test"
|
||||
assert vm.hdb_disk_image == str(tmpdir / "QEMU" / "test")
|
||||
|
||||
|
||||
def test_hdc_disk_image(vm, tmpdir):
|
||||
|
||||
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
|
||||
@ -322,6 +323,7 @@ def test_hdc_disk_image(vm, tmpdir):
|
||||
vm.hdc_disk_image = "test"
|
||||
assert vm.hdc_disk_image == str(tmpdir / "QEMU" / "test")
|
||||
|
||||
|
||||
def test_hdd_disk_image(vm, tmpdir):
|
||||
|
||||
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
|
||||
|
@ -103,10 +103,16 @@ def test_get_abs_image_path(qemu, tmpdir):
|
||||
|
||||
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
|
||||
assert qemu.get_abs_image_path(path1) == path1
|
||||
assert qemu.get_abs_image_path("test1.bin") == path1
|
||||
assert qemu.get_abs_image_path(path2) == path2
|
||||
assert qemu.get_abs_image_path("test2.bin") == path2
|
||||
assert qemu.get_abs_image_path("../test1.bin") == path1
|
||||
|
||||
# We look at first in new location
|
||||
path2 = str(tmpdir / "QEMU" / "test1.bin")
|
||||
open(path2, 'w+').close()
|
||||
assert qemu.get_abs_image_path("test1.bin") == path2
|
||||
|
||||
|
||||
def test_get_relative_image_path(qemu, tmpdir):
|
||||
os.makedirs(str(tmpdir / "QEMU"))
|
||||
@ -118,6 +124,7 @@ def test_get_relative_image_path(qemu, tmpdir):
|
||||
|
||||
with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}):
|
||||
assert qemu.get_relative_image_path(path1) == path1
|
||||
assert qemu.get_relative_image_path("test1.bin") == path1
|
||||
assert qemu.get_relative_image_path(path2) == "test2.bin"
|
||||
assert qemu.get_relative_image_path("test2.bin") == "test2.bin"
|
||||
assert qemu.get_relative_image_path("../test1.bin") == path1
|
||||
|
@ -45,3 +45,13 @@ def test_release_udp_port():
|
||||
pm.reserve_udp_port(4242, project)
|
||||
pm.release_udp_port(4242, project)
|
||||
pm.reserve_udp_port(4242, project)
|
||||
|
||||
|
||||
def test_find_unused_port():
|
||||
p = PortManager().find_unused_port(1000, 10000)
|
||||
assert p is not None
|
||||
|
||||
|
||||
def test_find_unused_port_invalid_range():
|
||||
with pytest.raises(aiohttp.web.HTTPConflict):
|
||||
p = PortManager().find_unused_port(10000, 1000)
|
||||
|
@ -69,15 +69,10 @@ def test_changing_path_temporary_flag(tmpdir):
|
||||
with patch("gns3server.modules.project.Project.is_local", return_value=True):
|
||||
p = Project(temporary=True)
|
||||
assert os.path.exists(p.path)
|
||||
original_path = p.path
|
||||
assert os.path.exists(os.path.join(p.path, ".gns3_temporary"))
|
||||
p.temporary = False
|
||||
assert not os.path.exists(os.path.join(p.path, ".gns3_temporary"))
|
||||
|
||||
with open(str(tmpdir / ".gns3_temporary"), "w+") as f:
|
||||
f.write("1")
|
||||
|
||||
p.path = str(tmpdir)
|
||||
assert not os.path.exists(os.path.join(str(tmpdir), ".gns3_temporary"))
|
||||
|
||||
|
||||
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):
|
||||
|
24
tests/utils/test_interfaces.py
Normal file
24
tests/utils/test_interfaces.py
Normal file
@ -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