Compare commits

..
56 Commits
Author SHA1 Message Date
Julien Duponchelle 078b72cafd Version 1.3.3 2015-05-14 18:57:30 +02:00
Jeremy b2457e0b3b Check for empty iourc path. 2015-05-13 16:05:54 -06:00
Jeremy 2531a05adc Merge remote-tracking branch 'origin/master' 2015-05-13 15:54:03 -06:00
Jeremy dd9f62158f Fixes bugs with IOS router configs. Fixes #354. 2015-05-13 15:53:58 -06:00
Julien Duponchelle a3c0f0754e Fix crash 2015-05-13 23:27:51 +02:00
Julien Duponchelle 157bc18ebd Use a temporary directory as egg cache
We have use with broken permission on their
system. We try to workaround the issue.

Fix #182
2015-05-13 14:29:03 +02:00
Julien Duponchelle 3704911c2d Fix tests 2015-05-13 10:19:50 +02:00
Julien Duponchelle 1e38b11f34 Catch crash error in IOU in case of permission denied
Fix #186
2015-05-13 10:16:24 +02:00
Jeremy bebdadc465 Bump version to 1.3.3.dev3 2015-05-07 11:52:17 -06:00
Julien Duponchelle b0ce091a4c 1.3.3rc1 2015-05-07 16:03:51 +02:00
Jeremy d21469a916 Return an error if an adapter slot doesn't exist on an IOS router. 2015-05-06 17:22:07 -06:00
Jeremy b57a023394 NIO NAT support for VirtualBox VMs. 2015-05-06 15:21:39 -06:00
grossmj a929dfea38 Merge remote-tracking branch 'origin/master' 2015-05-06 14:59:35 -06:00
grossmj fcff2d0813 NIO NAT support for QEMU VMs (user mode back-end is used). 2015-05-06 14:59:01 -06:00
Julien Duponchelle ed39afbf3d Throw an error if user put an invalid port range in config file
Fix #117
2015-05-06 10:40:51 +02:00
Julien Duponchelle 3ba4789ba6 New crash report key
Fix #180
2015-05-06 09:55:14 +02:00
grossmj 17b93e6a89 Bump version to 1.3.3dev2 2015-05-05 14:49:03 -06:00
Julien Duponchelle 4b21135ba7 Turn off configuration parser interpolation 2015-05-05 11:53:33 +02:00
Julien Duponchelle a3f00e1f45 Catch configuration file parsing errors
Fix #176
2015-05-05 11:44:35 +02:00
Julien Duponchelle b7dac1bec4 Force closing the event loop to avoid warning with Python 3.4.3
Fix #177
2015-05-05 11:33:47 +02:00
Julien Duponchelle 18c4154376 Catch error when you can't mark a project as no longer temporary
Fix #172
2015-05-05 10:51:51 +02:00
Julien Duponchelle 22efc7488f Catch BrokenPipeError for OSX frozen server
Fix #166
2015-05-05 10:46:09 +02:00
grossmj 9eeb8910fb Match how IOU initial-config is set for VPCS VM. 2015-05-04 21:54:56 -06:00
Jeremy 71e2586e17 Refactors how startup-config and private-config are handled for IOS routers. 2015-05-04 18:42:32 -06:00
Julien Duponchelle ee2dada88b Fix tests 2015-05-04 21:29:28 +02:00
Jeremy c4054cf810 Catch the "WinError 0 The operation completed successfully" exception at a higher level. 2015-05-04 12:14:04 -06:00
Julien Duponchelle d2d91ebdea Fix temporary project not cleanup with save as 2015-05-04 14:04:57 +02:00
Julien Duponchelle 0dea63c9ea If image is not found in VM directory look in images folder 2015-05-04 10:57:08 +02:00
grossmj 3467b42ab5 Ordered MAC addresses for QEMU based VMs. 2015-05-03 13:18:18 -06:00
grossmj 65103e9332 Merge remote-tracking branch 'origin/master' 2015-05-03 11:41:09 -06:00
grossmj f6bc823b58 Fixes #171. 2015-05-03 11:40:55 -06:00
Julien Duponchelle 151788e48a Force utf-8 configuraton files reading
Fix #170
2015-05-01 17:49:16 +02:00
Julien Duponchelle 6b70fa9794 Do not list file starting with a . in upload handler 2015-05-01 10:55:08 +02:00
Julien Duponchelle 359abb0286 Revert "Merge branch 'unstable'"
This reverts commit 929c337e8b, reversing
changes made to b9bc73fd01.
2015-04-30 16:43:30 +02:00
grossmj d18293ae7c Fixes list images in VirtualBox. 2015-04-29 22:17:17 -06:00
Jeremy 929c337e8b Merge branch 'unstable'
Conflicts:
	gns3server/modules/virtualbox/__init__.py
	gns3server/version.py
	tests/modules/test_manager.py
2015-04-29 16:29:45 -06:00
Julien Duponchelle b9bc73fd01 Do not crash when closing a project if VirtualBox is not accessible
Fix #164
2015-04-29 14:24:27 +02:00
Julien Duponchelle e75fbc9d73 Catch connection reset errors
Fix #162
2015-04-29 11:15:32 +02:00
grossmj 0311a0086e Fixes typo. 2015-04-28 22:16:15 -06:00
Julien Duponchelle 461e3ce53f 1.3.3dev1 2015-04-28 21:49:48 +02:00
Jeremy Grossmann ee1e5b8204 Merge pull request #156 from GNS3/daemon
Server daemonization support.
2015-04-24 16:38:59 -06:00
Jeremy 8f6e5b4ad8 Merge branch 'unstable' into daemon
Conflicts:
	gns3server/main.py
2015-04-24 16:37:56 -06:00
Jeremy Grossmann 14cc01bb8b Merge pull request #152 from GNS3/api_list_images
API in order to get the list of IOU, Dynamips, Qemu images.
2015-04-23 17:20:15 -06:00
Jeremy 40ce22222e Merge branch 'unstable' into api_list_images
Conflicts:
	gns3server/handlers/api/iou_handler.py
2015-04-23 17:19:37 -06:00
Jeremy Grossmann 7e991cc404 Merge pull request #154 from GNS3/wireshark_remote_capture
Support for Wireshark remote packet captures.
2015-04-23 16:17:44 -06:00
Julien Duponchelle 83f2509cfe Cleanup exceptions 2015-04-23 16:56:46 +02:00
Julien Duponchelle fac0f5ecd9 Proper daemon support 2015-04-23 11:10:02 +02:00
Julien Duponchelle 2e39265da1 Merge branch 'master' into unstable 2015-04-22 17:34:17 +02:00
Julien Duponchelle bf618d321c Max 100 thread executor 2015-04-22 10:39:43 +02:00
Julien Duponchelle 8b879c0614 Support wireshark remote capture 2015-04-20 19:27:07 +02:00
Julien Duponchelle cf0adf56a8 Merge branch 'master' into unstable 2015-04-20 19:26:27 +02:00
Julien Duponchelle dfdc18b20c Merge branch 'master' into unstable 2015-04-16 18:38:59 +02:00
Julien Duponchelle bca90bc563 API in order to get the list of IOU, Dynamips, Qemu images 2015-04-16 18:32:12 +02:00
Julien Duponchelle 5a4ffae6a2 Merge branch 'master' into unstable 2015-04-15 16:44:09 +02:00
Julien Duponchelle 789e24795e Merge branch 'master' into unstable 2015-04-15 16:29:54 +02:00
Julien Duponchelle 1d5dc2ecf0 1.4.0 dev1 2015-04-13 10:48:14 +02:00
32 changed files with 394 additions and 125 deletions

No files matched your search

+31
View File
@@ -1,5 +1,36 @@
# Change Log
## 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 -2
View File
@@ -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:
+1 -1
View File
@@ -40,7 +40,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "sync+https://22979234ab4749ceabce08e6da4c1476:1432c8c7a43d410b9b5bb33f8e55b2a6@app.getsentry.com/38482"
DSN = "sync+https://45147533567b4d529ca09c093758681f:12d8b456cdb34d23aba771325aa64ee6@app.getsentry.com/38482"
if hasattr(sys, "frozen"):
cacert = os.path.join(os.getcwd(), "cacert.pem")
if os.path.isfile(cacert):
+31 -2
View File
@@ -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)
+2 -1
View File
@@ -81,13 +81,14 @@ 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:
project.path = project_path
for module in MODULES:
yield from module.instance().project_moved(project)
# Very important we need to remove temporary flag after moving the project
project.temporary = request.json.get("temporary", project.temporary)
response.json(project)
@classmethod
+1 -1
View File
@@ -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)
+3 -2
View File
@@ -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")
+14
View File
@@ -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
@@ -181,6 +191,10 @@ def main():
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()
+13 -1
View File
@@ -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):
+42 -30
View File
@@ -410,6 +410,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 +473,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 +534,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 +570,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()))
+11 -4
View File
@@ -1436,6 +1436,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 +1456,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):
"""
+6 -3
View File
@@ -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
View 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"}
+1 -1
View File
@@ -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
+16 -1
View File
@@ -139,9 +139,21 @@ 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()
# The order of operation is important because we want to avoid losing
# data
if old_path:
try:
shutil.rmtree(old_path)
except OSError as e:
raise aiohttp.web.HTTPConflict(text="Can't remove temporary directory {}: {}".format(old_path, e))
@property
def name(self):
@@ -228,7 +240,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):
"""
+26 -25
View File
@@ -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
@@ -981,46 +981,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
+7 -2
View File
@@ -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
+14 -9
View File
@@ -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))
+5 -5
View File
@@ -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):
-1
View File
@@ -745,5 +745,4 @@ VM_CONFIGS_SCHEMA = {
},
},
"additionalProperties": False,
"required": ["startup_config_content", "private_config_content"]
}
+11
View File
@@ -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"},
+15 -5
View File
@@ -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
+41
View 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
+2 -2
View File
@@ -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.3"
__version_info__ = (1, 3, 3, 0)
+3
View File
@@ -74,6 +74,9 @@ 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)
+5
View File
@@ -161,6 +161,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)
+20 -18
View File
@@ -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)}):
+7
View File
@@ -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
+10
View File
@@ -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)
+4 -5
View File
@@ -69,15 +69,14 @@ 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)
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():