mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-06-02 08:42:04 +03:00
Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b7a2cda15 | ||
|
|
d0d60d26da | ||
|
|
b2c14c1218 | ||
|
|
2d59dc2c72 | ||
|
|
bb48cdf0a0 | ||
|
|
e00dcbcd92 | ||
|
|
5ab7d6e94e | ||
|
|
d1da9adc88 | ||
|
|
1455babd62 | ||
|
|
fc8529323c | ||
|
|
f9130794ee | ||
|
|
e62d6c0edd | ||
|
|
8d1dc4b090 | ||
|
|
acf6cf6ea2 | ||
|
|
d9ee44f90a | ||
|
|
bd6da5db9a | ||
|
|
4b3ade9b48 | ||
|
|
9daed7e0d4 | ||
|
|
4ef61e7af1 | ||
|
|
851f2d0517 | ||
|
|
5e5e04de8e | ||
|
|
e8b2f952af | ||
|
|
85b5d10e5a | ||
|
|
b916ca7bfb | ||
|
|
46190154f6 | ||
|
|
194552d2d3 | ||
|
|
a736cbc4d5 | ||
|
|
0310ecdfd0 | ||
|
|
28bbc8bbe9 | ||
|
|
f36ef66623 | ||
|
|
16ba51aa8c | ||
|
|
52847d1fad | ||
|
|
4733cc8a3e | ||
|
|
aecf61135f | ||
|
|
252d86eb70 | ||
|
|
438b0fe9d3 | ||
|
|
f6c58b5a28 | ||
|
|
dee2f94c38 | ||
|
|
1fc4dec5ca | ||
|
|
d7ab12bc61 | ||
|
|
1f50612a16 | ||
|
|
43715f1e34 | ||
|
|
1bf0ff69d8 | ||
|
|
abf992dfb8 | ||
|
|
707dfee696 | ||
|
|
e8c4f059c0 | ||
|
|
1a6f80f2df | ||
|
|
47ae310ac7 | ||
|
|
923c61f9c7 | ||
|
|
18b8c558cd | ||
|
|
84a091e380 | ||
|
|
1399098e30 | ||
|
|
593f8add5d | ||
|
|
2f26624f29 | ||
|
|
14c901d219 | ||
|
|
2b2e45ca45 | ||
|
|
868e9a322e | ||
|
|
d2f3f58de1 | ||
|
|
a69b3fcd11 | ||
|
|
a3505ee7f2 | ||
|
|
efe5e0e2c4 | ||
|
|
8745527c5d | ||
|
|
78b40df71e | ||
|
|
9675619ab9 | ||
|
|
68ca2c2be6 | ||
|
|
835ecbb410 | ||
|
|
22756a3c13 | ||
|
|
3c2fb04ed8 | ||
|
|
db5fb840a6 | ||
|
|
65b05d707b | ||
|
|
1b972df7e2 | ||
|
|
40266c275d | ||
|
|
197db35c80 | ||
|
|
8771e1ace7 | ||
|
|
e6e1275ad2 | ||
|
|
417dc0859b | ||
|
|
806e1f29bb | ||
|
|
9724f5769d | ||
|
|
5a0c8914c4 | ||
|
|
c2e0a30da8 | ||
|
|
45ae4f20a0 | ||
|
|
398826d7ef | ||
|
|
14e5f3bf74 | ||
|
|
57b1cbf41b | ||
|
|
51a0d88b9b | ||
|
|
16d4d7d1ea | ||
|
|
2affb1513d | ||
|
|
214d4b2a9e | ||
|
|
2f486d979b | ||
|
|
023c5fb99a | ||
|
|
329ed371f9 | ||
|
|
0577bfbde3 | ||
|
|
3ce5c35143 | ||
|
|
9258ef4bb3 | ||
|
|
e02facc170 | ||
|
|
4dc76926ea | ||
|
|
ba6be7e987 | ||
|
|
6b2da1ec97 | ||
|
|
6aa1b515c7 | ||
|
|
5d64ec1c8f | ||
|
|
e1b81fb931 | ||
|
|
c91752598c | ||
|
|
708515925f | ||
|
|
a61745f868 | ||
|
|
2ca7af34df | ||
|
|
bd7e6f4e3e | ||
|
|
32e86d461e | ||
|
|
66ea5ad979 | ||
|
|
ebfa80d444 | ||
|
|
4247cc819d | ||
|
|
1f9f5bd734 | ||
|
|
426d791eea | ||
|
|
30d99b812c | ||
|
|
981e55bc4d | ||
|
|
bab1090a25 | ||
|
|
84b5181fd8 | ||
|
|
222ebb58c0 | ||
|
|
319ac95f8a | ||
|
|
0370d3084f | ||
|
|
30cd030943 | ||
|
|
ee98a78264 | ||
|
|
2abf316192 | ||
|
|
25d1abcd6c | ||
|
|
3b823d9696 | ||
|
|
7e13c2c67a | ||
|
|
f5ad3a6a2e | ||
|
|
422af60827 | ||
|
|
9127321540 | ||
|
|
ad57e3e9b6 | ||
|
|
ceacb5aceb | ||
|
|
dcae059480 | ||
|
|
6d6603e013 | ||
|
|
90fe8078c3 | ||
|
|
367fb32114 | ||
|
|
b2b82afd71 | ||
|
|
dc9f012d25 | ||
|
|
a21a397397 | ||
|
|
fa86a04acc | ||
|
|
4cf3f595d6 | ||
|
|
e619e56537 | ||
|
|
0c9c81e9dc | ||
|
|
3b5184b007 | ||
|
|
0c6bfdafc3 | ||
|
|
f64838ac59 | ||
|
|
53cfec5ce0 | ||
|
|
c9dfd99697 | ||
|
|
3b29e898e4 | ||
|
|
e89ad3ec4f | ||
|
|
c2a193cb8a | ||
|
|
26b668099c | ||
|
|
f67f0f3768 | ||
|
|
860873ac21 | ||
|
|
0472e4fddd | ||
|
|
4aa8f6f49a | ||
|
|
4de3c78cfe | ||
|
|
7df9316539 | ||
|
|
c2b7a8e86f | ||
|
|
144576ee86 | ||
|
|
cabd57e5ab | ||
|
|
06c2d8d3e6 | ||
|
|
f0845e2fff | ||
|
|
e48d9d76b4 | ||
|
|
b250af6f26 | ||
|
|
a7688cec8b | ||
|
|
ec07873990 | ||
|
|
75cb485e6f | ||
|
|
4a7cc14270 | ||
|
|
eca99ccd2c | ||
|
|
dd03139706 | ||
|
|
8ad391d2ab | ||
|
|
f7e53f685b | ||
|
|
0698b3942b | ||
|
|
d866cedbca | ||
|
|
6105a1629f | ||
|
|
8a49d90400 | ||
|
|
10dfd59203 | ||
|
|
afa91ef2f3 | ||
|
|
862d97b0a4 | ||
|
|
05a437e2fc | ||
|
|
6b679fc89e | ||
|
|
9f3d831d4c | ||
|
|
1403c35443 | ||
|
|
d440d10371 | ||
|
|
58a218f826 | ||
|
|
0756f9104e | ||
|
|
2847296cd4 | ||
|
|
61317fced0 | ||
|
|
767d756ed4 | ||
|
|
43f1c04d3e | ||
|
|
c985298c9a | ||
|
|
8e9574871b | ||
|
|
3536c0fc19 | ||
|
|
d196db8303 | ||
|
|
18a4eeb6e9 | ||
|
|
cb29466e43 | ||
|
|
58ad493e9e | ||
|
|
36b8092e53 | ||
|
|
855b9901c7 | ||
|
|
f0f110b277 | ||
|
|
f0e51c59ff | ||
|
|
37b1c839e9 | ||
|
|
c4226315a5 | ||
|
|
9fbf593db8 | ||
|
|
79feec83fb | ||
|
|
b3b934847d | ||
|
|
9d71161a32 | ||
|
|
7485fb968a | ||
|
|
4a39aad83d | ||
|
|
56faee748e | ||
|
|
6577340e05 | ||
|
|
d5eef5a5f6 | ||
|
|
3347180a5a | ||
|
|
c4396fcac0 | ||
|
|
418b912379 | ||
|
|
7b6e32373a | ||
|
|
c54de58a0d | ||
|
|
7c21c7b29f | ||
|
|
57ee86d251 | ||
|
|
7c94595313 | ||
|
|
2bbfefb5e1 | ||
|
|
e9850ee962 | ||
|
|
4828b3a73b | ||
|
|
de5e43578a | ||
|
|
15380ae80a | ||
|
|
e39c1f9579 | ||
|
|
c0a7ed7a2c | ||
|
|
e2be6bd0a9 | ||
|
|
6513acf141 | ||
|
|
1c4ce90093 | ||
|
|
adace0d6a1 | ||
|
|
a960ee05ce | ||
|
|
0b46653c79 | ||
|
|
120cb89526 | ||
|
|
72c48968df | ||
|
|
2195a6199c | ||
|
|
509d5f8b82 | ||
|
|
35fe0514ba | ||
|
|
98fd7b73b2 | ||
|
|
00dfddde8a | ||
|
|
9d0bd31eea | ||
|
|
b4e42f50ec | ||
|
|
c8ef818e1c | ||
|
|
d59a2bd7f9 | ||
|
|
e6cb49a19b | ||
|
|
fa8c166c18 | ||
|
|
5ad6433e91 | ||
|
|
ff4af1e22d | ||
|
|
8a02288e28 | ||
|
|
bf1a797519 | ||
|
|
fddbe69ba0 | ||
|
|
1a154df621 | ||
|
|
e527b13930 | ||
|
|
aa3ec13e45 | ||
|
|
b7d7706682 | ||
|
|
20bb456fa8 | ||
|
|
865ae8445d |
39
.travis.yml
39
.travis.yml
@@ -4,15 +4,32 @@ python:
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
|
||||
install:
|
||||
- "pip install -r requirements.txt --use-mirrors"
|
||||
- "pip install tox"
|
||||
cache:
|
||||
directories:
|
||||
- build
|
||||
|
||||
script: "python setup.py test"
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
before_install:
|
||||
- mkdir -p build
|
||||
- sudo apt-get install -qqm wget build-essential python3-dev
|
||||
- sudo apt-get install -qqm python3-pyqt4 python3-sip-dev
|
||||
# install SIP
|
||||
- cd build
|
||||
- wget --quiet --output-document=sip.tar.gz http://downloads.sourceforge.net/project/pyqt/sip/sip-4.15.3/sip-4.15.3.tar.gz
|
||||
- tar -xf sip.tar.gz
|
||||
- cd sip-4.15.3
|
||||
- python -B configure.py
|
||||
- make
|
||||
- sudo make install
|
||||
# install PyQt
|
||||
- cd $TRAVIS_BUILD_DIR/build
|
||||
- wget --quiet --output-document=pyqt.tar.gz http://downloads.sourceforge.net/project/pyqt/PyQt4/PyQt-4.10.3/PyQt-x11-gpl-4.10.3.tar.gz
|
||||
- tar -xf pyqt.tar.gz
|
||||
- cd PyQt-x11-gpl-4.10.3
|
||||
- python -B configure.py --confirm-license
|
||||
- make
|
||||
- sudo make install
|
||||
- python -c 'import PyQt4' # Check if it's ok
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
@@ -22,3 +39,9 @@ notifications:
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
||||
install:
|
||||
- "pip install -r dev-requirements.txt"
|
||||
|
||||
script: "xvfb-run py.test --verbose" # Run tests in a fake X server
|
||||
|
||||
|
||||
|
||||
31
README.rst
31
README.rst
@@ -1,20 +1,31 @@
|
||||
GNS3-gui
|
||||
========
|
||||
|
||||
New GNS3 GUI repository (beta stage).
|
||||
GNS3 GUI repository (beta stage).
|
||||
|
||||
Warning: this is not the repository for the stable version of GNS3 (0.8.6), please go to the gns3-legacy repository for it.
|
||||
Linux (Debian based)
|
||||
--------------------
|
||||
|
||||
Linux/Unix
|
||||
----------
|
||||
The following instructions have been tested with Ubuntu and Mint.
|
||||
You must be connected to the Internet in order to install the dependencies.
|
||||
|
||||
Dependencies:
|
||||
|
||||
- Python version 3.3 or above
|
||||
- pip & setuptools must be installed, please see http://pip.readthedocs.org/en/latest/installing.html
|
||||
(or sudo apt-get install python3-pip but install more packages)
|
||||
- PyQt must be installed, to install on Debian-like Linux: sudo apt-get install python3-pyqt4
|
||||
- Dynamips version 0.2.11 or above (http://github.com/GNS3/dynamips)
|
||||
- Python 3.3 or above
|
||||
- Setuptools
|
||||
- PyQt libraries
|
||||
- Apache Libcloud library
|
||||
- Requests library
|
||||
- Paramiko library
|
||||
|
||||
The following commands will install some of these dependencies:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sudo apt-get install python3-setuptools
|
||||
sudo apt-get install python3-pyqt4
|
||||
|
||||
Finally these commands will install the GUI as well as the rest of the dependencies:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
@@ -34,7 +45,7 @@ Please use our DMG package or you can manually install using the following steps
|
||||
|
||||
`First install homebrew <http://brew.sh/>`_.
|
||||
|
||||
Then install GNS3 dependencies.
|
||||
Then install the GNS3 dependencies.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ from collections import namedtuple
|
||||
import hashlib
|
||||
import os
|
||||
import logging
|
||||
from io import StringIO
|
||||
from io import StringIO, BytesIO
|
||||
|
||||
from libcloud.compute.base import NodeAuthSSHKey
|
||||
from libcloud.storage.types import ContainerAlreadyExistsError
|
||||
from libcloud.storage.types import ContainerAlreadyExistsError, ContainerDoesNotExistError, ObjectDoesNotExistError
|
||||
|
||||
from .exceptions import ItemNotFound, KeyPairExists, MethodNotAllowed
|
||||
from .exceptions import OverLimit, BadRequest, ServiceUnavailable
|
||||
@@ -141,7 +141,7 @@ class BaseCloudCtrl(object):
|
||||
self._handle_exception(status, error_text)
|
||||
else:
|
||||
log.error("create_instance method raised an exception: {}".format(e))
|
||||
log.error('image id {}'.format(image))
|
||||
log.error('image id {}'.format(image_id))
|
||||
|
||||
def delete_instance(self, instance):
|
||||
""" Delete the specified instance. Returns True or False. """
|
||||
@@ -170,10 +170,7 @@ class BaseCloudCtrl(object):
|
||||
def list_instances(self):
|
||||
""" Return a list of instances in the current region. """
|
||||
|
||||
try:
|
||||
return self.driver.list_nodes()
|
||||
except Exception as e:
|
||||
log.error("list_instances returned an error: {}".format(e))
|
||||
return self.driver.list_nodes()
|
||||
|
||||
|
||||
def create_key_pair(self, name):
|
||||
@@ -216,11 +213,11 @@ class BaseCloudCtrl(object):
|
||||
|
||||
return self.driver.list_key_pairs()
|
||||
|
||||
def upload_file(self, file_path, folder):
|
||||
def upload_file(self, file_path, cloud_object_name):
|
||||
"""
|
||||
Uploads file to cloud storage (if it is not identical to a file already in cloud storage).
|
||||
:param file_path: path to file to upload
|
||||
:param folder: folder in cloud storage to save file in
|
||||
:param cloud_object_name: name of file saved in cloud storage
|
||||
:return: True if file was uploaded, False if it was skipped because it already existed and was identical
|
||||
"""
|
||||
try:
|
||||
@@ -231,7 +228,6 @@ class BaseCloudCtrl(object):
|
||||
with open(file_path, 'rb') as file:
|
||||
local_file_hash = hashlib.md5(file.read()).hexdigest()
|
||||
|
||||
cloud_object_name = folder + '/' + os.path.basename(file_path)
|
||||
cloud_hash_name = cloud_object_name + '.md5'
|
||||
cloud_objects = [obj.name for obj in gns3_container.list_objects()]
|
||||
|
||||
@@ -250,3 +246,93 @@ class BaseCloudCtrl(object):
|
||||
self.storage_driver.upload_object_via_stream(file, gns3_container, cloud_object_name)
|
||||
self.storage_driver.upload_object_via_stream(StringIO(local_file_hash), gns3_container, cloud_hash_name)
|
||||
return True
|
||||
|
||||
def list_projects(self):
|
||||
"""
|
||||
Lists projects in cloud storage
|
||||
:return: Dictionary where project names are keys and values are names of objects in storage
|
||||
"""
|
||||
|
||||
try:
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
projects = {
|
||||
obj.name.replace('projects/', '').replace('.zip', ''): obj.name
|
||||
for obj in gns3_container.list_objects()
|
||||
if obj.name.startswith('projects/') and obj.name[-4:] == '.zip'
|
||||
}
|
||||
return projects
|
||||
except ContainerDoesNotExistError:
|
||||
return []
|
||||
|
||||
def download_file(self, file_name, destination=None):
|
||||
"""
|
||||
Downloads file from cloud storage. If a file exists at destination, and it is identical to the file in cloud
|
||||
storage, it is not downloaded.
|
||||
:param file_name: name of file in cloud storage to download
|
||||
:param destination: local path to save file to (if None, returns file contents as a file-like object)
|
||||
:return: A file-like object if file contents are returned, or None if file is saved to filesystem
|
||||
"""
|
||||
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
storage_object = gns3_container.get_object(file_name)
|
||||
|
||||
if destination is not None:
|
||||
if os.path.isfile(destination):
|
||||
# if a file exists at destination and its hash matches that of the
|
||||
# file in cloud storage, don't download it
|
||||
with open(destination, 'rb') as f:
|
||||
local_file_hash = hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
cloud_object_hash = ''
|
||||
for chunk in hash_object.as_stream():
|
||||
cloud_object_hash += chunk.decode('utf8')
|
||||
|
||||
if local_file_hash == cloud_object_hash:
|
||||
return
|
||||
|
||||
storage_object.download(destination)
|
||||
else:
|
||||
contents = b''
|
||||
|
||||
for chunk in storage_object.as_stream():
|
||||
contents += chunk
|
||||
|
||||
return BytesIO(contents)
|
||||
|
||||
def find_storage_image_names(self, images_to_find):
|
||||
"""
|
||||
Maps names of image files to their full name in cloud storage
|
||||
:param images_to_find: list of image names to find
|
||||
:return: A dictionary where keys are image names, and values are the corresponding names of
|
||||
the files in cloud storage
|
||||
"""
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
images_in_storage = [obj.name for obj in gns3_container.list_objects() if obj.name.startswith('images/')]
|
||||
|
||||
images = {}
|
||||
for image_name in images_to_find:
|
||||
images_with_same_name =\
|
||||
list(filter(lambda storage_image_name: storage_image_name.endswith(image_name), images_in_storage))
|
||||
|
||||
if len(images_with_same_name) == 1:
|
||||
images[image_name] = images_with_same_name[0]
|
||||
else:
|
||||
raise Exception('Image does not exist in cloud storage or is duplicated')
|
||||
|
||||
return images
|
||||
|
||||
def delete_file(self, file_name):
|
||||
gns3_container = self.storage_driver.get_container(self.GNS3_CONTAINER_NAME)
|
||||
|
||||
try:
|
||||
object_to_delete = gns3_container.get_object(file_name)
|
||||
object_to_delete.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
|
||||
try:
|
||||
hash_object = gns3_container.get_object(file_name + '.md5')
|
||||
hash_object.delete()
|
||||
except ObjectDoesNotExistError:
|
||||
pass
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
""" Interacts with Rackspace API to create and manage cloud instances. """
|
||||
|
||||
from gns3.cloud.base_cloud_ctrl import BaseCloudCtrl
|
||||
from .base_cloud_ctrl import BaseCloudCtrl
|
||||
import json
|
||||
import requests
|
||||
from libcloud.compute.drivers.rackspace import ENDPOINT_ARGS_MAP
|
||||
@@ -42,11 +42,9 @@ class RackspaceCtrl(BaseCloudCtrl):
|
||||
|
||||
""" Controller class for interacting with Rackspace API. """
|
||||
|
||||
def __init__(self, username, api_key, gns3_ias_url):
|
||||
def __init__(self, username, api_key, *args, **kwargs):
|
||||
super(RackspaceCtrl, self).__init__(username, api_key)
|
||||
|
||||
self.gns3_ias_url = gns3_ias_url
|
||||
|
||||
# set this up so it can be swapped out with a mock for testing
|
||||
self.post_fn = requests.post
|
||||
self.driver_cls = get_driver(Provider.RACKSPACE)
|
||||
@@ -225,54 +223,37 @@ class RackspaceCtrl(BaseCloudCtrl):
|
||||
self.region = region
|
||||
return True
|
||||
|
||||
def _get_shared_images(self, username, region, gns3_version):
|
||||
"""
|
||||
Given a GNS3 version, ask gns3-ias to share compatible images
|
||||
|
||||
Response:
|
||||
[{"created_at": "", "schema": "", "status": "", "member_id": "", "image_id": "", "updated_at": ""},]
|
||||
or, if access was already asked
|
||||
[{"image_id": "", "member_id": "", "status": "ALREADYREQUESTED"},]
|
||||
"""
|
||||
endpoint = self.gns3_ias_url+"/images/grant_access"
|
||||
params = {
|
||||
"user_id": username,
|
||||
"user_region": region.upper(),
|
||||
"gns3_version": gns3_version,
|
||||
}
|
||||
try:
|
||||
response = requests.get(endpoint, params=params)
|
||||
except requests.ConnectionError:
|
||||
raise ApiError("Unable to connect to IAS")
|
||||
|
||||
status = response.status_code
|
||||
|
||||
if status == 200:
|
||||
return response.json()
|
||||
elif status == 404:
|
||||
raise ItemNotFound()
|
||||
else:
|
||||
raise ApiError("IAS status code: %d" % status)
|
||||
|
||||
def list_images(self):
|
||||
"""
|
||||
Return a dictionary containing RackSpace server images
|
||||
retrieved from gns3-ias server
|
||||
"""
|
||||
if not (self.tenant_id and self.region):
|
||||
return {}
|
||||
|
||||
try:
|
||||
shared_images = self._get_shared_images(self.tenant_id, self.region, __version__)
|
||||
images = {}
|
||||
for i in shared_images:
|
||||
images[i['image_id']] = i['image_name']
|
||||
return images
|
||||
except ItemNotFound:
|
||||
return {}
|
||||
except ApiError as e:
|
||||
log.error('Error while retrieving image list: %s' % e)
|
||||
return {}
|
||||
|
||||
def get_image(self, image_id):
|
||||
return self.driver.get_image(image_id)
|
||||
|
||||
|
||||
def get_provider(cloud_settings):
|
||||
"""
|
||||
Utility function to retrieve a cloud provider instance already authenticated and with the
|
||||
region set
|
||||
|
||||
:param cloud_settings: cloud settings dictionary
|
||||
:return: a provider instance or None on errors
|
||||
"""
|
||||
try:
|
||||
username = cloud_settings['cloud_user_name']
|
||||
apikey = cloud_settings['cloud_api_key']
|
||||
region = cloud_settings['cloud_region']
|
||||
except KeyError as e:
|
||||
log.error("Unable to create cloud provider: {}".format(e))
|
||||
return
|
||||
|
||||
provider = RackspaceCtrl(username, apikey)
|
||||
|
||||
if not provider.authenticate():
|
||||
log.error("Authentication failed for cloud provider")
|
||||
return
|
||||
|
||||
if not region:
|
||||
region = provider.list_regions().values()[0]
|
||||
|
||||
if not provider.set_region(region):
|
||||
log.error("Unable to set cloud provider region")
|
||||
return
|
||||
|
||||
return provider
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from contextlib import contextmanager
|
||||
import io
|
||||
import json
|
||||
from socket import error as socket_error
|
||||
import logging
|
||||
import os
|
||||
import zipfile
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
from PyQt4 import QtCore
|
||||
from PyQt4.QtCore import QThread
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
from ..qt import QtCore
|
||||
|
||||
from .rackspace_ctrl import RackspaceCtrl
|
||||
from .exceptions import KeyPairExists
|
||||
from .rackspace_ctrl import get_provider
|
||||
from ..topology import Topology
|
||||
from ..servers import Servers
|
||||
|
||||
@@ -45,94 +46,73 @@ def ssh_client(host, key_string):
|
||||
client.connect(hostname=host, username="root", pkey=key)
|
||||
yield client
|
||||
except socket_error as e:
|
||||
log.error("SSH connection error to {}: {}".format(host, e))
|
||||
log.debug("SSH connection socket error to {}: {}".format(host, e))
|
||||
yield None
|
||||
except Exception as e:
|
||||
log.debug("SSH connection error to {}: {}".format(host, e))
|
||||
yield None
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def get_provider(cloud_settings):
|
||||
"""
|
||||
Utility function to retrieve a cloud provider instance already authenticated and with the
|
||||
region set
|
||||
|
||||
:param cloud_settings: cloud settings dictionary
|
||||
:return: a provider instance or None on errors
|
||||
"""
|
||||
try:
|
||||
username = cloud_settings['cloud_user_name']
|
||||
apikey = cloud_settings['cloud_api_key']
|
||||
region = cloud_settings['cloud_region']
|
||||
ias_url = cloud_settings['gns3_ias_url']
|
||||
except KeyError as e:
|
||||
log.error("Unable to create cloud provider: {}".format(e))
|
||||
return
|
||||
|
||||
provider = RackspaceCtrl(username, apikey, ias_url)
|
||||
|
||||
if not provider.authenticate():
|
||||
log.error("Authentication failed for cloud provider")
|
||||
return
|
||||
|
||||
if not region:
|
||||
region = provider.list_regions().values()[0]
|
||||
|
||||
if not provider.set_region(region):
|
||||
log.error("Unable to set cloud provider region")
|
||||
return
|
||||
|
||||
return provider
|
||||
|
||||
|
||||
class ListInstancesThread(QThread):
|
||||
class ListInstancesThread(QtCore.QThread):
|
||||
"""
|
||||
Helper class to retrieve data from the provider in a separate thread,
|
||||
avoid freezing the gui
|
||||
"""
|
||||
instancesReady = pyqtSignal(object)
|
||||
instancesReady = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider):
|
||||
super(QThread, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
instances = self._provider.list_instances()
|
||||
log.debug('Instance list:')
|
||||
for instance in instances:
|
||||
log.debug(' name={}, state={}'.format(instance.name, instance.state))
|
||||
log.debug('Instance list: {}'.format([(i.name, i.state) for i in instances]))
|
||||
self.instancesReady.emit(instances)
|
||||
except Exception as e:
|
||||
log.info('list_instances error: {}'.format(e))
|
||||
|
||||
|
||||
class CreateInstanceThread(QThread):
|
||||
class CreateInstanceThread(QtCore.QThread):
|
||||
"""
|
||||
Helper class to create instances in a separate thread
|
||||
"""
|
||||
instanceCreated = pyqtSignal(object, object)
|
||||
instanceCreated = QtCore.pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, parent, provider, name, flavor_id, image_id):
|
||||
super(QThread, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._name = name
|
||||
self._flavor_id = flavor_id
|
||||
self._image_id = image_id
|
||||
|
||||
def run(self):
|
||||
k = self._provider.create_key_pair(self._name)
|
||||
log.debug("Creating cloud keypair with name {}".format(self._name))
|
||||
try:
|
||||
k = self._provider.create_key_pair(self._name)
|
||||
except KeyPairExists:
|
||||
log.debug("Cloud keypair with name {} exists. Recreating.".format(self._name))
|
||||
# delete keypairs if they already exist
|
||||
self._provider.delete_key_pair_by_name(self._name)
|
||||
k = self._provider.create_key_pair(self._name)
|
||||
|
||||
log.debug("Creating cloud server with name {}".format(self._name))
|
||||
i = self._provider.create_instance(self._name, self._flavor_id, self._image_id, k)
|
||||
log.debug("Cloud server {} created".format(self._name))
|
||||
|
||||
self.instanceCreated.emit(i, k)
|
||||
|
||||
|
||||
class DeleteInstanceThread(QThread):
|
||||
class DeleteInstanceThread(QtCore.QThread):
|
||||
"""
|
||||
Helper class to remove an instance in a separate thread
|
||||
"""
|
||||
instanceDeleted = pyqtSignal(object)
|
||||
instanceDeleted = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent, provider, instance):
|
||||
super(QThread, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._instance = instance
|
||||
|
||||
@@ -141,15 +121,60 @@ class DeleteInstanceThread(QThread):
|
||||
self.instanceDeleted.emit(self._instance)
|
||||
|
||||
|
||||
class StartGNS3ServerThread(QThread):
|
||||
class StartGNS3ServerThread(QtCore.QThread):
|
||||
"""
|
||||
Perform an SSH connection to the instances in a separate thread,
|
||||
outside the GUI event loop, and start GNS3 server
|
||||
"""
|
||||
gns3server_started = pyqtSignal(str, str, str)
|
||||
gns3server_started = QtCore.pyqtSignal(str, str, str)
|
||||
|
||||
# This is for testing without pushing to github
|
||||
# commands = '''
|
||||
# DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
# DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
# DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
# ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
# mkdir -p /opt/gns3
|
||||
# tar xzf /tmp/gns3-server.tgz -C /opt/gns3
|
||||
# cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
# cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
# ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
# wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
|
||||
# python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
# hostname gns3-iouvm
|
||||
# tar xzf iouyap.tar.gz -C /usr/local/bin
|
||||
# killall python3 gns3server gns3dms
|
||||
# '''
|
||||
|
||||
commands = '''
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
mkdir -p /opt/gns3
|
||||
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
|
||||
cd /opt/gns3/gns3-server; git checkout dev; git pull
|
||||
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap-64-bit.tar.gz'
|
||||
tar xzf iouyap-64-bit.tar.gz -C /usr/local/bin
|
||||
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
hostname gns3-iouvm # set hostname for iou
|
||||
wget -O vpcs http://sourceforge.net/projects/vpcs/files/0.6/vpcs_0.6_Linux64/download
|
||||
cp vpcs /usr/local/bin/vpcs
|
||||
chmod a+x /usr/local/bin/vpcs
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
|
||||
def __init__(self, parent, host, private_key_string, server_id, username, api_key, region, dead_time):
|
||||
super(QThread, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
self._host = host
|
||||
self._private_key_string = private_key_string
|
||||
self._server_id = server_id
|
||||
@@ -158,40 +183,86 @@ class StartGNS3ServerThread(QThread):
|
||||
self._region = region
|
||||
self._dead_time = dead_time
|
||||
|
||||
def exec_command(self, client, cmd, wait_time=-1):
|
||||
|
||||
cmd += '; exit $?'
|
||||
|
||||
stdout_data = b''
|
||||
stderr_data = b''
|
||||
|
||||
log.debug('cmd: {}'.format(cmd))
|
||||
# Send the command (non-blocking)
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
|
||||
# Wait for the command to terminate
|
||||
wait = int(wait_time)
|
||||
while not stdout.channel.exit_status_ready() and wait != 0:
|
||||
time.sleep(1)
|
||||
wait -= 1
|
||||
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
log.debug('exit status: {}'.format(stdout.channel.exit_status))
|
||||
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
|
||||
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
|
||||
return stdout_data, stderr_data
|
||||
|
||||
|
||||
def run(self):
|
||||
with ssh_client(self._host, self._private_key_string) as client:
|
||||
if client is not None:
|
||||
# We might be attempting a connection before the instance is fully booted, so retry
|
||||
# when the ssh connection fails.
|
||||
ssh_connected = False
|
||||
while not ssh_connected:
|
||||
with ssh_client(self._host, self._private_key_string) as client:
|
||||
if client is None:
|
||||
time.sleep(1)
|
||||
continue
|
||||
ssh_connected = True
|
||||
|
||||
# This is for testing without pushing to github
|
||||
# os.system('rm -rf /tmp/gns3-server')
|
||||
# os.system('cp -a /Users/jseutter/projects/gns3-server /tmp/gns3-server')
|
||||
# os.system('cd /tmp; tar czf /tmp/gns3-server.tgz gns3-server')
|
||||
# sftp = client.open_sftp()
|
||||
# sftp.put('/tmp/gns3-server.tgz', '/tmp/gns3-server.tgz')
|
||||
# sftp.close()
|
||||
|
||||
for cmd in [l for l in self.commands.splitlines() if l.strip()]:
|
||||
self.exec_command(client, cmd)
|
||||
|
||||
data = {
|
||||
'instance_id': self._server_id,
|
||||
'cloud_user_name': self._username,
|
||||
'cloud_api_key': self._api_key,
|
||||
'region': self._region,
|
||||
'cloud_region': self._region,
|
||||
'dead_time': self._dead_time,
|
||||
}
|
||||
# TODO: Properly escape the data portion of the command line
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --data="{}" 2>/tmp/gns3_stderr.log'.format(data)
|
||||
log.debug(start_cmd)
|
||||
stdin, stdout, stderr = client.exec_command(start_cmd)
|
||||
response = stdout.read().decode('ascii')
|
||||
log.debug('ssh response: {}'.format(response))
|
||||
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._host, data)
|
||||
stdout, stderr = self.exec_command(client, start_cmd, wait_time=15)
|
||||
response = stdout.decode('utf-8')
|
||||
self.gns3server_started.emit(str(self._server_id), str(self._host), str(response))
|
||||
|
||||
|
||||
class WSConnectThread(QThread):
|
||||
class WSConnectThread(QtCore.QThread):
|
||||
"""
|
||||
Establish a websocket connection with the remote gns3server
|
||||
instance. Run outside the GUI event loop.
|
||||
"""
|
||||
established = pyqtSignal(str)
|
||||
established = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent, provider, server_id, host, port, ca_file):
|
||||
super(QThread, self).__init__(parent)
|
||||
def __init__(self, parent, provider, server_id, host, port, ca_file,
|
||||
auth_user, auth_password, ssh_pkey, instance_id):
|
||||
super().__init__(parent)
|
||||
self._provider = provider
|
||||
self._server_id = server_id
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._ca_file = ca_file
|
||||
self._auth_user = auth_user
|
||||
self._auth_password = auth_password
|
||||
self._ssh_pkey = ssh_pkey
|
||||
self._instance_id = instance_id
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
@@ -200,7 +271,9 @@ class WSConnectThread(QThread):
|
||||
|
||||
log.debug('WSConnectThread.run() begin')
|
||||
servers = Servers.instance()
|
||||
server = servers.getCloudServer(self._host, self._port, self._ca_file)
|
||||
server = servers.getCloudServer(self._host, self._port, self._ca_file,
|
||||
self._auth_user, self._auth_password, self._ssh_pkey,
|
||||
self._instance_id)
|
||||
log.debug('after getCloudServer call. {}'.format(server))
|
||||
self.established.emit(str(self._server_id))
|
||||
|
||||
@@ -209,7 +282,7 @@ class WSConnectThread(QThread):
|
||||
self.established.emit(self._server_id)
|
||||
|
||||
|
||||
class UploadProjectThread(QThread):
|
||||
class UploadProjectThread(QtCore.QThread):
|
||||
"""
|
||||
Zip and Upload project to the cloud
|
||||
"""
|
||||
@@ -219,10 +292,11 @@ class UploadProjectThread(QThread):
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, project_settings, cloud_settings):
|
||||
super().__init__()
|
||||
self.project_settings = project_settings
|
||||
def __init__(self, parent, cloud_settings, project_path, images_path):
|
||||
super().__init__(parent)
|
||||
self.cloud_settings = cloud_settings
|
||||
self.project_path = project_path
|
||||
self.images_path = images_path
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -234,7 +308,7 @@ class UploadProjectThread(QThread):
|
||||
self.update.emit(10) # update progress to 10%
|
||||
|
||||
provider = get_provider(self.cloud_settings)
|
||||
provider.upload_file(zipped_project_file, 'projects')
|
||||
provider.upload_file(zipped_project_file, 'projects/' + os.path.basename(zipped_project_file))
|
||||
|
||||
self.update.emit(20) # update progress to 20%
|
||||
|
||||
@@ -242,22 +316,22 @@ class UploadProjectThread(QThread):
|
||||
images = set([node.settings()["image"] for node in topology.nodes() if 'image' in node.settings()])
|
||||
|
||||
for i, image in enumerate(images):
|
||||
provider.upload_file(image, 'images')
|
||||
provider.upload_file(image, 'images/' + os.path.relpath(image, self.images_path))
|
||||
self.update.emit(20 + (float(i) / len(images) * 80))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error exporting project to cloud")
|
||||
self.error.emit("Error exporting project {}".format(str(e)), True)
|
||||
self.error.emit("Error exporting project: {}".format(e), True)
|
||||
|
||||
def zip_project_dir(self):
|
||||
"""
|
||||
Zips project files
|
||||
:return: path to zipped project file
|
||||
"""
|
||||
project_name = os.path.basename(self.project_settings["project_path"])
|
||||
project_name = os.path.basename(self.project_path)
|
||||
output_filename = os.path.join(tempfile.gettempdir(), project_name + ".zip")
|
||||
project_dir = os.path.dirname(self.project_settings["project_path"])
|
||||
project_dir = os.path.dirname(self.project_path)
|
||||
relroot = os.path.abspath(os.path.join(project_dir, os.pardir))
|
||||
with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for root, dirs, files in os.walk(project_dir):
|
||||
@@ -281,3 +355,171 @@ class UploadProjectThread(QThread):
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class UploadFilesThread(QtCore.QThread):
|
||||
"""
|
||||
Uploads files to cloud files
|
||||
|
||||
:param cloud_settings:
|
||||
:param files_to_upload: list of tuples of (file path, file name to save in cloud)
|
||||
"""
|
||||
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_settings, files_to_upload):
|
||||
super().__init__(parent)
|
||||
self._cloud_settings = cloud_settings
|
||||
self._files_to_upload = files_to_upload
|
||||
|
||||
def run(self):
|
||||
self.update.emit(0)
|
||||
|
||||
try:
|
||||
for i, file_to_upload in enumerate(self._files_to_upload):
|
||||
provider = get_provider(self._cloud_settings)
|
||||
|
||||
log.debug('Uploading image {} to cloud as {}'.format(file_to_upload[0], file_to_upload[1]))
|
||||
provider.upload_file(file_to_upload[0], file_to_upload[1])
|
||||
|
||||
self.update.emit((i+1) * 100 / len(self._files_to_upload))
|
||||
log.debug('Uploading image completed')
|
||||
except Exception as e:
|
||||
log.exception("Error uploading images to cloud")
|
||||
self.error.emit("Error uploading images: {}".format(e), True)
|
||||
|
||||
self.completed.emit()
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DownloadProjectThread(QtCore.QThread):
|
||||
"""
|
||||
Downloads project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, cloud_project_file_name, project_dest_path, images_dest_path, cloud_settings):
|
||||
super().__init__(parent)
|
||||
self.project_name = cloud_project_file_name
|
||||
self.project_dest_path = project_dest_path
|
||||
self.images_dest_path = images_dest_path
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.update.emit(0)
|
||||
provider = get_provider(self.cloud_settings)
|
||||
zip_file = provider.download_file(self.project_name)
|
||||
zip_file = zipfile.ZipFile(zip_file, mode='r')
|
||||
zip_file.extractall(self.project_dest_path)
|
||||
zip_file.close()
|
||||
project_name = zip_file.namelist()[0].strip('/')
|
||||
|
||||
self.update.emit(20)
|
||||
|
||||
with open(os.path.join(self.project_dest_path, project_name, project_name + '.gns3'), 'r') as f:
|
||||
project_settings = json.loads(f.read())
|
||||
|
||||
images = set()
|
||||
for node in project_settings["topology"].get("nodes", []):
|
||||
if "properties" in node and "image" in node["properties"]:
|
||||
images.add(node["properties"]["image"])
|
||||
|
||||
image_names_in_cloud = provider.find_storage_image_names(images)
|
||||
|
||||
for i, image in enumerate(images):
|
||||
dest_path = os.path.join(self.images_dest_path, *image_names_in_cloud[image].split('/')[1:])
|
||||
|
||||
if not os.path.exists(os.path.dirname(dest_path)):
|
||||
os.makedirs(os.path.dirname(dest_path))
|
||||
|
||||
provider.download_file(image_names_in_cloud[image], dest_path)
|
||||
self.update.emit(20 + (float(i) / len(images) * 80))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error importing project from cloud")
|
||||
self.error.emit("Error importing project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
class DownloadImagesThread(QtCore.QThread):
|
||||
"""
|
||||
Downloads multiple files from cloud files
|
||||
"""
|
||||
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, cloud_settings, images_dest_path, image_names):
|
||||
super().__init__()
|
||||
self._cloud_settings = cloud_settings
|
||||
self._images_dest_path = images_dest_path
|
||||
self._image_names = image_names
|
||||
|
||||
def run(self):
|
||||
self.update.emit(0)
|
||||
try:
|
||||
provider = get_provider(self._cloud_settings)
|
||||
image_names_in_cloud = provider.find_storage_image_names(self._image_names)
|
||||
|
||||
for i, image in enumerate(self._image_names):
|
||||
dest_path = os.path.join(self._images_dest_path, *image_names_in_cloud[image].split('/')[1:])
|
||||
|
||||
if not os.path.exists(os.path.dirname(dest_path)):
|
||||
os.makedirs(os.path.dirname(dest_path))
|
||||
|
||||
provider.download_file(image_names_in_cloud[image], dest_path)
|
||||
|
||||
self.update.emit(i * 100 / len(self._image_names))
|
||||
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error importing project from cloud")
|
||||
self.error.emit("Error importing project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
self.quit()
|
||||
|
||||
|
||||
class DeleteProjectThread(QtCore.QThread):
|
||||
"""
|
||||
Deletes project from cloud storage
|
||||
"""
|
||||
|
||||
# signals to update the progress dialog.
|
||||
error = QtCore.pyqtSignal(str, bool)
|
||||
completed = QtCore.pyqtSignal()
|
||||
update = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, parent, project_file_name, cloud_settings):
|
||||
super().__init__(parent)
|
||||
self.project_file_name = project_file_name
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
provider = get_provider(self.cloud_settings)
|
||||
provider.delete_file(self.project_file_name)
|
||||
self.completed.emit()
|
||||
except Exception as e:
|
||||
log.exception("Error deleting project")
|
||||
self.error.emit("Error deleting project: {}".format(e), True)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_cloud_projects(cloud_settings):
|
||||
provider = get_provider(cloud_settings)
|
||||
return provider.list_projects()
|
||||
|
||||
253
gns3/cloud_builder.py
Normal file
253
gns3/cloud_builder.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# -*- 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 PyQt4.QtCore import pyqtSignal
|
||||
from PyQt4.QtCore import QThread
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from .cloud.utils import ssh_client
|
||||
from .cloud.exceptions import KeyPairExists
|
||||
|
||||
from .servers import Servers
|
||||
from .topology import Topology
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudBuilder(QThread):
|
||||
"""
|
||||
"""
|
||||
# Notify with progress amount and instance_id
|
||||
progressUpdate = pyqtSignal(object, str)
|
||||
|
||||
# Notify with current state and instance_id
|
||||
stateChange = pyqtSignal(object, str)
|
||||
|
||||
# Notify when instance is ready with instance_id
|
||||
buildComplete = pyqtSignal(str)
|
||||
|
||||
# Notify when the instance has been created with instance and keypair
|
||||
instanceCreated = pyqtSignal(object, object)
|
||||
|
||||
# Notify when the public ip is available with ip and instance_id
|
||||
instanceHasIP = pyqtSignal(str, str)
|
||||
|
||||
# Notify when instance id exists with builder and instance_id
|
||||
instanceIdExists = pyqtSignal(object, str)
|
||||
|
||||
|
||||
def __init__(self, parent, cloud_provider, ca_dir):
|
||||
super(QThread, self).__init__(parent)
|
||||
# Store our parent so it can be passed to threads we spawn.
|
||||
self._parent = parent
|
||||
self._provider = cloud_provider
|
||||
self._ca_dir = ca_dir
|
||||
self._start_at_create = False
|
||||
self._start_at_setup = False
|
||||
self._instance = None
|
||||
|
||||
def startAtCreate(self, instance_name, flavor_id, image_id):
|
||||
self._start_at_create = True
|
||||
self._instance_name = instance_name
|
||||
self._flavor_id = flavor_id
|
||||
self._image_id = image_id
|
||||
|
||||
def startAtSetup(self, instance, keypair):
|
||||
self._start_at_setup = True
|
||||
self._instance = instance
|
||||
self._key_pair = keypair
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
log.debug('CloudBuilder.run')
|
||||
if self._start_at_create:
|
||||
log.debug('CloudBuilder._start_at_create')
|
||||
self._createInstance(self._provider, self._instance_name, self._flavor_id,
|
||||
self._image_id)
|
||||
log.debug('got here 3')
|
||||
if self._start_at_setup:
|
||||
log.debug('CloudBuilder start at setup')
|
||||
self._instanceCreated(self._instance, self._key_pair)
|
||||
except Exception:
|
||||
log.exception("CloudBuilder trapped an exception:")
|
||||
log.error('CloudBuilder stopped in error state.')
|
||||
|
||||
def _createInstance(self, provider, name, flavor_id, image_id):
|
||||
log.debug("Creating cloud keypair with name {}".format(name))
|
||||
key_pair = None
|
||||
while key_pair is None:
|
||||
try:
|
||||
key_pair = provider.create_key_pair(name)
|
||||
except KeyPairExists:
|
||||
log.debug("Deleting old key pair with name {}.".format(name))
|
||||
self._provider.delete_key_pair_by_name(name)
|
||||
except Exception as e:
|
||||
log.debug("create_key_pair exception {}".format(e))
|
||||
|
||||
log.debug("Creating cloud server with name {}".format(name))
|
||||
instance = None
|
||||
while instance is None:
|
||||
try:
|
||||
instance = self._provider.create_instance(name, flavor_id, image_id, key_pair)
|
||||
except Exception as e:
|
||||
log.debug("create_instance exception {}".format(e))
|
||||
log.debug("Cloud server {} created".format(name))
|
||||
self._instanceCreated(instance, key_pair)
|
||||
|
||||
def _instanceCreated(self, instance, key_pair):
|
||||
log.debug('CloudBuilder._instanceCreated {}'.format(instance.id))
|
||||
self._instance = instance
|
||||
self._instance_id = instance.id
|
||||
self._key_pair = key_pair
|
||||
self.instanceIdExists.emit(self, instance.id)
|
||||
self.instanceCreated.emit(instance, key_pair)
|
||||
self._waitForPublicIP()
|
||||
|
||||
|
||||
def _waitForPublicIP(self):
|
||||
public_ip = None
|
||||
while public_ip is None:
|
||||
time.sleep(10)
|
||||
try:
|
||||
instance = self._provider.get_instance(self._instance)
|
||||
# Look for public ip address
|
||||
for ip in instance.public_ips:
|
||||
# Don't use the ipv6 address
|
||||
if ':' not in ip:
|
||||
public_ip = ip
|
||||
break
|
||||
except Exception as e:
|
||||
log.debug('list_instances error: {}'.format(e))
|
||||
|
||||
# updated info, keep it.
|
||||
self._instance = instance
|
||||
self._public_ip = public_ip
|
||||
self.instanceHasIP.emit(self._public_ip, self._instance.id)
|
||||
time.sleep(60)
|
||||
self._startGNS3Server(1800)
|
||||
|
||||
def _startGNS3Server(self, dead_time):
|
||||
commands = '''
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
mkdir -p /opt/gns3
|
||||
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
|
||||
cd /opt/gns3/gns3-server; git checkout dev; git pull
|
||||
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
|
||||
tar xzf iouyap.tar.gz -C /usr/local/bin
|
||||
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
hostname gns3-iouvm # set hostname for iou
|
||||
wget 'http://downloads.sourceforge.net/project/vpcs/0.6/vpcs_0.6_Linux64'
|
||||
cp vpcs_0.6_Linux64 /usr/local/bin/vpcs
|
||||
chmod a+x /usr/local/bin/vpcs
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
def exec_command(client, cmd, wait_time=-1):
|
||||
|
||||
cmd += '; exit $?'
|
||||
|
||||
stdout_data = b''
|
||||
stderr_data = b''
|
||||
|
||||
log.debug('cmd: {}'.format(cmd))
|
||||
# Send the command (non-blocking)
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
|
||||
# Wait for the command to terminate
|
||||
wait = int(wait_time)
|
||||
while not stdout.channel.exit_status_ready() and wait != 0:
|
||||
time.sleep(1)
|
||||
wait -= 1
|
||||
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
log.debug('exit status: {}'.format(stdout.channel.exit_status))
|
||||
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
|
||||
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
|
||||
return stdout_data, stderr_data
|
||||
|
||||
|
||||
# We might be attempting a connection before the instance is fully booted, so retry
|
||||
# when the ssh connection fails.
|
||||
ssh_connected = False
|
||||
response = None
|
||||
while not ssh_connected:
|
||||
with ssh_client(self._public_ip, self._key_pair.private_key) as client:
|
||||
if client is None:
|
||||
time.sleep(1)
|
||||
continue
|
||||
ssh_connected = True
|
||||
|
||||
for cmd in [l for l in commands.splitlines() if l.strip()]:
|
||||
exec_command(client, cmd)
|
||||
|
||||
data = {
|
||||
'instance_id': self._instance_id,
|
||||
'cloud_user_name': self._provider.username,
|
||||
'cloud_api_key': self._provider.api_key,
|
||||
'cloud_region': self._provider.region,
|
||||
'dead_time': dead_time,
|
||||
}
|
||||
# TODO: Properly escape the data portion of the command line
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._public_ip, data)
|
||||
stdout, stderr = exec_command(client, start_cmd, wait_time=15)
|
||||
response = stdout.decode('utf-8')
|
||||
|
||||
log.debug(response)
|
||||
data = ast.literal_eval(response)
|
||||
# TODO: have the server return the port it is running on
|
||||
port = 8000
|
||||
|
||||
username = data['WEB_USERNAME']
|
||||
password = data['WEB_PASSWORD']
|
||||
|
||||
ssl_cert = ''.join(data['SSL_CRT'])
|
||||
ca_filename = 'cloud_server_{}.crt'.format(self._public_ip)
|
||||
ca_dir = self._ca_dir
|
||||
ca_file = os.path.join(ca_dir, ca_filename)
|
||||
try:
|
||||
os.makedirs(ca_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
with open(ca_file, 'wb') as ca_fh:
|
||||
ca_fh.write(ssl_cert.encode('utf-8'))
|
||||
|
||||
topology = Topology.instance()
|
||||
top_instance = topology.getInstance(self._instance_id)
|
||||
top_instance.set_later_attributes(self._public_ip, port, ssl_cert, ca_file)
|
||||
|
||||
servers = Servers.instance()
|
||||
server = servers.getCloudServer(self._public_ip, port, ca_file, username, password,
|
||||
self._key_pair.private_key, self._instance_id)
|
||||
servers.save()
|
||||
log.debug('Cloud server gns3server started.')
|
||||
self.buildComplete.emit(self._instance_id)
|
||||
@@ -1,40 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import ast
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
from PyQt4.QtGui import QWidget
|
||||
from PyQt4.QtGui import QIcon
|
||||
from PyQt4.QtGui import QMenu
|
||||
from PyQt4.QtGui import QAction
|
||||
from PyQt4.QtGui import QInputDialog
|
||||
from PyQt4.QtCore import QAbstractTableModel
|
||||
from PyQt4.QtCore import QModelIndex
|
||||
from PyQt4.QtCore import QTimer
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
from PyQt4.Qt import Qt
|
||||
import os
|
||||
import json
|
||||
|
||||
from .cloud.utils import (ListInstancesThread, CreateInstanceThread, DeleteInstanceThread,
|
||||
StartGNS3ServerThread, WSConnectThread)
|
||||
from libcloud.compute.types import NodeState
|
||||
|
||||
from .qt import QtCore, QtGui
|
||||
from .cloud.utils import (ListInstancesThread, DeleteInstanceThread)
|
||||
from .topology import Topology
|
||||
from .servers import Servers
|
||||
|
||||
|
||||
# this widget was promoted on Creator, must use absolute imports
|
||||
from gns3.ui.cloud_inspector_view_ui import Ui_CloudInspectorView
|
||||
from gns3.cloud_builder import CloudBuilder
|
||||
from gns3.cloud_instances import CloudInstances
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
POLLING_TIMER = 10000 # in milliseconds
|
||||
|
||||
|
||||
class RunningInstanceState():
|
||||
class RunningInstanceState(NodeState):
|
||||
"""
|
||||
GNS3 states for running instances
|
||||
"""
|
||||
IDLE = 0
|
||||
GNS3SERVER_STARTED = 1
|
||||
WS_CONNECTED = 2
|
||||
GNS3SERVER_STARTING = -1
|
||||
GNS3SERVER_STARTED = -2
|
||||
WS_CONNECTED = -3
|
||||
|
||||
|
||||
class InstanceTableModel(QAbstractTableModel):
|
||||
class InstanceTableModel(QtCore.QAbstractTableModel):
|
||||
"""
|
||||
A custom table model storing data of cloud instances
|
||||
"""
|
||||
@@ -55,16 +52,18 @@ class InstanceTableModel(QAbstractTableModel):
|
||||
self._ids = []
|
||||
self.reset()
|
||||
|
||||
def _get_status_icon_path(self, state):
|
||||
def _get_status_icon_path(self, instance):
|
||||
"""
|
||||
Return a string pointing to the graphic resource
|
||||
"""
|
||||
if state == NodeState.RUNNING:
|
||||
if instance.state == RunningInstanceState.WS_CONNECTED:
|
||||
return ':/icons/led_green.svg'
|
||||
elif state in (NodeState.REBOOTING, NodeState.PENDING, NodeState.UNKNOWN):
|
||||
return ':/icons/led_yellow.svg'
|
||||
else:
|
||||
elif instance.state in (RunningInstanceState.STOPPED,
|
||||
RunningInstanceState.TERMINATED,
|
||||
RunningInstanceState.UNKNOWN):
|
||||
return ':/icons/led_red.svg'
|
||||
else:
|
||||
return ':/icons/led_yellow.svg'
|
||||
|
||||
def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
|
||||
return len(self._instances)
|
||||
@@ -76,12 +75,12 @@ class InstanceTableModel(QAbstractTableModel):
|
||||
instance = self._instances.get(self._ids[index.row()])
|
||||
col = index.column()
|
||||
|
||||
if role == Qt.DecorationRole:
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if col == 1:
|
||||
# status
|
||||
return QIcon(self._get_status_icon_path(instance.state))
|
||||
return QtGui.QIcon(self._get_status_icon_path(instance))
|
||||
|
||||
elif role == Qt.DisplayRole:
|
||||
elif role == QtCore.Qt.DisplayRole:
|
||||
if col == 0:
|
||||
# name
|
||||
return instance.name
|
||||
@@ -98,11 +97,17 @@ class InstanceTableModel(QAbstractTableModel):
|
||||
return 'Unknown'
|
||||
elif col == 3:
|
||||
# devices
|
||||
return 0
|
||||
count = 0
|
||||
topology = Topology.instance()
|
||||
for node in topology.nodes():
|
||||
id = node._server.instance_id or 0
|
||||
if instance.id == id:
|
||||
count += 1
|
||||
return count
|
||||
return None
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
|
||||
try:
|
||||
return self._header_data[section]
|
||||
except IndexError:
|
||||
@@ -110,9 +115,9 @@ class InstanceTableModel(QAbstractTableModel):
|
||||
return super(InstanceTableModel, self).headerData(section, orientation, role)
|
||||
|
||||
def addInstance(self, instance):
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
|
||||
if not len(self._instances):
|
||||
self.beginInsertColumns(QModelIndex(), 0, self._width-1)
|
||||
self.beginInsertColumns(QtCore.QModelIndex(), 0, self._width-1)
|
||||
self.endInsertColumns()
|
||||
self._ids.append(instance.id)
|
||||
self._instances[instance.id] = instance
|
||||
@@ -133,7 +138,7 @@ class InstanceTableModel(QAbstractTableModel):
|
||||
def removeInstanceById(self, instance_id):
|
||||
try:
|
||||
index = self._ids.index(instance_id)
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
self.beginRemoveRows(QtCore.QModelIndex(), index, index)
|
||||
del self._instances[instance_id]
|
||||
del self._ids[index]
|
||||
self.endRemoveRows()
|
||||
@@ -156,10 +161,10 @@ class InstanceTableModel(QAbstractTableModel):
|
||||
self.addInstance(instance)
|
||||
|
||||
def getInstanceById(self, instance_id):
|
||||
return self._instances[instance_id]
|
||||
return self._instances.get(instance_id, None)
|
||||
|
||||
|
||||
class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
class CloudInspectorView(QtGui.QWidget, Ui_CloudInspectorView):
|
||||
"""
|
||||
Table view showing data coming from InstanceTableModel
|
||||
|
||||
@@ -167,10 +172,10 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
instanceSelected(int) Emitted when users click and select an instance on the inspector.
|
||||
Param int is the ID of the instance
|
||||
"""
|
||||
instanceSelected = pyqtSignal(int)
|
||||
instanceSelected = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent):
|
||||
super(QWidget, self).__init__(parent)
|
||||
super(QtGui.QWidget, self).__init__(parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._provider = None
|
||||
@@ -181,21 +186,21 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
self._model = InstanceTableModel() # shortcut for self.uiInstancesTableView.model()
|
||||
self.uiInstancesTableView.setModel(self._model)
|
||||
self.uiInstancesTableView.verticalHeader().hide()
|
||||
self.uiInstancesTableView.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.uiInstancesTableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.uiInstancesTableView.horizontalHeader().setStretchLastSection(True)
|
||||
# connections
|
||||
self.uiInstancesTableView.customContextMenuRequested.connect(self._contextMenu)
|
||||
self.uiInstancesTableView.selectionModel().currentRowChanged.connect(self._rowChanged)
|
||||
self.uiInstancesTableView.clicked.connect(self._rowChanged)
|
||||
self.uiCreateInstanceButton.clicked.connect(self._create_new_instance)
|
||||
|
||||
self._pollingTimer = QTimer(self)
|
||||
self._pollingTimer = QtCore.QTimer(self)
|
||||
self._pollingTimer.timeout.connect(self._polling_slot)
|
||||
|
||||
# map flavor ids to combobox indexes
|
||||
self.flavor_index_id = []
|
||||
|
||||
# internal status for running instances
|
||||
self._running_instances = {}
|
||||
# A dictionary of {image_id, CloudBuilder}
|
||||
self._builders = {}
|
||||
|
||||
def _get_flavor_index(self, flavor_id):
|
||||
try:
|
||||
@@ -203,23 +208,20 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
def load(self, main_win, instances):
|
||||
def load(self, main_win, instance_ids):
|
||||
"""
|
||||
Fill the model data layer with instances retrieved through libcloud
|
||||
Fill the model data layer with instance info loaded from the topology file
|
||||
"""
|
||||
self._main_window = main_win
|
||||
self._provider = main_win.cloudProvider
|
||||
self._settings = main_win.cloudSettings()
|
||||
log.info('CloudInspectorView.load')
|
||||
# TODO: If a network error occurs in the first ListInstances call,
|
||||
# the instance will *never* appear in the cloud inspector and will
|
||||
# not get cleaned up when the gui exits. Fix this bug.
|
||||
|
||||
for i in instances:
|
||||
self._project_instances_id.append(i["id"])
|
||||
for instance_id in instance_ids:
|
||||
self._project_instances_id.append(instance_id)
|
||||
|
||||
update_thread = ListInstancesThread(self, self._provider)
|
||||
update_thread.instancesReady.connect(self._populate_model)
|
||||
update_thread.instancesReady.connect(self._update_model)
|
||||
update_thread.start()
|
||||
self._pollingTimer.start(POLLING_TIMER)
|
||||
# fill sizes comboboxes
|
||||
@@ -246,10 +248,10 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
|
||||
def _contextMenu(self, pos):
|
||||
# create actions
|
||||
delete_action = QAction("Delete", self)
|
||||
delete_action = QtGui.QAction("Delete", self)
|
||||
delete_action.triggered.connect(self._deleteSelectedInstance)
|
||||
# create context menu and add actions
|
||||
menu = QMenu(self.uiInstancesTableView)
|
||||
menu = QtGui.QMenu(self.uiInstancesTableView)
|
||||
menu.addAction(delete_action)
|
||||
# show the menu
|
||||
menu.popup(self.uiInstancesTableView.viewport().mapToGlobal(pos))
|
||||
@@ -262,20 +264,42 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
if len(sel) and self._provider is not None:
|
||||
index = sel[0].row()
|
||||
instance = self._model.getInstance(index)
|
||||
delete_thread = DeleteInstanceThread(self, self._provider, instance)
|
||||
delete_thread.instanceDeleted.connect(self._main_window.remove_instance_from_project)
|
||||
delete_thread.start()
|
||||
|
||||
instance.name = 'Deleting...'
|
||||
self._model.updateInstanceFields(instance, ['name',])
|
||||
# warn user this is destructive
|
||||
msg = "Do you want to remove the instance and any devices running on it?"
|
||||
proceed = QtGui.QMessageBox.question(self, 'Warning', msg,
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
|
||||
|
||||
def _rowChanged(self, current, previous):
|
||||
if proceed == QtGui.QMessageBox.Yes:
|
||||
# disconnect and remove the server
|
||||
servers = Servers.instance()
|
||||
cs = servers.cloudServerById(instance.id)
|
||||
if cs is not None:
|
||||
servers.removeCloudServer(cs)
|
||||
|
||||
# remove instance from the the topology
|
||||
topology = Topology.instance()
|
||||
topology.removeInstance(instance.id)
|
||||
|
||||
delete_thread = DeleteInstanceThread(self, self._provider, instance)
|
||||
delete_thread.instanceDeleted.connect(self._main_window.remove_instance_from_project)
|
||||
delete_thread.start()
|
||||
|
||||
instance.name = 'Deleting...'
|
||||
self._model.updateInstanceFields(instance, ['name'])
|
||||
|
||||
def _rowChanged(self, index):
|
||||
"""
|
||||
This slot is invoked every time users change the current selected row on the
|
||||
inspector
|
||||
"""
|
||||
if current.isValid():
|
||||
instance = self._model.getInstance(current.row())
|
||||
selection = self.uiInstancesTableView.selectionModel().selection()
|
||||
if selection.isEmpty():
|
||||
return
|
||||
|
||||
item = selection.indexes()[0]
|
||||
if item.isValid():
|
||||
instance = self._model.getInstance(item.row())
|
||||
self.instanceSelected.emit(instance.id)
|
||||
|
||||
def _polling_slot(self):
|
||||
@@ -289,107 +313,62 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
update_thread.instancesReady.connect(self._update_model)
|
||||
update_thread.start()
|
||||
|
||||
def _gns3server_started_slot(self, id, host_ip, start_response):
|
||||
def _instanceBuilt(self, id):
|
||||
"""
|
||||
This slot is called when the StartGNS3ServerThread succesfully started
|
||||
the server.
|
||||
|
||||
:param id: the id of the instance
|
||||
:param host_ip: the host ip of the instance
|
||||
:param start_response: the output of the server start script on the remote host
|
||||
This slot is called when instance has finished building.
|
||||
"""
|
||||
self._running_instances[id] = RunningInstanceState.GNS3SERVER_STARTED
|
||||
data = ast.literal_eval(start_response)
|
||||
instance = self._model.getInstanceById(id)
|
||||
instance.state = RunningInstanceState.WS_CONNECTED
|
||||
self._model.updateInstanceFields(instance, ['state'])
|
||||
|
||||
# TODO: have the server return the port it is running on
|
||||
port = 8000
|
||||
|
||||
username = data['WEB_USERNAME']
|
||||
password = data['WEB_PASSWORD']
|
||||
|
||||
ssl_cert = ''.join(data['SSL_CRT'])
|
||||
# TODO: Store the cert file in an appropriate spot
|
||||
ca_file = '/tmp/cloud_server_{}.crt'.format(host_ip)
|
||||
open(ca_file, 'w').write(ssl_cert)
|
||||
|
||||
log.debug('Cloud server gns3server started.')
|
||||
wss_thread = WSConnectThread(self, self._provider, id, host_ip, port, ca_file)
|
||||
wss_thread.established.connect(self._wss_connected_slot)
|
||||
wss_thread.start()
|
||||
|
||||
def _wss_connected_slot(self, id):
|
||||
"""
|
||||
This slot is called when the WSConnectThread succesfully connected to
|
||||
the websocket on the remote host
|
||||
"""
|
||||
self._running_instances[id] = RunningInstanceState.WS_CONNECTED
|
||||
|
||||
def _get_public_ip(self, ip_list):
|
||||
"""
|
||||
Pick the ipv4 address from the list of ip addresses that the instance
|
||||
has.
|
||||
"""
|
||||
for ip in ip_list:
|
||||
log.debug('Cloud server ip {}'.format(ip))
|
||||
# Don't use the ipv6 address
|
||||
if ':' not in ip:
|
||||
log.debug('Chose {} as public ip'.format(ip))
|
||||
return ip
|
||||
return None
|
||||
if self._main_window.loading_cloud_project:
|
||||
project_settings = self._main_window.projectSettings()
|
||||
path = project_settings.get("project_path")
|
||||
with open(path, "r") as f:
|
||||
json_topology = json.load(f)
|
||||
topology = Topology.instance()
|
||||
topology.load(json_topology)
|
||||
self._main_window.loading_cloud_project = False
|
||||
|
||||
def _update_model(self, instances):
|
||||
if not instances:
|
||||
return
|
||||
|
||||
# filter instances for current project
|
||||
# Filter instances to only those in the current project
|
||||
project_instances = [i for i in instances if i.id in self._project_instances_id]
|
||||
for i in project_instances:
|
||||
self._model.updateInstanceFields(i, ['state'])
|
||||
|
||||
# cleanup removed instances
|
||||
# populate underlying model if this is the first call
|
||||
if self._model.rowCount() == 0 and len(project_instances) > 0:
|
||||
self._populate_model(project_instances)
|
||||
self._rebuild_instances(project_instances)
|
||||
|
||||
|
||||
instance_manager = CloudInstances.instance()
|
||||
instance_manager.update_instances(instances)
|
||||
|
||||
|
||||
# Clean up removed instances
|
||||
real = set(i.id for i in project_instances)
|
||||
current = set(self._model.instanceIds)
|
||||
for i in current.difference(real):
|
||||
self._model.removeInstanceById(i)
|
||||
self.uiInstancesTableView.resizeColumnsToContents()
|
||||
|
||||
# handle state for running instances
|
||||
topology = Topology.instance()
|
||||
# Update instance status
|
||||
for i in project_instances:
|
||||
if i.state != NodeState.RUNNING:
|
||||
self._running_instances = {}
|
||||
continue
|
||||
# get the customized instance state from self._model
|
||||
model_instance = self._model.getInstanceById(i.id)
|
||||
|
||||
topology_instance = topology.getInstance(i.id)
|
||||
if topology_instance is None:
|
||||
continue
|
||||
|
||||
state = self._running_instances.setdefault(i.id, RunningInstanceState.IDLE)
|
||||
|
||||
if state == RunningInstanceState.IDLE:
|
||||
public_ip = self._get_public_ip(i.public_ips)
|
||||
# start GNS3 server and deadman switch
|
||||
ssh_thread = StartGNS3ServerThread(
|
||||
self, public_ip, topology_instance.private_key, i.id,
|
||||
self._provider.username, self._provider.api_key, self._provider.region,
|
||||
1800)
|
||||
ssh_thread.gns3server_started.connect(self._gns3server_started_slot)
|
||||
ssh_thread.start()
|
||||
elif state == RunningInstanceState.GNS3SERVER_STARTED:
|
||||
# start WSS connection
|
||||
# TODO: Don't think this is used, remove.
|
||||
# wss_thread = WSConnectThread(self, i.id)
|
||||
# wss_thread.established.connect(self._wss_connected_slot)
|
||||
# wss_thread.start()
|
||||
pass
|
||||
# update model instance state if needed
|
||||
if i.state != RunningInstanceState.RUNNING:
|
||||
self._model.updateInstanceFields(i, ['state'])
|
||||
|
||||
def _populate_model(self, instances):
|
||||
log.info('CloudInspectorView._populate_model')
|
||||
self._model.flavors = self._provider.list_flavors()
|
||||
# filter instances for current project
|
||||
project_instances = [i for i in instances if i.id in self._project_instances_id]
|
||||
for i in project_instances:
|
||||
self._model.addInstance(i)
|
||||
for inst in instances:
|
||||
self._model.addInstance(inst)
|
||||
self.uiInstancesTableView.resizeColumnsToContents()
|
||||
|
||||
def _create_new_instance(self):
|
||||
@@ -397,12 +376,50 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView):
|
||||
flavor_id = self.flavor_index_id[idx]
|
||||
image_id = self._settings['default_image']
|
||||
|
||||
name, ok = QInputDialog.getText(self,
|
||||
"New instance",
|
||||
"Choose a name for the instance and press Ok,\n"
|
||||
"then wait for the instance to appear in the inspector.")
|
||||
name, ok = QtGui.QInputDialog.getText(self,
|
||||
"New instance",
|
||||
"Choose a name for the instance and press Ok,\n"
|
||||
"then wait for the instance to appear in the inspector.")
|
||||
|
||||
if ok:
|
||||
create_thread = CreateInstanceThread(self, self._provider, name, flavor_id, image_id)
|
||||
create_thread.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
create_thread.start()
|
||||
self.createInstance(name, flavor_id, image_id)
|
||||
|
||||
def createInstance(self, instance_name, flavor_id, image_id):
|
||||
if not instance_name.endswith("-gns3"):
|
||||
instance_name += "-gns3"
|
||||
# TODO: Add a keys_dir to projectSettings
|
||||
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
|
||||
|
||||
builder = CloudBuilder(self, self._provider, ca_dir)
|
||||
builder.startAtCreate(instance_name, flavor_id, image_id)
|
||||
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
|
||||
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
|
||||
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
|
||||
builder.buildComplete.connect(self._instanceBuilt)
|
||||
builder.start()
|
||||
return builder
|
||||
|
||||
def _associateBuilderWithInstance(self, builder, instance_id):
|
||||
self._builders[instance_id] = builder
|
||||
|
||||
def _rebuild_instances(self, instances):
|
||||
# TODO: Add a keys_dir to projectSettings
|
||||
ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
|
||||
|
||||
for instance in instances:
|
||||
log.debug('CloudInspectorView._rebuild_instances {}'.format(instance.name))
|
||||
builder = CloudBuilder(self, self._provider, ca_dir)
|
||||
cloud_instance = CloudInstances.instance().get_instance(instance.id)
|
||||
public_key = cloud_instance.public_key
|
||||
private_key = cloud_instance.private_key
|
||||
# Fake a KeyPair object because we don't store it.
|
||||
keypair = namedtuple('KeyPair', ['private_key', 'public_key'])(private_key, public_key)
|
||||
builder.startAtSetup(instance, keypair)
|
||||
builder.instanceCreated.connect(self._main_window.add_instance_to_project)
|
||||
builder.instanceCreated.connect(CloudInstances.instance().add_instance)
|
||||
builder.instanceIdExists.connect(self._associateBuilderWithInstance)
|
||||
builder.instanceHasIP.connect(CloudInstances.instance().update_host_for_instance)
|
||||
builder.buildComplete.connect(self._instanceBuilt)
|
||||
builder.start()
|
||||
return builder
|
||||
|
||||
154
gns3/cloud_instances.py
Normal file
154
gns3/cloud_instances.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""
|
||||
Keeps track of all cloud instances the app has started.
|
||||
"""
|
||||
|
||||
from .qt import QtCore
|
||||
from gns3.topology import TopologyInstance
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudInstances(QtCore.QObject):
|
||||
"""
|
||||
This class stores the instances that gns3 gui has started. This can be different than the list
|
||||
of instances in the topology that can be changed when switching projects. This list is not touched
|
||||
when switching projects and is stored in the .ini file.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CloudInstances, self).__init__(*args, **kwargs)
|
||||
self._instances = []
|
||||
|
||||
|
||||
@staticmethod
|
||||
def instance():
|
||||
"""
|
||||
Singleton to return only one instance of CloudInstances.
|
||||
|
||||
:returns: instance of CloudInstances
|
||||
"""
|
||||
|
||||
if not hasattr(CloudInstances, "_instance"):
|
||||
CloudInstances._instance = CloudInstances()
|
||||
return CloudInstances._instance
|
||||
|
||||
@property
|
||||
def instances(self):
|
||||
return self._instances
|
||||
|
||||
def clear(self):
|
||||
self._instances.clear()
|
||||
|
||||
def add(self, topology_instance):
|
||||
self._instances.append(topology_instance)
|
||||
|
||||
def add_instance(self, instance, keypair):
|
||||
if instance is None:
|
||||
return
|
||||
existing = self.get_instance(instance.id)
|
||||
if existing is None:
|
||||
ti = TopologyInstance(instance.name, instance.id, instance.extra['flavorId'],
|
||||
instance.extra['imageId'], keypair.private_key, keypair.public_key)
|
||||
self._instances.append(ti)
|
||||
self.save()
|
||||
|
||||
def update_instances(self, instances):
|
||||
"""
|
||||
Compare with the existing list of instances to purge instances that no
|
||||
longer exist.
|
||||
"""
|
||||
save_needed = False
|
||||
# Look for instances that have been deleted
|
||||
for stored in self._instances:
|
||||
found = False
|
||||
for dynamic in instances:
|
||||
if stored.id == dynamic.id:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
self._instances.remove(stored)
|
||||
save_needed = True
|
||||
|
||||
if save_needed:
|
||||
self.save()
|
||||
|
||||
def update_host_for_instance(self, host, instance_id):
|
||||
"""
|
||||
Update the public IP for the instance.
|
||||
"""
|
||||
for instance in self.instances:
|
||||
if instance.id == instance_id:
|
||||
if instance.host != host:
|
||||
instance.host = host
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the list of cloud instances to the config file
|
||||
"""
|
||||
log.debug('Saving cloud instances')
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("CloudInstances")
|
||||
settings.remove("")
|
||||
|
||||
# Save the instances
|
||||
settings.beginWriteArray("cloud_instance", len(self._instances))
|
||||
index = 0
|
||||
for instance in self._instances:
|
||||
settings.setArrayIndex(index)
|
||||
for name in instance.fields():
|
||||
value = getattr(instance, name) if not None else ""
|
||||
log.debug('{}={}'.format(name, str(value)[0:60]))
|
||||
settings.setValue(name, value)
|
||||
index += 1
|
||||
settings.endArray()
|
||||
settings.endGroup()
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Load instance info from the config file to the topology
|
||||
"""
|
||||
log.debug('Loading cloud instances')
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("CloudInstances")
|
||||
|
||||
# Load the instances
|
||||
size = settings.beginReadArray("cloud_instance")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
info = {}
|
||||
for name in TopologyInstance.fields():
|
||||
value = settings.value(name, "")
|
||||
log.debug('{}={}'.format(name, str(value)[0:60]))
|
||||
info[name] = value
|
||||
ti = TopologyInstance(**info)
|
||||
self._instances.append(ti)
|
||||
|
||||
def get_instance(self, instance_id):
|
||||
"""
|
||||
Retrieve a TopologyInstance objects if present
|
||||
"""
|
||||
for i in self._instances:
|
||||
if i.id == instance_id:
|
||||
return i
|
||||
return None
|
||||
@@ -30,83 +30,83 @@ ip tcp synwait-time 5
|
||||
!
|
||||
interface Ethernet0/0
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/1
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/2
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet0/3
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/0
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/1
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/2
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Ethernet1/3
|
||||
no ip address
|
||||
shutdown
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/0
|
||||
interface Ethernet2/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/1
|
||||
interface Ethernet2/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/2
|
||||
interface Ethernet2/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial2/3
|
||||
interface Ethernet2/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/0
|
||||
interface Ethernet3/0
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/1
|
||||
interface Ethernet3/1
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/2
|
||||
interface Ethernet3/2
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Serial3/3
|
||||
interface Ethernet3/3
|
||||
no ip address
|
||||
shutdown
|
||||
serial restart-delay 0
|
||||
no shutdown
|
||||
duplex auto
|
||||
!
|
||||
interface Vlan1
|
||||
no ip address
|
||||
|
||||
@@ -218,6 +218,8 @@ class ConsoleCmd(cmd.Cmd):
|
||||
else:
|
||||
print("Activating debugging")
|
||||
root.addHandler(ch)
|
||||
from .main_window import MainWindow
|
||||
MainWindow.instance().setSettings({"debug_level": level})
|
||||
except:
|
||||
print(self.do_debug.__doc__)
|
||||
else:
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
from ..qt import QtCore, QtGui, QtWebKit
|
||||
from ..ui.getting_started_dialog_ui import Ui_GettingStartedDialog
|
||||
from ..utils.get_resource import get_resource
|
||||
|
||||
|
||||
class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
|
||||
@@ -39,7 +39,7 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
|
||||
self.uiWebView.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
|
||||
self.uiWebView.linkClicked.connect(self._urlClickedSlot)
|
||||
self.uiWebView.loadFinished.connect(self._loadFinishedSlot)
|
||||
self.uiCheckBox.setChecked(QtCore.QSettings().value("GUI/show_getting_started_dialog", True, type=bool))
|
||||
self.uiCheckBox.setChecked(QtCore.QSettings().value("GUI/hide_getting_started_dialog", False, type=bool))
|
||||
self._timer = QtCore.QTimer(self)
|
||||
self._timer.timeout.connect(self._loadFinishedSlot)
|
||||
self._timer.setSingleShot(True)
|
||||
@@ -53,7 +53,7 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self.uiCheckBox.isChecked()
|
||||
return not self.uiCheckBox.isChecked()
|
||||
|
||||
def done(self, result):
|
||||
"""
|
||||
@@ -62,7 +62,7 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
|
||||
:param result: ignored
|
||||
"""
|
||||
|
||||
QtCore.QSettings().setValue("GUI/show_getting_started_dialog", self.uiCheckBox.isChecked())
|
||||
QtCore.QSettings().setValue("GUI/hide_getting_started_dialog", self.uiCheckBox.isChecked())
|
||||
QtGui.QDialog.done(self, result)
|
||||
|
||||
def _urlClickedSlot(self, url):
|
||||
@@ -87,14 +87,10 @@ class GettingStartedDialog(QtGui.QDialog, Ui_GettingStartedDialog):
|
||||
self._timer.timeout.disconnect()
|
||||
if result is False:
|
||||
# load a local resource if the page is not available
|
||||
resource_name = os.path.join("static", "getting_started.html")
|
||||
getting_started = None
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
getting_started = os.path.normpath(resource_name)
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
getting_started_page = pkg_resources.resource_filename("gns3", resource_name)
|
||||
getting_started = os.path.normpath(getting_started_page)
|
||||
if getting_started:
|
||||
getting_started = get_resource(os.path.join("static", "getting_started.html"))
|
||||
if getting_started and not (sys.platform.startswith("win") and not sys.maxsize > 2 ** 32):
|
||||
# do not show the page on Windows 32-bit (crash when no Internet connection)
|
||||
self.uiWebView.load(QtCore.QUrl("file://{}".format(getting_started)))
|
||||
else:
|
||||
self.uiCheckBox.setChecked(True)
|
||||
self.accept()
|
||||
|
||||
69
gns3/dialogs/import_cloud_project_dialog.py
Normal file
69
gns3/dialogs/import_cloud_project_dialog.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Dialog for importing cloud projects
|
||||
"""
|
||||
|
||||
from ..ui.import_cloud_project_dialog_ui import Ui_ImportCloudProjectDialog
|
||||
from ..qt import QtGui
|
||||
from ..cloud.utils import get_cloud_projects, DownloadProjectThread, DeleteProjectThread
|
||||
from ..utils.progress_dialog import ProgressDialog
|
||||
|
||||
|
||||
class ImportCloudProjectDialog(QtGui.QDialog, Ui_ImportCloudProjectDialog):
|
||||
"""
|
||||
Import cloud project dialog implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, project_dest_path, images_dest_path, cloud_settings):
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.project_dest_path = project_dest_path
|
||||
self.images_dest_path = images_dest_path
|
||||
self.cloud_settings = cloud_settings
|
||||
|
||||
self.uiImportProjectAction.clicked.connect(self._importProject)
|
||||
self.uiDeleteProjectAction.clicked.connect(self._deleteProject)
|
||||
self._listCloudProjects()
|
||||
|
||||
def _listCloudProjects(self):
|
||||
self.listWidget.clear()
|
||||
self.projects = get_cloud_projects(self.cloud_settings)
|
||||
self.listWidget.addItems(list(self.projects.keys()))
|
||||
|
||||
def _importProject(self):
|
||||
project_file_name = self.projects[self.listWidget.currentItem().text()]
|
||||
|
||||
download_thread = DownloadProjectThread(
|
||||
self,
|
||||
project_file_name,
|
||||
self.project_dest_path,
|
||||
self.images_dest_path,
|
||||
self.cloud_settings
|
||||
)
|
||||
progress_dialog = ProgressDialog(download_thread, "Importing project", "Downloading project files...", "Cancel",
|
||||
parent=self.parent())
|
||||
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
self.close()
|
||||
|
||||
def _deleteProject(self):
|
||||
project_file_name = self.projects[self.listWidget.currentItem().text()]
|
||||
|
||||
button_clicked = QtGui.QMessageBox.question(
|
||||
self,
|
||||
"Delete project",
|
||||
"Are you sure you want to delete project " + self.listWidget.currentItem().text(),
|
||||
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
|
||||
QtGui.QMessageBox.Yes
|
||||
)
|
||||
|
||||
if button_clicked == QtGui.QMessageBox.Yes:
|
||||
delete_project_thread = DeleteProjectThread(self, project_file_name, self.cloud_settings)
|
||||
progress_dialog = ProgressDialog(delete_project_thread, "Deleting project", "Deleting project files...",
|
||||
"Cancel", parent=self)
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
|
||||
self._listCloudProjects()
|
||||
@@ -19,14 +19,19 @@ import os
|
||||
import shutil
|
||||
from ..qt import QtCore, QtGui
|
||||
from ..ui.new_project_dialog_ui import Ui_NewProjectDialog
|
||||
from ..settings import ENABLE_CLOUD
|
||||
|
||||
|
||||
class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
"""
|
||||
New project dialog.
|
||||
|
||||
:param parent: parent widget.
|
||||
:param showed_from_startup: boolean to indicate if this dialog
|
||||
has been opened automatically when GNS3 started.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent, showed_from_startup=False):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
@@ -39,6 +44,14 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
|
||||
self.uiNameLineEdit.textEdited.connect(self._projectNameSlot)
|
||||
self.uiLocationBrowserToolButton.clicked.connect(self._projectPathSlot)
|
||||
self.uiOpenProjectPushButton.clicked.connect(self._openProjectActionSlot)
|
||||
self.uiRecentProjectsPushButton.clicked.connect(self._showRecentProjectsSlot)
|
||||
if not ENABLE_CLOUD:
|
||||
self.uiCloudRadioButton.hide()
|
||||
|
||||
if not showed_from_startup:
|
||||
self.uiOpenProjectPushButton.hide()
|
||||
self.uiRecentProjectsPushButton.hide()
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
"""
|
||||
@@ -68,6 +81,35 @@ class NewProjectDialog(QtGui.QDialog, Ui_NewProjectDialog):
|
||||
|
||||
return self._project_settings
|
||||
|
||||
def _menuTriggeredSlot(self, action):
|
||||
"""
|
||||
Closes this dialog when a recent project
|
||||
has been opened.
|
||||
|
||||
:param action: ignored.
|
||||
"""
|
||||
|
||||
self.reject()
|
||||
|
||||
def _openProjectActionSlot(self):
|
||||
"""
|
||||
Opens a project and closes this dialog.
|
||||
"""
|
||||
|
||||
self._main_window.openProjectActionSlot()
|
||||
self.reject()
|
||||
|
||||
def _showRecentProjectsSlot(self):
|
||||
"""
|
||||
lot to show all the recent projects in a menu.
|
||||
"""
|
||||
|
||||
menu = QtGui.QMenu()
|
||||
menu.triggered.connect(self._menuTriggeredSlot)
|
||||
for action in self._main_window._recent_file_actions:
|
||||
menu.addAction(action)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
if result:
|
||||
|
||||
@@ -26,6 +26,7 @@ from ..pages.general_preferences_page import GeneralPreferencesPage
|
||||
from ..pages.cloud_preferences_page import CloudPreferencesPage
|
||||
from ..pages.packet_capture_preferences_page import PacketCapturePreferencesPage
|
||||
from ..modules import MODULES
|
||||
from ..settings import ENABLE_CLOUD
|
||||
|
||||
|
||||
class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
@@ -58,8 +59,9 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
GeneralPreferencesPage,
|
||||
ServerPreferencesPage,
|
||||
PacketCapturePreferencesPage,
|
||||
#CloudPreferencesPage,
|
||||
]
|
||||
if ENABLE_CLOUD:
|
||||
pages.append(CloudPreferencesPage)
|
||||
|
||||
for page in pages:
|
||||
preferences_page = page()
|
||||
@@ -87,6 +89,9 @@ class PreferencesDialog(QtGui.QDialog, Ui_PreferencesDialog):
|
||||
if cls is preference_pages[0]:
|
||||
parent = item
|
||||
|
||||
# expand all items by default
|
||||
self.uiTreeWidget.expandAll()
|
||||
|
||||
def _showPreferencesPageSlot(self, current, previous):
|
||||
"""
|
||||
Shows a preference page in the current dialog.
|
||||
|
||||
@@ -32,7 +32,7 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
:param items: list of items
|
||||
"""
|
||||
|
||||
def __init__(self, parent, items=None):
|
||||
def __init__(self, parent, items=None, symbol=None, category=None):
|
||||
|
||||
QtGui.QDialog.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
@@ -40,6 +40,8 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
self._items = items
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self._applyPreferencesSlot)
|
||||
|
||||
selected_symbol = symbol
|
||||
selected_category = category
|
||||
if not self._items:
|
||||
self.uiButtonBox.button(QtGui.QDialogButtonBox.Apply).hide()
|
||||
|
||||
@@ -50,20 +52,39 @@ class SymbolSelectionDialog(QtGui.QDialog, Ui_SymbolSelectionDialog):
|
||||
"Security devices": Node.security_devices
|
||||
}
|
||||
|
||||
index = 0
|
||||
for name, category in categories.items():
|
||||
self.uiCategoryComboBox.addItem(name, category)
|
||||
if category == selected_category:
|
||||
self.uiCategoryComboBox.setCurrentIndex(index)
|
||||
index += 1
|
||||
else:
|
||||
self.uiCategoryLabel.hide()
|
||||
self.uiCategoryComboBox.hide()
|
||||
custom_symbol = items[0].defaultRenderer().objectName()
|
||||
if not custom_symbol:
|
||||
symbol_name = items[0].node().defaultSymbol()
|
||||
else:
|
||||
symbol_name = custom_symbol
|
||||
selected_symbol = symbol_name
|
||||
|
||||
self.uiSymbolListWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
symbol_resources = QtCore.QResource(":/symbols")
|
||||
for symbol in symbol_resources.children():
|
||||
if symbol.endswith('.normal.svg'):
|
||||
if symbol.endswith(".normal.svg"):
|
||||
name = symbol[:-11]
|
||||
item = QtGui.QListWidgetItem(self.uiSymbolListWidget)
|
||||
item.setText(name)
|
||||
item.setIcon(QtGui.QIcon(':/symbols/' + symbol))
|
||||
resource_path = ":/symbols/" + symbol
|
||||
svg_renderer = QtSvg.QSvgRenderer(resource_path)
|
||||
if resource_path == selected_symbol:
|
||||
self.uiSymbolListWidget.setCurrentItem(item)
|
||||
image = QtGui.QImage(64, 64, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(icon)
|
||||
|
||||
def _applyPreferencesSlot(self):
|
||||
"""
|
||||
|
||||
@@ -53,6 +53,10 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
if not first_item.editable():
|
||||
self.uiPlainTextEdit.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
|
||||
|
||||
if len(self._items) == 1:
|
||||
self.uiApplyTextToAllItemsCheckBox.setChecked(True)
|
||||
self.uiApplyTextToAllItemsCheckBox.hide()
|
||||
|
||||
def _setFontSlot(self):
|
||||
"""
|
||||
Slot to select the font.
|
||||
@@ -82,7 +86,7 @@ class TextEditorDialog(QtGui.QDialog, Ui_TextEditorDialog):
|
||||
item.setDefaultTextColor(self._color)
|
||||
item.setFont(self.uiPlainTextEdit.font())
|
||||
item.setRotation(self.uiRotationSpinBox.value())
|
||||
if item.editable():
|
||||
if item.editable() and self.uiApplyTextToAllItemsCheckBox.isChecked():
|
||||
item.setPlainText(self.uiPlainTextEdit.toPlainText())
|
||||
|
||||
def done(self, result):
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
Graphical view on the scene where items are drawn.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
|
||||
@@ -52,6 +53,8 @@ from .items.rectangle_item import RectangleItem
|
||||
from .items.ellipse_item import EllipseItem
|
||||
from .items.image_item import ImageItem
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GraphicsView(QtGui.QGraphicsView):
|
||||
"""
|
||||
@@ -605,23 +608,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
item = self.itemAt(event.pos())
|
||||
if not self._adding_link and isinstance(item, NodeItem) and item.node().initialized():
|
||||
item.setSelected(True)
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized() and item.node().status() == Node.started:
|
||||
if hasattr(item.node(), "serialConsole") and item.node().serialConsole():
|
||||
try:
|
||||
from .serial_console import serialConsole
|
||||
serialConsole(item.node().name())
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
|
||||
else:
|
||||
node = item.node()
|
||||
name = node.name()
|
||||
console_port = node.console()
|
||||
console_host = node.server().host
|
||||
try:
|
||||
from .telnet_console import telnetConsole
|
||||
telnetConsole(name, console_host, console_port)
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
if isinstance(item, NodeItem):
|
||||
self.consoleToNode(item.node())
|
||||
else:
|
||||
self.configureSlot()
|
||||
else:
|
||||
@@ -724,19 +712,42 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
configure_action.triggered.connect(self.configureActionSlot)
|
||||
menu.addAction(configure_action)
|
||||
|
||||
# Action: Change hostname
|
||||
change_hostname_action = QtGui.QAction("Change hostname", menu)
|
||||
change_hostname_action.setIcon(QtGui.QIcon(':/icons/show-hostname.svg'))
|
||||
self.connect(change_hostname_action, QtCore.SIGNAL('triggered()'), self.changeHostnameActionSlot)
|
||||
menu.addAction(change_hostname_action)
|
||||
|
||||
# Action: Change symbol
|
||||
change_symbol_action = QtGui.QAction("Change symbol", menu)
|
||||
change_symbol_action.setIcon(QtGui.QIcon(':/icons/node_conception.svg'))
|
||||
self.connect(change_symbol_action, QtCore.SIGNAL('triggered()'), self.changeSymbolActionSlot)
|
||||
menu.addAction(change_symbol_action)
|
||||
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "console"), items)):
|
||||
console_action = QtGui.QAction("Console", menu)
|
||||
console_action.setIcon(QtGui.QIcon(':/icons/console.svg'))
|
||||
console_action.triggered.connect(self.consoleActionSlot)
|
||||
menu.addAction(console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "auxConsole"), items)):
|
||||
aux_console_action = QtGui.QAction("Auxiliary console", menu)
|
||||
aux_console_action.setIcon(QtGui.QIcon(':/icons/aux-console.svg'))
|
||||
aux_console_action.triggered.connect(self.auxConsoleActionSlot)
|
||||
menu.addAction(aux_console_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "importConfig"), items)):
|
||||
import_config_action = QtGui.QAction("Import config", menu)
|
||||
import_config_action.setIcon(QtGui.QIcon(':/icons/import_config.svg'))
|
||||
import_config_action.triggered.connect(self.importConfigActionSlot)
|
||||
menu.addAction(import_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "exportConfig"), items)):
|
||||
export_config_action = QtGui.QAction("Export config", menu)
|
||||
export_config_action.setIcon(QtGui.QIcon(':/icons/export_config.svg'))
|
||||
export_config_action.triggered.connect(self.exportConfigActionSlot)
|
||||
menu.addAction(export_config_action)
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "startPacketCapture"), items)):
|
||||
capture_action = QtGui.QAction("Capture", menu)
|
||||
capture_action.setIcon(QtGui.QIcon(':/icons/inspect.svg'))
|
||||
@@ -751,7 +762,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
|
||||
if True in list(map(lambda item: isinstance(item, NodeItem) and hasattr(item.node(), "start"), items)):
|
||||
start_action = QtGui.QAction("Start", menu)
|
||||
start_action.setIcon(QtGui.QIcon(':/icons/play.svg'))
|
||||
start_action.setIcon(QtGui.QIcon(':/icons/start.svg'))
|
||||
start_action.triggered.connect(self.startActionSlot)
|
||||
menu.addAction(start_action)
|
||||
|
||||
@@ -863,6 +874,22 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if items:
|
||||
self.configureSlot(items)
|
||||
|
||||
def changeHostnameActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the change hostname action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and item.node().initialized():
|
||||
new_hostname, ok = QtGui.QInputDialog.getText(self, "Change hostname", "Hostname:", QtGui.QLineEdit.Normal, item.node().name())
|
||||
if ok:
|
||||
if hasattr(item.node(), "validateHostname"):
|
||||
if not item.node().validateHostname(new_hostname):
|
||||
QtGui.QMessageBox.critical(self, "Change hostname", "Invalid name detected for this node: {}".format(new_hostname))
|
||||
continue
|
||||
item.node().update({"name": new_hostname})
|
||||
|
||||
def changeSymbolActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the change symbol action in the
|
||||
@@ -878,33 +905,152 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
|
||||
def consoleToNode(self, node, aux=False):
|
||||
"""
|
||||
Start a console application to connect to a node.
|
||||
|
||||
:param node: Node instance
|
||||
:param aux: auxiliary console mode
|
||||
|
||||
:returns: False if the console application could not be started
|
||||
"""
|
||||
|
||||
if not hasattr(node, "console") or not node.initialized() or node.status() != Node.started:
|
||||
# returns True to ignore this node.
|
||||
return True
|
||||
|
||||
if aux and not hasattr(node, "auxConsole"):
|
||||
# returns True to ignore this node.
|
||||
return True
|
||||
|
||||
if hasattr(node, "serialConsole") and node.serialConsole():
|
||||
try:
|
||||
from .serial_console import serialConsole
|
||||
serialConsole(node.name())
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
|
||||
return False
|
||||
else:
|
||||
name = node.name()
|
||||
if aux:
|
||||
console_port = node.auxConsole()
|
||||
else:
|
||||
console_port = node.console()
|
||||
console_host = node.server().host
|
||||
try:
|
||||
from .telnet_console import telnetConsole
|
||||
|
||||
telnet_callback = None
|
||||
|
||||
try:
|
||||
if node.server().isCloud():
|
||||
# override target host with localhost
|
||||
console_host = '127.0.0.1'
|
||||
ep = node.server().tunnel.add_endpoint(console_host, console_port)
|
||||
# override target port with local tunneled port
|
||||
local_addr, _ = ep.get()
|
||||
console_port = local_addr[1]
|
||||
|
||||
def cb(*args, **kwargs):
|
||||
node.server().tunnel.remove_endpoint(ep)
|
||||
log.debug('Console DONE on port {}'.format(args[2]))
|
||||
telnet_callback = cb
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
telnetConsole(name, console_host, console_port, telnet_callback)
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def consoleActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the console action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
from .telnet_console import telnetConsole
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "console") and item.node().initialized():
|
||||
if hasattr(item.node(), "serialConsole") and item.node().serialConsole():
|
||||
try:
|
||||
from .serial_console import serialConsole
|
||||
serialConsole(item.node().name())
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start serial console application: {}".format(e))
|
||||
else:
|
||||
node = item.node()
|
||||
if node.status() != Node.started:
|
||||
continue
|
||||
name = node.name()
|
||||
console_port = node.console()
|
||||
console_host = node.server().host
|
||||
try:
|
||||
telnetConsole(name, console_host, console_port)
|
||||
except (OSError, ValueError) as e:
|
||||
QtGui.QMessageBox.critical(self, "Console", "Cannot start console application: {}".format(e))
|
||||
break
|
||||
if isinstance(item, NodeItem):
|
||||
if self.consoleToNode(item.node()):
|
||||
continue
|
||||
|
||||
def auxConsoleActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the auxiliary console action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem):
|
||||
if self.consoleToNode(item.node(), aux=True):
|
||||
continue
|
||||
|
||||
def importConfigActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the import config action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "importConfig") and item.node().initialized():
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
return
|
||||
|
||||
if len(items) > 1:
|
||||
path = QtGui.QFileDialog.getExistingDirectory(self, "Import directory", ".", QtGui.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
for item in items:
|
||||
item.node().importConfigFromDirectory(path)
|
||||
else:
|
||||
item = items[0]
|
||||
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
|
||||
"Import config",
|
||||
".",
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
if path:
|
||||
item.node().importConfig(path)
|
||||
if hasattr(item.node(), "importPrivateConfig"):
|
||||
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
|
||||
"Import private-config",
|
||||
".",
|
||||
"All files (*.*);;Config files (*.cfg)",
|
||||
"Config files (*.cfg)")
|
||||
if path:
|
||||
item.node().importPrivateConfig(path)
|
||||
|
||||
def exportConfigActionSlot(self):
|
||||
"""
|
||||
Slot to receive events from the export config action in the
|
||||
contextual menu.
|
||||
"""
|
||||
|
||||
items = []
|
||||
for item in self.scene().selectedItems():
|
||||
if isinstance(item, NodeItem) and hasattr(item.node(), "exportConfig") and item.node().initialized():
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
return
|
||||
|
||||
if len(items) > 1:
|
||||
path = QtGui.QFileDialog.getExistingDirectory(self, "Export directory", ".", QtGui.QFileDialog.ShowDirsOnly)
|
||||
if path:
|
||||
for item in items:
|
||||
item.node().exportConfigToDirectory(path)
|
||||
else:
|
||||
item = items[0]
|
||||
config_path = QtGui.QFileDialog.getSaveFileName(self, "Export config")
|
||||
if hasattr(item.node(), "importPrivateConfig"):
|
||||
private_config_path = QtGui.QFileDialog.getSaveFileName(self, "Export private-config")
|
||||
item.node().exportConfig(config_path, private_config_path)
|
||||
else:
|
||||
item.node().exportConfig(config_path)
|
||||
|
||||
def captureActionSlot(self):
|
||||
"""
|
||||
@@ -1104,6 +1250,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
"""
|
||||
|
||||
try:
|
||||
log.debug('In createNode')
|
||||
node_module = None
|
||||
for module in MODULES:
|
||||
instance = module.instance()
|
||||
@@ -1119,9 +1266,18 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
server = node_module.allocateServer(node_class)
|
||||
elif node_data["server"] == "local":
|
||||
server = Servers.instance().localServer()
|
||||
elif node_data["server"] == "cloud":
|
||||
server = Servers.instance().anyCloudServer()
|
||||
else:
|
||||
host, port = node_data["server"].rsplit(":", 1)
|
||||
try:
|
||||
host, port = node_data["server"].rsplit(":", 1)
|
||||
except ValueError:
|
||||
raise ModuleError("Wrong format for server: '{}', please recreate the node in preferences".format(node_data["server"]))
|
||||
server = Servers.instance().getRemoteServer(host, port)
|
||||
|
||||
if server is None:
|
||||
return
|
||||
|
||||
if not server.connected() and ConnectToServer(self, server) is False:
|
||||
return
|
||||
|
||||
|
||||
@@ -224,7 +224,10 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
when a the node has been updated.
|
||||
"""
|
||||
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
if self._node_label:
|
||||
if self._node_label.toPlainText() != self._node.name():
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
self._centerLabel()
|
||||
self.setUnsavedState()
|
||||
|
||||
# update the link tooltips in case the
|
||||
@@ -307,6 +310,19 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
|
||||
self._node_label = label
|
||||
|
||||
def _centerLabel(self):
|
||||
"""
|
||||
Centers the node label.
|
||||
"""
|
||||
|
||||
text_rect = self._node_label.boundingRect()
|
||||
text_middle = text_rect.topRight() / 2
|
||||
node_rect = self.boundingRect()
|
||||
node_middle = node_rect.topRight() / 2
|
||||
label_x_pos = node_middle.x() - text_middle.x()
|
||||
label_y_pos = -25
|
||||
self._node_label.setPos(label_x_pos, label_y_pos)
|
||||
|
||||
def _showLabel(self):
|
||||
"""
|
||||
Shows the node label on the scene.
|
||||
@@ -316,13 +332,7 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
self._node_label = NoteItem(self)
|
||||
self._node_label.setEditable(False)
|
||||
self._node_label.setPlainText(self._node.name())
|
||||
text_rect = self._node_label.boundingRect()
|
||||
text_middle = text_rect.topRight() / 2
|
||||
node_rect = self.boundingRect()
|
||||
node_middle = node_rect.topRight() / 2
|
||||
label_x_pos = node_middle.x() - text_middle.x()
|
||||
label_y_pos = -25
|
||||
self._node_label.setPos(label_x_pos, label_y_pos)
|
||||
self._centerLabel()
|
||||
|
||||
def connectToPort(self, unavailable_ports=[]):
|
||||
"""
|
||||
@@ -340,29 +350,33 @@ class NodeItem(QtSvg.QGraphicsSvgItem):
|
||||
QtGui.QMessageBox.critical(self.scene().parent(), "Link", "No port available, please configure this device")
|
||||
return None
|
||||
|
||||
# sort by port name
|
||||
port_names = {}
|
||||
# sort the ports
|
||||
ports_dict = {}
|
||||
for port in ports:
|
||||
port_names[port.name()] = port
|
||||
if port.slotNumber() is not None:
|
||||
# multiply the slot number by 16 to make the port number unique.
|
||||
ports_dict[(port.slotNumber() * 16) + port.portNumber()] = port
|
||||
elif port.portNumber()is not None:
|
||||
ports_dict[port.portNumber()] = port
|
||||
else:
|
||||
ports_dict[port.name()] = port
|
||||
|
||||
try:
|
||||
# try a numeric sort first
|
||||
ports = sorted(port_names.keys(), key=int)
|
||||
ports = sorted(ports_dict.keys(), key=int)
|
||||
except ValueError:
|
||||
# fall back to a classic sort
|
||||
ports = sorted(port_names.keys())
|
||||
ports = sorted(ports_dict.keys())
|
||||
|
||||
# show a contextual menu for the user to choose a port
|
||||
for port in ports:
|
||||
port_object = port_names[port]
|
||||
port_object = ports_dict[port]
|
||||
if port in unavailable_ports:
|
||||
# this port cannot be chosen by the user (grayed out)
|
||||
action = menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port)
|
||||
action = menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
|
||||
action.setDisabled(True)
|
||||
elif port_object.isFree():
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_red.svg'), port)
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_red.svg'), port_object.name())
|
||||
else:
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port)
|
||||
menu.addAction(QtGui.QIcon(':/icons/led_green.svg'), port_object.name())
|
||||
|
||||
menu.triggered.connect(self.selectedPortSlot)
|
||||
menu.exec_(QtGui.QCursor.pos())
|
||||
|
||||
@@ -280,7 +280,7 @@ class ShapeItem:
|
||||
color = QtGui.QColor(color)
|
||||
else:
|
||||
color = QtGui.QColor(255, 255, 255)
|
||||
if transparency:
|
||||
if transparency is not None:
|
||||
color.setAlpha(transparency)
|
||||
self.setBrush(QtGui.QBrush(color))
|
||||
|
||||
@@ -293,8 +293,8 @@ class ShapeItem:
|
||||
border_color.setAlpha(border_transparency)
|
||||
pen.setColor(border_color)
|
||||
if border_width is not None:
|
||||
pen.setWidth(border_width)
|
||||
if border_style:
|
||||
pen.setWidth(int(border_width))
|
||||
if border_style is not None:
|
||||
pen.setStyle(QtCore.Qt.PenStyle(border_style))
|
||||
self.setPen(pen)
|
||||
|
||||
|
||||
86
gns3/main.py
86
gns3/main.py
@@ -82,11 +82,15 @@ def main():
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--version', help="show the version", action='version', version=__version__)
|
||||
parser.add_argument('--debug', help="print out debug messages", action='store_true', default=False)
|
||||
parser.add_argument("project", help="load a GNS3 project (.gns3)", metavar="path", nargs="?")
|
||||
parser.add_argument("--version", help="show the version", action="version", version=__version__)
|
||||
parser.add_argument("--debug", help="print out debug messages", action="store_true", default=False)
|
||||
options = parser.parse_args()
|
||||
exception_file_path = "exception.log"
|
||||
|
||||
if options.project and hasattr(sys, "frozen"):
|
||||
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
|
||||
|
||||
def exceptionHook(exception, value, tb):
|
||||
|
||||
if exception == KeyboardInterrupt:
|
||||
@@ -103,7 +107,7 @@ def main():
|
||||
logfile.write("".join(lines))
|
||||
logfile.close()
|
||||
except OSError as e:
|
||||
print("Could not save traceback to {}: {}".format(exception_file_path, e))
|
||||
print("Could not save traceback to {}: {}".format(os.path.normpath(exception_file_path), e))
|
||||
|
||||
if not sys.stdout.isatty():
|
||||
# if stdout is not a tty (redirected to the console view),
|
||||
@@ -156,7 +160,7 @@ def main():
|
||||
if sys.platform.startswith('win') or sys.platform.startswith('darwin'):
|
||||
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if sys.platform.startswith('win') and hasattr(sys, "frozen"):
|
||||
try:
|
||||
import win32console
|
||||
import win32con
|
||||
@@ -165,56 +169,56 @@ def main():
|
||||
raise RuntimeError("Python for Windows extensions must be installed.")
|
||||
|
||||
try:
|
||||
win32console.AllocConsole()
|
||||
# hide the console
|
||||
console_window = win32console.GetConsoleWindow()
|
||||
win32gui.ShowWindow(console_window, win32con.SW_HIDE)
|
||||
except win32console.error as e:
|
||||
print("warning: could not allocate console: {}".format(e))
|
||||
|
||||
exit_code = MainWindow.exit_code_reboot
|
||||
while exit_code == MainWindow.exit_code_reboot:
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
|
||||
exit_code = 0
|
||||
app = QtGui.QApplication(sys.argv)
|
||||
# this info is necessary for QSettings
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
|
||||
# this info is necessary for QSettings
|
||||
app.setOrganizationName("GNS3")
|
||||
app.setOrganizationDomain("gns3.net")
|
||||
app.setApplicationName("GNS3")
|
||||
app.setApplicationVersion(__version__)
|
||||
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "GNS3_client.log")
|
||||
# save client logging info to a file
|
||||
logfile = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "GNS3_client.log") # FIXME: does it work?
|
||||
try:
|
||||
try:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(QtCore.QSettings().fileName()))
|
||||
except FileExistsError:
|
||||
pass
|
||||
handler = logging.FileHandler(logfile, "w")
|
||||
if options.debug:
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
os.makedirs(os.path.dirname(QtCore.QSettings().fileName()))
|
||||
except FileExistsError:
|
||||
pass
|
||||
handler = logging.FileHandler(logfile, "w")
|
||||
if options.debug:
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
if len(root_logger.handlers) > 0:
|
||||
root_handler = root_logger.handlers[0]
|
||||
root_handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
log.info('Log level: {}'.format(logging.getLevelName(log.getEffectiveLevel())))
|
||||
root_handler = logging.StreamHandler()
|
||||
root_logger.addHandler(root_handler)
|
||||
root_handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
log.info('Log level: {}'.format(logging.getLevelName(log.getEffectiveLevel())))
|
||||
|
||||
formatter = logging.Formatter("[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s",
|
||||
datefmt="%y%m%d %H:%M:%S")
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
except OSError as e:
|
||||
log.warn("could not log to {}: {}".format(logfile, e))
|
||||
formatter = logging.Formatter("[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d] %(message)s",
|
||||
datefmt="%y%m%d %H:%M:%S")
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
except OSError as e:
|
||||
log.warn("could not log to {}: {}".format(logfile, e))
|
||||
|
||||
# update the exception file path to have it in the same directory as the settings file.
|
||||
exception_file_path = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), exception_file_path)
|
||||
# update the exception file path to have it in the same directory as the settings file.
|
||||
exception_file_path = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), exception_file_path)
|
||||
|
||||
mainwindow = MainWindow.instance()
|
||||
mainwindow.show()
|
||||
exit_code = app.exec_()
|
||||
delattr(MainWindow, "_instance")
|
||||
app.deleteLater()
|
||||
mainwindow = MainWindow(options.project)
|
||||
mainwindow.show()
|
||||
exit_code = app.exec_()
|
||||
delattr(MainWindow, "_instance")
|
||||
app.deleteLater()
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ from gns3.servers import Servers
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .cloud import Cloud
|
||||
from .host import Host
|
||||
|
||||
|
||||
import logging
|
||||
@@ -241,7 +242,7 @@ class Builtin(Module):
|
||||
:returns: list of classes
|
||||
"""
|
||||
|
||||
return [Cloud]
|
||||
return [Cloud, Host]
|
||||
|
||||
def nodes(self):
|
||||
"""
|
||||
|
||||
@@ -211,53 +211,55 @@ class Cloud(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
nios = new_settings["nios"]
|
||||
|
||||
updated = False
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
nio_object = None
|
||||
if nio.lower().startswith("nio_udp"):
|
||||
nio_object = self._createNIOUDP(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
nio_object = self._createNIOGenericEthernet(nio)
|
||||
if nio.lower().startswith("nio_gen_linux"):
|
||||
nio_object = self._createNIOLinuxEthernet(nio)
|
||||
if nio.lower().startswith("nio_tap"):
|
||||
nio_object = self._createNIOTAP(nio)
|
||||
if nio.lower().startswith("nio_unix"):
|
||||
nio_object = self._createNIOUNIX(nio)
|
||||
if nio.lower().startswith("nio_vde"):
|
||||
nio_object = self._createNIOVDE(nio)
|
||||
if nio.lower().startswith("nio_null"):
|
||||
nio_object = self._createNIONull(nio)
|
||||
if nio_object == None:
|
||||
log.error("Could not create NIO object from {}".format(nio))
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(nio))
|
||||
if "nios" in new_settings:
|
||||
nios = new_settings["nios"]
|
||||
|
||||
# delete ports
|
||||
for nio in self._settings["nios"]:
|
||||
if nio not in nios:
|
||||
for port in self._ports.copy():
|
||||
if port.name() == nio:
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been deleted".format(nio))
|
||||
break
|
||||
# add ports
|
||||
for nio in nios:
|
||||
if nio in self._settings["nios"]:
|
||||
# port already created for this NIO
|
||||
continue
|
||||
nio_object = None
|
||||
if nio.lower().startswith("nio_udp"):
|
||||
nio_object = self._createNIOUDP(nio)
|
||||
if nio.lower().startswith("nio_gen_eth"):
|
||||
nio_object = self._createNIOGenericEthernet(nio)
|
||||
if nio.lower().startswith("nio_gen_linux"):
|
||||
nio_object = self._createNIOLinuxEthernet(nio)
|
||||
if nio.lower().startswith("nio_tap"):
|
||||
nio_object = self._createNIOTAP(nio)
|
||||
if nio.lower().startswith("nio_unix"):
|
||||
nio_object = self._createNIOUNIX(nio)
|
||||
if nio.lower().startswith("nio_vde"):
|
||||
nio_object = self._createNIOVDE(nio)
|
||||
if nio.lower().startswith("nio_null"):
|
||||
nio_object = self._createNIONull(nio)
|
||||
if nio_object is None:
|
||||
log.error("Could not create NIO object from {}".format(nio))
|
||||
continue
|
||||
port = Port(nio, nio_object, stub=True)
|
||||
port.setStatus(Port.started)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(nio))
|
||||
|
||||
# delete ports
|
||||
for nio in self._settings["nios"]:
|
||||
if nio not in nios:
|
||||
for port in self._ports.copy():
|
||||
if port.name() == nio:
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been deleted".format(nio))
|
||||
break
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
self._settings["name"] = new_settings["name"]
|
||||
updated = True
|
||||
|
||||
self._settings["nios"] = new_settings["nios"].copy()
|
||||
if updated:
|
||||
log.info("cloud {} has been updated".format(self.name()))
|
||||
self.updated_signal.emit()
|
||||
|
||||
116
gns3/modules/builtin/host.py
Normal file
116
gns3/modules/builtin/host.py
Normal file
@@ -0,0 +1,116 @@
|
||||
# -*- 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 gns3.node import Node
|
||||
from .cloud import Cloud
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Host(Cloud):
|
||||
"""
|
||||
Pseudo host based on a Dynamips Cloud.
|
||||
|
||||
:param module: parent module for this node
|
||||
:param server: GNS3 server instance
|
||||
"""
|
||||
|
||||
_name_instance_count = 1
|
||||
|
||||
def __init__(self, module, server):
|
||||
Cloud.__init__(self, module, server)
|
||||
|
||||
log.info("host is being created")
|
||||
# create an unique id and name
|
||||
self._name_id = Host._name_instance_count
|
||||
Host._name_instance_count += 1
|
||||
|
||||
name = "Host{}".format(self._name_id)
|
||||
self._settings["name"] = name
|
||||
|
||||
def setup(self, name=None, initial_settings={}):
|
||||
"""
|
||||
Setups this host.
|
||||
|
||||
:param name: optional name for this host
|
||||
"""
|
||||
|
||||
if name:
|
||||
self._settings["name"] = name
|
||||
|
||||
if initial_settings:
|
||||
self._initial_settings = initial_settings
|
||||
else:
|
||||
self.created_signal.connect(self._autoConfigure)
|
||||
self._server.send_message("builtin.interfaces", None, self._setupCallback)
|
||||
|
||||
def _autoConfigure(self, node_id):
|
||||
"""
|
||||
Auto adds all Ethernet and TAP interfaces.
|
||||
|
||||
:param node_id: ignored
|
||||
"""
|
||||
|
||||
new_settings = {"nios": []}
|
||||
for interface in self._settings["interfaces"]:
|
||||
if interface["name"].startswith("tap"):
|
||||
new_settings["nios"].append("nio_tap:{}".format(interface["name"]))
|
||||
else:
|
||||
new_settings["nios"].append("nio_gen_eth:{}".format(interface["name"]))
|
||||
|
||||
self.update(new_settings)
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
Returns the default symbol path for this host.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/computer.normal.svg"
|
||||
|
||||
@staticmethod
|
||||
def hoverSymbol():
|
||||
"""
|
||||
Returns the symbol to use when the host is hovered.
|
||||
|
||||
:returns: symbol path (or resource).
|
||||
"""
|
||||
|
||||
return ":/symbols/computer.selected.svg"
|
||||
|
||||
@staticmethod
|
||||
def symbolName():
|
||||
|
||||
return "Host"
|
||||
|
||||
@staticmethod
|
||||
def categories():
|
||||
"""
|
||||
Returns the node categories the node is part of (used by the device panel).
|
||||
|
||||
:returns: list of node category (integer)
|
||||
"""
|
||||
|
||||
return [Node.end_devices]
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return "Host"
|
||||
@@ -474,6 +474,9 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiGenericEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in settings["interfaces"]:
|
||||
if interface["name"].startswith("tap"):
|
||||
# do not add TAP interfaces
|
||||
continue
|
||||
self.uiGenericEthernetComboBox.addItem(interface["name"])
|
||||
self.uiGenericEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
@@ -483,7 +486,7 @@ class CloudConfigurationPage(QtGui.QWidget, Ui_cloudConfigPageWidget):
|
||||
self.uiLinuxEthernetComboBox.clear()
|
||||
index = 0
|
||||
for interface in settings["interfaces"]:
|
||||
if not interface["name"].startswith(r"\Device\NPF_"):
|
||||
if not interface["name"].startswith(r"\Device\NPF_") and not interface["name"].startswith("tap"):
|
||||
self.uiLinuxEthernetComboBox.addItem(interface["name"])
|
||||
self.uiLinuxEthernetComboBox.setItemData(index, interface["id"], QtCore.Qt.ToolTipRole)
|
||||
index += 1
|
||||
|
||||
@@ -24,7 +24,6 @@ import glob
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.servers import Servers
|
||||
from gns3.node import Node
|
||||
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
@@ -41,7 +40,9 @@ from .nodes.ethernet_switch import EthernetSwitch
|
||||
from .nodes.ethernet_hub import EthernetHub
|
||||
from .nodes.frame_relay_switch import FrameRelaySwitch
|
||||
from .nodes.atm_switch import ATMSwitch
|
||||
from .settings import DYNAMIPS_SETTINGS, DYNAMIPS_SETTING_TYPES, PLATFORMS_DEFAULT_RAM
|
||||
from .settings import DYNAMIPS_SETTINGS, DYNAMIPS_SETTING_TYPES
|
||||
from .settings import IOS_ROUTER_SETTINGS, IOS_ROUTER_SETTING_TYPES
|
||||
from .settings import PLATFORMS_DEFAULT_RAM
|
||||
|
||||
PLATFORM_TO_CLASS = {
|
||||
"c1700": C1700,
|
||||
@@ -111,54 +112,18 @@ class Dynamips(Module):
|
||||
settings = QtCore.QSettings()
|
||||
settings.beginGroup("IOSRouters")
|
||||
|
||||
#FIXME: this is ugly
|
||||
# load the IOS images
|
||||
size = settings.beginReadArray("ios_router")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
name = settings.value("name", "")
|
||||
path = settings.value("path", "")
|
||||
image = settings.value("image", "")
|
||||
startup_config = settings.value("startup_config", "")
|
||||
private_config = settings.value("private_config", "")
|
||||
default_symbol = settings.value("default_symbol", ":/symbols/router.normal.svg")
|
||||
hover_symbol = settings.value("hover_symbol", ":/symbols/router.selected.svg")
|
||||
category = settings.value("category", Node.routers, type=int)
|
||||
platform = settings.value("platform", "")
|
||||
chassis = settings.value("chassis", "")
|
||||
idlepc = settings.value("idlepc", "")
|
||||
mac_addr = settings.value("mac_addr", "")
|
||||
ram = settings.value("ram", 128, type=int)
|
||||
nvram = settings.value("nvram", 256, type=int)
|
||||
disk0 = settings.value("disk0", 16, type=int)
|
||||
disk1 = settings.value("disk1", 0, type=int)
|
||||
confreg = settings.value("confreg", "0x2102")
|
||||
system_id = settings.value("system_id", "FTX0945W0MY")
|
||||
server = settings.value("server", "local")
|
||||
|
||||
name = settings.value("name")
|
||||
server = settings.value("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
if key in self._ios_routers:
|
||||
if key in self._ios_routers or not name or not server:
|
||||
continue
|
||||
|
||||
self._ios_routers[key] = {"name": name,
|
||||
"path": path,
|
||||
"image": image,
|
||||
"default_symbol": default_symbol,
|
||||
"hover_symbol": hover_symbol,
|
||||
"category": category,
|
||||
"startup_config": startup_config,
|
||||
"private_config": private_config,
|
||||
"platform": platform,
|
||||
"chassis": chassis,
|
||||
"idlepc": idlepc,
|
||||
"ram": ram,
|
||||
"nvram": nvram,
|
||||
"mac_addr": mac_addr,
|
||||
"disk0": disk0,
|
||||
"disk1": disk1,
|
||||
"confreg": confreg,
|
||||
"system_id": system_id,
|
||||
"server": server}
|
||||
self._ios_routers[key] = {}
|
||||
for setting_name, default_value in IOS_ROUTER_SETTINGS.items():
|
||||
self._ios_routers[key][setting_name] = settings.value(setting_name, default_value, IOS_ROUTER_SETTING_TYPES[setting_name])
|
||||
|
||||
for slot_id in range(0, 7):
|
||||
slot = "slot{}".format(slot_id)
|
||||
@@ -170,10 +135,13 @@ class Dynamips(Module):
|
||||
if settings.contains(wic):
|
||||
self._ios_routers[key][wic] = settings.value(wic, "")
|
||||
|
||||
platform = self._ios_routers[key]["platform"]
|
||||
chassis = self._ios_routers[key]["chassis"]
|
||||
|
||||
if platform == "c7200":
|
||||
self._ios_routers[key]["midplane"] = settings.value("midplane", "vxr")
|
||||
self._ios_routers[key]["npe"] = settings.value("npe", "npe-400")
|
||||
self._ios_routers[key]["slot0"] = settings.value("slot0", "C7200-IO-2FE")
|
||||
self._ios_routers[key]["slot0"] = settings.value("slot0", "C7200-IO-FE")
|
||||
else:
|
||||
self._ios_routers[key]["iomem"] = 5
|
||||
|
||||
@@ -364,6 +332,8 @@ class Dynamips(Module):
|
||||
if server.isLocal():
|
||||
params.update({"working_dir": self._working_dir})
|
||||
else:
|
||||
if "path" in params:
|
||||
del params["path"] # do not send Dynamips path to remote servers
|
||||
project_name = os.path.basename(self._working_dir)
|
||||
if project_name.endswith("-files"):
|
||||
project_name = project_name[:-6]
|
||||
@@ -386,12 +356,42 @@ class Dynamips(Module):
|
||||
if server.isLocal():
|
||||
params.update({"working_dir": self._working_dir})
|
||||
else:
|
||||
if "path" in params:
|
||||
del params["path"] # do not send Dynamips path to remote servers
|
||||
project_name = os.path.basename(self._working_dir)
|
||||
if project_name.endswith("-files"):
|
||||
project_name = project_name[:-6]
|
||||
params.update({"project_name": project_name})
|
||||
server.send_notification("dynamips.settings", params)
|
||||
|
||||
def allocateServer(self, node_class, use_cloud=False):
|
||||
"""
|
||||
Allocates a server.
|
||||
|
||||
:param node_class: Node object
|
||||
|
||||
:returns: allocated server (WebSocketClient instance)
|
||||
"""
|
||||
|
||||
# allocate a server for the node
|
||||
servers = Servers.instance()
|
||||
|
||||
if use_cloud:
|
||||
from ...topology import Topology
|
||||
topology = Topology.instance()
|
||||
top_instance = topology.anyInstance()
|
||||
server = servers.getCloudServer(top_instance.host, top_instance.port, top_instance.ssl_ca_file)
|
||||
else:
|
||||
if self._settings["use_local_server"]:
|
||||
# use the local server
|
||||
server = servers.localServer()
|
||||
else:
|
||||
# pick up a remote server (round-robin method)
|
||||
server = next(iter(servers))
|
||||
if not server:
|
||||
raise ModuleError("No remote server is configured")
|
||||
return server
|
||||
|
||||
def createNode(self, node_class, server):
|
||||
"""
|
||||
Creates a new node.
|
||||
@@ -435,19 +435,19 @@ class Dynamips(Module):
|
||||
ios_router = self._ios_routers[ios_key]
|
||||
break
|
||||
|
||||
# hack for EtherSwitch router
|
||||
if isinstance(node, EtherSwitchRouter) and node.server() == Servers.instance().localServer():
|
||||
for info in self._ios_routers.values():
|
||||
if info["platform"] == "c3725" and info["server"] == "local":
|
||||
ios_router = {
|
||||
"platform": "c3725",
|
||||
"path": info["path"],
|
||||
"ram": info["ram"],
|
||||
"startup_config": info["startup_config"],
|
||||
}
|
||||
break
|
||||
if not ios_router:
|
||||
raise ModuleError("Please create an c3725 IOS router in order to use an EtherSwitch router")
|
||||
# # hack for EtherSwitch router
|
||||
# if isinstance(node, EtherSwitchRouter) and node.server() == Servers.instance().localServer():
|
||||
# for info in self._ios_routers.values():
|
||||
# if info["platform"] == "c3725" and info["server"] == "local":
|
||||
# ios_router = {
|
||||
# "platform": "c3725",
|
||||
# "path": info["path"],
|
||||
# "ram": info["ram"],
|
||||
# "startup_config": info["startup_config"],
|
||||
# }
|
||||
# break
|
||||
# if not ios_router:
|
||||
# raise ModuleError("Please create an c3725 IOS router in order to use an EtherSwitch router")
|
||||
|
||||
if not ios_router:
|
||||
raise ModuleError("No IOS router for platform {}".format(node.settings()["platform"]))
|
||||
@@ -487,7 +487,11 @@ class Dynamips(Module):
|
||||
if wic in ios_router:
|
||||
settings[wic] = ios_router[wic]
|
||||
|
||||
node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings)
|
||||
base_name = "R"
|
||||
if "slot1" in settings and settings["slot1"] == "NM-16ESW":
|
||||
# must be an EtherSwitch router
|
||||
base_name = "ESW"
|
||||
node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings, base_name=base_name)
|
||||
else:
|
||||
node.setup()
|
||||
|
||||
@@ -546,8 +550,8 @@ class Dynamips(Module):
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "exportConfigs") and node.initialized():
|
||||
node.exportConfigs(directory)
|
||||
if isinstance(node, Router) and node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
@@ -557,8 +561,8 @@ class Dynamips(Module):
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "importConfigs") and node.initialized():
|
||||
node.importConfigs(directory)
|
||||
if isinstance(node, Router) and node.initialized():
|
||||
node.importConfigFromDirectory(directory)
|
||||
|
||||
def findAlternativeIOSImage(self, image, node):
|
||||
"""
|
||||
@@ -643,7 +647,7 @@ class Dynamips(Module):
|
||||
server = "{}:{}".format(remote_server.host, remote_server.port)
|
||||
|
||||
nodes = []
|
||||
for node_class in [EtherSwitchRouter, EthernetSwitch, EthernetHub, FrameRelaySwitch, ATMSwitch]:
|
||||
for node_class in [EthernetSwitch, EthernetHub, FrameRelaySwitch, ATMSwitch]:
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName(),
|
||||
|
||||
@@ -19,14 +19,19 @@
|
||||
Wizard for IOS routers.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.servers import Servers
|
||||
from gns3.node import Node
|
||||
from gns3.utils.message_box import MessageBox
|
||||
from gns3.dialogs.exec_command_dialog import ExecCommandDialog
|
||||
from gns3.utils.run_in_terminal import RunInTerminal
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
|
||||
from ....settings import ENABLE_CLOUD
|
||||
from ..ui.ios_router_wizard_ui import Ui_IOSRouterWizard
|
||||
from ..settings import PLATFORMS_DEFAULT_RAM, CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
|
||||
from .. import Dynamips
|
||||
@@ -54,23 +59,44 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
Wizard to create an IOS router.
|
||||
|
||||
:param parent: parent widget
|
||||
:param ios_routers: existing IOS routers
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, ios_routers, parent):
|
||||
|
||||
QtGui.QWizard.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/router.normal.svg"))
|
||||
self.setWizardStyle(QtGui.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtGui.QWizard.NoDefaultButton)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
self.uiIOSImageToolButton.clicked.connect(self._iosImageBrowserSlot)
|
||||
self.uiTestIOSImagePushButton.clicked.connect(self._testIOSImageSlot)
|
||||
self.uiIdlePCFinderPushButton.clicked.connect(self._idlePCFinderSlot)
|
||||
self.uiEtherSwitchCheckBox.stateChanged.connect(self._etherSwitchSlot)
|
||||
self.uiPlatformComboBox.currentIndexChanged[str].connect(self._platformChangedSlot)
|
||||
self.uiPlatformComboBox.addItems(list(PLATFORMS_DEFAULT_RAM.keys()))
|
||||
|
||||
# Validate the Idle PC value
|
||||
self._idle_valid = False
|
||||
idle_pc_rgx = QtCore.QRegExp("^(0x[0-9a-fA-F]+)?$")
|
||||
validator = QtGui.QRegExpValidator(idle_pc_rgx)
|
||||
self.uiIdlepcLineEdit.setValidator(validator)
|
||||
self.uiIdlepcLineEdit.textChanged.connect(self._idlePCValidateSlot)
|
||||
self.uiIdlepcLineEdit.textChanged.emit(self.uiIdlepcLineEdit.text())
|
||||
|
||||
# location of the base config templates
|
||||
self._base_startup_config_template = get_resource(os.path.join("configs", "ios_base_startup-config.txt"))
|
||||
self._base_private_config_template = get_resource(os.path.join("configs", "ios_base_private-config.txt"))
|
||||
self._base_etherswitch_startup_config_template = get_resource(os.path.join("configs", "ios_etherswitch_startup-config.txt"))
|
||||
|
||||
#FIXME: hide because of issue on Windows.
|
||||
self.uiTestIOSImagePushButton.hide()
|
||||
|
||||
# Mandatory fields
|
||||
self.uiNamePlatformWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
self.uiIOSImageWizardPage.registerField("image*", self.uiIOSImageLineEdit)
|
||||
@@ -87,12 +113,15 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
1: self.uiWic1comboBox,
|
||||
2: self.uiWic2comboBox}
|
||||
|
||||
self.uiTestIOSImagePushButton.hide() # hide it because it doesn't work
|
||||
self._ios_routers = ios_routers
|
||||
|
||||
if Dynamips.instance().settings()["use_local_server"]:
|
||||
# skip the server page if we use the local server
|
||||
self.setStartId(1)
|
||||
|
||||
if not ENABLE_CLOUD:
|
||||
self.uiCloudRadioButton.hide()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
@@ -127,16 +156,50 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
self.uiChassisComboBox.clear()
|
||||
if platform in CHASSIS:
|
||||
self.uiChassisComboBox.addItems(CHASSIS[platform])
|
||||
if platform not in ("c2600", "c3600", "c2691", "c3725", "c3745"):
|
||||
self.uiEtherSwitchCheckBox.setChecked(False)
|
||||
self.uiEtherSwitchCheckBox.hide()
|
||||
else:
|
||||
self.uiEtherSwitchCheckBox.show()
|
||||
|
||||
def _testIOSImageSlot(self):
|
||||
"""
|
||||
Slot to locally test the IOS image.
|
||||
"""
|
||||
|
||||
platform = self.uiPlatformComboBox.currentText()
|
||||
ram = self.uiRamSpinBox.value()
|
||||
ios_image = self.uiIOSImageLineEdit.text()
|
||||
params = ["-P", platform[1:], "-r", str(ram), ios_image]
|
||||
dialog = ExecCommandDialog(self, "/usr/bin/dynamips", params)
|
||||
dialog.show()
|
||||
dialog.exec_()
|
||||
dynamips = os.path.realpath(Dynamips.instance().settings()["path"])
|
||||
if not os.path.exists(dynamips):
|
||||
QtGui.QMessageBox.critical(self, "IOS image", "Could not find Dynamips executable: {}".format(dynamips))
|
||||
return
|
||||
command = '"{path}" -P {platform} -r {ram} "{ios_image}"'.format(path=dynamips,
|
||||
platform=platform[1:],
|
||||
ram=ram,
|
||||
ios_image=ios_image)
|
||||
try:
|
||||
RunInTerminal(command)
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self, "IOS image", "Could not test the IOS image: {}".format(e))
|
||||
|
||||
def _idlePCValidateSlot(self):
|
||||
"""
|
||||
Slot to validate the entered Idle-PC Value
|
||||
"""
|
||||
sender = self.sender()
|
||||
validator = sender.validator()
|
||||
state = validator.validate(sender.text(), 0)[0]
|
||||
if state == QtGui.QValidator.Acceptable:
|
||||
color = '#A2C964' # green
|
||||
self._idle_valid = True
|
||||
elif state == QtGui.QValidator.Intermediate:
|
||||
color = '#fff79a' # yellow
|
||||
self._idle_valid = False
|
||||
else:
|
||||
color = '#f6989d' # red
|
||||
self._idle_valid = False
|
||||
sender.setStyleSheet('QLineEdit { background-color: %s }' % color)
|
||||
|
||||
def _idlePCFinderSlot(self):
|
||||
"""
|
||||
@@ -154,6 +217,20 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
self._router.created_signal.connect(self.createdSlot)
|
||||
self.uiIdlePCFinderPushButton.setEnabled(False)
|
||||
|
||||
def _etherSwitchSlot(self, state):
|
||||
"""
|
||||
Slot if the EtherSwitch option is chosen or not.
|
||||
:param state: boolean
|
||||
"""
|
||||
|
||||
if state:
|
||||
# forces the name to EtherSwitch
|
||||
self.uiNameLineEdit.setText("EtherSwitch router")
|
||||
#self.uiNameLineEdit.setEnabled(False)
|
||||
else:
|
||||
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())
|
||||
#self.uiNameLineEdit.setEnabled(True)
|
||||
|
||||
def createdSlot(self, node_id):
|
||||
"""
|
||||
The node for the auto Idle-PC has been created.
|
||||
@@ -203,7 +280,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
|
||||
# try to guess the platform
|
||||
image = os.path.basename(path)
|
||||
match = re.match("^(c[0-9]+)\\-\w+", image)
|
||||
match = re.match("^(c[0-9]+)p?\\-\w+", image.lower())
|
||||
if not match:
|
||||
QtGui.QMessageBox.warning(self, "IOS image", "Could not detect the platform, make sure this is a valid IOS image!")
|
||||
return
|
||||
@@ -221,6 +298,9 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for the {} platform/chassis and is not supported by this application!".format(detected_platform))
|
||||
return
|
||||
|
||||
if image.lower().startswith("c7200p"):
|
||||
QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for c7200 PowerPC routers and is not recommended. Please use an IOS image that do not start with c7200p.")
|
||||
|
||||
index = self.uiPlatformComboBox.findText(detected_platform)
|
||||
if index != -1:
|
||||
self.uiPlatformComboBox.setCurrentIndex(index)
|
||||
@@ -277,6 +357,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
self.uiNameLineEdit.setText(self.uiPlatformComboBox.currentText())
|
||||
ios_image = self.uiIOSImageLineEdit.text()
|
||||
self.setWindowTitle("New IOS router - {}".format(os.path.basename(ios_image)))
|
||||
|
||||
elif self.page(page_id) == self.uiMemoryWizardPage:
|
||||
# set the correct amount of RAM based on the platform
|
||||
from ..pages.ios_router_preferences_page import IOSRouterPreferencesPage
|
||||
@@ -284,7 +365,7 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
path = self.uiIOSImageLineEdit.text()
|
||||
if os.path.isfile(path):
|
||||
minimum_required_ram = IOSRouterPreferencesPage.getMinimumRequiredRAM(path)
|
||||
if minimum_required_ram > PLATFORMS_DEFAULT_RAM[platform]:
|
||||
if minimum_required_ram >= PLATFORMS_DEFAULT_RAM[platform]:
|
||||
self.uiRamSpinBox.setValue(minimum_required_ram)
|
||||
else:
|
||||
self.uiRamSpinBox.setValue(PLATFORMS_DEFAULT_RAM[platform])
|
||||
@@ -298,45 +379,35 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
chassis = ""
|
||||
self._populateAdapters(platform, chassis)
|
||||
if platform == "c7200":
|
||||
self.uiSlot0comboBox.setCurrentIndex(self.uiSlot0comboBox.findText("C7200-IO-2FE"))
|
||||
self.uiSlot0comboBox.setCurrentIndex(self.uiSlot0comboBox.findText("C7200-IO-FE"))
|
||||
if self.uiEtherSwitchCheckBox.isChecked():
|
||||
self.uiSlot1comboBox.setCurrentIndex(self.uiSlot1comboBox.findText("NM-16ESW"))
|
||||
|
||||
def validateCurrentPage(self):
|
||||
"""
|
||||
Validates the IOS name.
|
||||
Validates the IOS name and checks validation state for Idle-PC value
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
|
||||
#FIXME: prevent users to use "cloud"
|
||||
if self.uiCloudRadioButton.isChecked():
|
||||
QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!")
|
||||
if self.currentPage() == self.uiNamePlatformWizardPage:
|
||||
name = self.uiNameLineEdit.text()
|
||||
for ios_router in self._ios_routers.values():
|
||||
if ios_router["name"] == name:
|
||||
QtGui.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
|
||||
return False
|
||||
#if self.uiEtherSwitchCheckBox.isChecked() and ios_router["etherswitch"]:
|
||||
# QtGui.QMessageBox.critical(self, "EtherSwitch router", "A router has already been configured to be used as the EtherSwitch router".format(name))
|
||||
# return False
|
||||
if self.currentPage() == self.uiIdlePCWizardPage:
|
||||
if not self._idle_valid:
|
||||
idle_pc = self.uiIdlepcLineEdit.text()
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "{} is not a valid Idle-PC value ".format(idle_pc))
|
||||
return False
|
||||
if self.currentPage() == self.uiServerWizardPage and self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in Dynamips preferences")
|
||||
return False
|
||||
|
||||
# if self.currentPage() == self.uiNameImageWizardPage:
|
||||
# name = self.uiNameLineEdit.text()
|
||||
# if not re.search(r"""^[\-\w]+$""", name):
|
||||
# # IOS names must start with a letter, end with a letter or digit, and
|
||||
# # have as interior characters only letters, digits, and hyphens.
|
||||
# # They must be 63 characters or fewer.
|
||||
# QtGui.QMessageBox.critical(self, "Name", "Invalid name detected: {}".format(name))
|
||||
# return False
|
||||
return True
|
||||
|
||||
|
||||
# minimum_required_ram = self._getMinimumRequiredRAM(path)
|
||||
# if minimum_required_ram > ram:
|
||||
# QtGui.QMessageBox.warning(self, "IOS image", "There is not sufficient RAM allocated to this IOS image, recommended RAM is {} MB".format(minimum_required_ram))
|
||||
#
|
||||
# # basename doesn't work on Unix with Windows paths
|
||||
# if not sys.platform.startswith('win') and len(path) > 2 and path[1] == ":":
|
||||
# import ntpath
|
||||
# image = ntpath.basename(path)
|
||||
# else:
|
||||
# image = os.path.basename(path)
|
||||
#
|
||||
# if image.startswith("c7200p"):
|
||||
# QtGui.QMessageBox.warning(self, "IOS image", "This IOS image is for the c7200 platform with NPE-G2 and using it is not recommended.\nPlease use an IOS image that do not start with c7200p.")
|
||||
|
||||
def getSettings(self):
|
||||
"""
|
||||
Returns the settings set in this Wizard.
|
||||
@@ -345,28 +416,40 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
"""
|
||||
|
||||
path = self.uiIOSImageLineEdit.text()
|
||||
image = os.path.basename(path)
|
||||
if Dynamips.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
server = "local"
|
||||
elif self.uiLoadBalanceCheckBox.isChecked():
|
||||
server = next(iter(Servers.instance()))
|
||||
if not server:
|
||||
QtGui.QMessageBox.critical(self, "IOS router", "No remote server available!")
|
||||
return
|
||||
server = "{}:{}".format(server.host, server.port)
|
||||
else:
|
||||
server = self.uiRemoteServersComboBox.currentText()
|
||||
elif self.uiRemoteRadioButton.isChecked():
|
||||
if self.uiLoadBalanceCheckBox.isChecked():
|
||||
server = next(iter(Servers.instance()))
|
||||
server = "{}:{}".format(server.host, server.port)
|
||||
else:
|
||||
server = self.uiRemoteServersComboBox.currentText()
|
||||
else: # Cloud is selected
|
||||
server = "cloud"
|
||||
|
||||
settings = {
|
||||
"name": self.uiNameLineEdit.text(),
|
||||
"path": path,
|
||||
"startup_config": get_default_base_config(self._base_startup_config_template),
|
||||
"private_config": get_default_base_config(self._base_private_config_template),
|
||||
"ram": self.uiRamSpinBox.value(),
|
||||
"idlepc": self.uiIdlepcLineEdit.text(),
|
||||
"image": os.path.basename(path),
|
||||
"image": image,
|
||||
"platform": self.uiPlatformComboBox.currentText(),
|
||||
"chassis": self.uiChassisComboBox.currentText(),
|
||||
"server": server,
|
||||
}
|
||||
|
||||
if self.uiEtherSwitchCheckBox.isChecked():
|
||||
settings["startup_config"] = get_default_base_config(self._base_etherswitch_startup_config_template)
|
||||
settings["default_symbol"] = ":/symbols/multilayer_switch.normal.svg"
|
||||
settings["hover_symbol"] = ":/symbols/multilayer_switch.selected.svg"
|
||||
settings["category"] = Node.switches
|
||||
|
||||
if image.lower().startswith("c7200p"):
|
||||
settings["npe"] = "npe-g2"
|
||||
|
||||
for slot_id, widget in self._widget_slots.items():
|
||||
if widget.isEnabled():
|
||||
settings["slot{}".format(slot_id)] = widget.currentText()
|
||||
@@ -388,4 +471,4 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard):
|
||||
if platform not in WIC_MATRIX:
|
||||
# skip the WIC modules page if the platform doesn't support any.
|
||||
return self.uiNetworkAdaptersWizardPage.nextId() + 1
|
||||
return QtGui.QWizard.nextId(self)
|
||||
return QtGui.QWizard.nextId(self)
|
||||
|
||||
@@ -141,33 +141,36 @@ class ATMSwitch(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
ports_to_create = []
|
||||
mapping = new_settings["mappings"]
|
||||
|
||||
updated = False
|
||||
for source, destination in mapping.items():
|
||||
source_port = source.split(":")[0]
|
||||
destination_port = destination.split(":")[0]
|
||||
if source_port not in ports_to_create:
|
||||
ports_to_create.append(source_port)
|
||||
if destination_port not in ports_to_create:
|
||||
ports_to_create.append(destination_port)
|
||||
if "mappings" in new_settings:
|
||||
ports_to_create = []
|
||||
mapping = new_settings["mappings"]
|
||||
|
||||
for port in self._ports.copy():
|
||||
if port.isFree():
|
||||
for source, destination in mapping.items():
|
||||
source_port = source.split(":")[0]
|
||||
destination_port = destination.split(":")[0]
|
||||
if source_port not in ports_to_create:
|
||||
ports_to_create.append(source_port)
|
||||
if destination_port not in ports_to_create:
|
||||
ports_to_create.append(destination_port)
|
||||
|
||||
for port in self._ports.copy():
|
||||
if port.isFree():
|
||||
updated = True
|
||||
self._ports.remove(port)
|
||||
else:
|
||||
ports_to_create.remove(port.name())
|
||||
|
||||
for port_name in ports_to_create:
|
||||
port = ATMPort(port_name)
|
||||
port.setPortNumber(int(port_name))
|
||||
port.setStatus(ATMPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
self._ports.remove(port)
|
||||
else:
|
||||
ports_to_create.remove(port.name())
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
|
||||
for port_name in ports_to_create:
|
||||
port = ATMPort(port_name)
|
||||
port.setPortNumber(int(port_name))
|
||||
port.setStatus(ATMPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
self._settings["mappings"] = new_settings["mappings"].copy()
|
||||
|
||||
params = {}
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
@@ -178,7 +181,6 @@ class ATMSwitch(Node):
|
||||
"name": new_settings["name"]}
|
||||
updated = True
|
||||
|
||||
self._settings["mappings"] = new_settings["mappings"].copy()
|
||||
if updated:
|
||||
if params:
|
||||
log.debug("{} is being updated: {}".format(self.name(), params))
|
||||
@@ -337,7 +339,7 @@ class ATMSwitch(Node):
|
||||
try:
|
||||
port.startPacketCapture(result["capture_file_path"])
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
|
||||
self.updated_signal.emit()
|
||||
break
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class C1700(Router):
|
||||
def __init__(self, module, server, chassis="1720"):
|
||||
Router.__init__(self, module, server, platform="c1700")
|
||||
|
||||
self._platform_settings = {"ram": 64,
|
||||
self._platform_settings = {"ram": 128,
|
||||
"nvram": 32,
|
||||
"disk0": 0,
|
||||
"disk1": 0,
|
||||
|
||||
@@ -45,7 +45,7 @@ class C2600(Router):
|
||||
def __init__(self, module, server, chassis="2610"):
|
||||
Router.__init__(self, module, server, platform="c2600")
|
||||
|
||||
self._platform_settings = {"ram": 64,
|
||||
self._platform_settings = {"ram": 128,
|
||||
"nvram": 128,
|
||||
"disk0": 0,
|
||||
"disk1": 0,
|
||||
|
||||
@@ -33,7 +33,7 @@ class C2691(Router):
|
||||
def __init__(self, module, server):
|
||||
Router.__init__(self, module, server, platform="c2691")
|
||||
|
||||
self._platform_settings = {"ram": 128,
|
||||
self._platform_settings = {"ram": 192,
|
||||
"nvram": 112,
|
||||
"disk0": 16,
|
||||
"disk1": 0,
|
||||
|
||||
@@ -33,7 +33,7 @@ class C3600(Router):
|
||||
def __init__(self, module, server, chassis="3640"):
|
||||
Router.__init__(self, module, server, platform="c3600")
|
||||
|
||||
self._platform_settings = {"ram": 128,
|
||||
self._platform_settings = {"ram": 192,
|
||||
"nvram": 128,
|
||||
"disk0": 0,
|
||||
"disk1": 0,
|
||||
|
||||
@@ -33,7 +33,7 @@ class C3745(Router):
|
||||
def __init__(self, module, server):
|
||||
Router.__init__(self, module, server, platform="c3745")
|
||||
|
||||
self._platform_settings = {"ram": 128,
|
||||
self._platform_settings = {"ram": 256,
|
||||
"nvram": 304,
|
||||
"disk0": 16,
|
||||
"disk1": 0,
|
||||
|
||||
@@ -33,7 +33,7 @@ class C7200(Router):
|
||||
def __init__(self, module, server, npe="npe-400"):
|
||||
Router.__init__(self, module, server, platform="c7200")
|
||||
|
||||
self._platform_settings = {"ram": 256,
|
||||
self._platform_settings = {"ram": 512,
|
||||
"nvram": 128,
|
||||
"disk0": 64,
|
||||
"disk1": 0,
|
||||
@@ -47,7 +47,7 @@ class C7200(Router):
|
||||
if npe == "npe-g2":
|
||||
self._platform_settings["slot0"] = "C7200-IO-GE-E"
|
||||
else:
|
||||
self._platform_settings["slot0"] = "C7200-IO-2FE"
|
||||
self._platform_settings["slot0"] = "C7200-IO-FE"
|
||||
|
||||
# merge platform settings with the generic ones
|
||||
self._settings.update(self._platform_settings)
|
||||
|
||||
@@ -143,30 +143,33 @@ class EthernetHub(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
ports_to_create = []
|
||||
ports = new_settings["ports"]
|
||||
|
||||
updated = False
|
||||
for port_number in ports:
|
||||
if port_number not in ports_to_create:
|
||||
ports_to_create.append(port_number)
|
||||
if "ports" in new_settings:
|
||||
ports_to_create = []
|
||||
ports = new_settings["ports"]
|
||||
for port_number in ports:
|
||||
if port_number not in ports_to_create:
|
||||
ports_to_create.append(port_number)
|
||||
|
||||
for port in self._ports.copy():
|
||||
if port.isFree():
|
||||
self._ports.remove(port)
|
||||
for port in self._ports.copy():
|
||||
if port.isFree():
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been removed".format(port.name()))
|
||||
else:
|
||||
ports_to_create.remove(port.name())
|
||||
|
||||
for port_name in ports_to_create:
|
||||
port = EthernetPort(port_name)
|
||||
port.setPortNumber(int(port_name))
|
||||
port.setStatus(EthernetPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been removed".format(port.name()))
|
||||
else:
|
||||
ports_to_create.remove(port.name())
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
|
||||
self._settings["ports"] = new_settings["ports"].copy()
|
||||
|
||||
for port_name in ports_to_create:
|
||||
port = EthernetPort(port_name)
|
||||
port.setPortNumber(int(port_name))
|
||||
port.setStatus(EthernetPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
|
||||
params = {}
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
@@ -177,7 +180,6 @@ class EthernetHub(Node):
|
||||
"name": new_settings["name"]}
|
||||
updated = True
|
||||
|
||||
self._settings["ports"] = new_settings["ports"].copy()
|
||||
if updated:
|
||||
if params:
|
||||
log.debug("{} is being updated: {}".format(self.name(), params))
|
||||
@@ -326,7 +328,7 @@ class EthernetHub(Node):
|
||||
try:
|
||||
port.startPacketCapture(result["capture_file_path"])
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
|
||||
self.updated_signal.emit()
|
||||
break
|
||||
|
||||
|
||||
@@ -146,31 +146,44 @@ class EthernetSwitch(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
ports_to_update = {}
|
||||
ports = new_settings["ports"]
|
||||
updated = False
|
||||
for port_number in ports.keys():
|
||||
if port_number in self._settings["ports"]:
|
||||
if self._settings["ports"][port_number] != ports[port_number]:
|
||||
for port in self._ports:
|
||||
if port.portNumber() == port_number and not port.isFree():
|
||||
ports_to_update[port_number] = ports[port_number]
|
||||
break
|
||||
continue
|
||||
port = EthernetPort(str(port_number))
|
||||
port.setPortNumber(port_number)
|
||||
port.setStatus(EthernetPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(port_number))
|
||||
|
||||
params = {"id": self._ethsw_id}
|
||||
if ports_to_update:
|
||||
params["ports"] = {}
|
||||
for port_number, info in ports_to_update.items():
|
||||
params["ports"][port_number] = info
|
||||
updated = True
|
||||
if "ports" in new_settings:
|
||||
ports_to_update = {}
|
||||
ports = new_settings["ports"]
|
||||
|
||||
for port_number in ports.keys():
|
||||
if port_number in self._settings["ports"]:
|
||||
if self._settings["ports"][port_number] != ports[port_number]:
|
||||
for port in self._ports:
|
||||
if port.portNumber() == port_number and not port.isFree():
|
||||
ports_to_update[port_number] = ports[port_number]
|
||||
break
|
||||
continue
|
||||
port = EthernetPort(str(port_number))
|
||||
port.setPortNumber(port_number)
|
||||
port.setStatus(EthernetPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(port_number))
|
||||
|
||||
if ports_to_update:
|
||||
params["ports"] = {}
|
||||
for port_number, info in ports_to_update.items():
|
||||
params["ports"][port_number] = info
|
||||
updated = True
|
||||
|
||||
# delete ports that are not configured
|
||||
for port_number in self._settings["ports"].keys():
|
||||
if port_number not in new_settings["ports"]:
|
||||
for port in self._ports.copy():
|
||||
if port.portNumber() == port_number:
|
||||
self._ports.remove(port)
|
||||
log.debug("port {} has been removed".format(port.name()))
|
||||
break
|
||||
|
||||
self._settings["ports"] = new_settings["ports"].copy()
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
if self.hasAllocatedName(new_settings["name"]):
|
||||
@@ -179,16 +192,6 @@ class EthernetSwitch(Node):
|
||||
params["name"] = new_settings["name"]
|
||||
updated = True
|
||||
|
||||
# delete ports that are not configured
|
||||
for port_number in self._settings["ports"].keys():
|
||||
if port_number not in new_settings["ports"]:
|
||||
for port in self._ports.copy():
|
||||
if port.portNumber() == port_number:
|
||||
self._ports.remove(port)
|
||||
log.debug("port {} has been removed".format(port.name()))
|
||||
break
|
||||
|
||||
self._settings["ports"] = new_settings["ports"].copy()
|
||||
if updated:
|
||||
log.debug("{} is being updated: {}".format(self.name(), params))
|
||||
self._server.send_message("dynamips.ethsw.update", params, self._updateCallback)
|
||||
@@ -337,7 +340,7 @@ class EthernetSwitch(Node):
|
||||
try:
|
||||
port.startPacketCapture(result["capture_file_path"])
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
|
||||
self.updated_signal.emit()
|
||||
break
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
"""
|
||||
EtherSwitch router implementation (based on Dynamips c3745).
|
||||
This is legacy code, kept only to support topologies made with GNS3 < 1.2.2
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
@@ -140,34 +140,37 @@ class FrameRelaySwitch(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
ports_to_create = []
|
||||
mapping = new_settings["mappings"]
|
||||
|
||||
updated = False
|
||||
for source, destination in mapping.items():
|
||||
source_port = source.split(":")[0]
|
||||
destination_port = destination.split(":")[0]
|
||||
if source_port not in ports_to_create:
|
||||
ports_to_create.append(source_port)
|
||||
if destination_port not in ports_to_create:
|
||||
ports_to_create.append(destination_port)
|
||||
if "mappings" in new_settings:
|
||||
ports_to_create = []
|
||||
mapping = new_settings["mappings"]
|
||||
|
||||
for port in self._ports.copy():
|
||||
if port.isFree():
|
||||
self._ports.remove(port)
|
||||
for source, destination in mapping.items():
|
||||
source_port = source.split(":")[0]
|
||||
destination_port = destination.split(":")[0]
|
||||
if source_port not in ports_to_create:
|
||||
ports_to_create.append(source_port)
|
||||
if destination_port not in ports_to_create:
|
||||
ports_to_create.append(destination_port)
|
||||
|
||||
for port in self._ports.copy():
|
||||
if port.isFree():
|
||||
self._ports.remove(port)
|
||||
updated = True
|
||||
log.debug("port {} has been removed".format(port.name()))
|
||||
else:
|
||||
ports_to_create.remove(port.name())
|
||||
|
||||
for port_name in ports_to_create:
|
||||
port = FrameRelayPort(port_name)
|
||||
port.setPortNumber(int(port_name))
|
||||
port.setStatus(FrameRelayPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been removed".format(port.name()))
|
||||
else:
|
||||
ports_to_create.remove(port.name())
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
|
||||
for port_name in ports_to_create:
|
||||
port = FrameRelayPort(port_name)
|
||||
port.setPortNumber(int(port_name))
|
||||
port.setStatus(FrameRelayPort.started)
|
||||
port.setPacketCaptureSupported(True)
|
||||
self._ports.append(port)
|
||||
updated = True
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
self._settings["mappings"] = new_settings["mappings"].copy()
|
||||
|
||||
params = {}
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
@@ -178,7 +181,6 @@ class FrameRelaySwitch(Node):
|
||||
"name": new_settings["name"]}
|
||||
updated = True
|
||||
|
||||
self._settings["mappings"] = new_settings["mappings"].copy()
|
||||
if updated:
|
||||
if params:
|
||||
log.debug("{} is being updated: {}".format(self.name(), params))
|
||||
@@ -336,7 +338,7 @@ class FrameRelaySwitch(Node):
|
||||
try:
|
||||
port.startPacketCapture(result["capture_file_path"])
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
|
||||
self.updated_signal.emit()
|
||||
break
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ Asynchronously sends JSON messages to the GNS3 server and receives responses wit
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import base64
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
@@ -103,9 +104,12 @@ class Router(Node):
|
||||
if "chassis" in self._settings and self._settings["chassis"] in ("1720", "1721", "1750"):
|
||||
# these chassis show their interface without a slot number
|
||||
port_name = port.longNameType() + str(port_number)
|
||||
short_name = port.shortNameType() + str(port_number)
|
||||
else:
|
||||
port_name = port.longNameType() + str(slot_number) + "/" + str(port_number)
|
||||
short_name = port.shortNameType() + str(slot_number) + "/" + str(port_number)
|
||||
new_port = port(port_name)
|
||||
new_port.setShortName(short_name)
|
||||
new_port.setPortNumber(port_number)
|
||||
new_port.setSlotNumber(slot_number)
|
||||
new_port.setPacketCaptureSupported(True)
|
||||
@@ -138,7 +142,9 @@ class Router(Node):
|
||||
port = WIC_MATRIX[wic]["port"]
|
||||
# Dynamips WICs port number start on a multiple of 16.
|
||||
port_name = port.longNameType() + str(base + port_number)
|
||||
short_name = port.shortNameType() + str(base + port_number)
|
||||
new_port = port(port_name)
|
||||
new_port.setShortName(short_name)
|
||||
new_port.setPortNumber(base + port_number)
|
||||
# WICs are always in adapter slot 0.
|
||||
new_port.setSlotNumber(0)
|
||||
@@ -190,8 +196,10 @@ class Router(Node):
|
||||
if "chassis" in self._settings and self._settings["chassis"] in ("1720", "1721", "1750"):
|
||||
# these chassis show their interface without a slot number
|
||||
port.setName(port.longNameType() + str(wic_port_number))
|
||||
port.setShortName(port.shortNameType() + str(wic_port_number))
|
||||
else:
|
||||
port.setName(port.longNameType() + "0/" + str(wic_port_number))
|
||||
port.setShortName(port.shortNameType() + "0/" + str(wic_port_number))
|
||||
log.debug("port {} renamed to {}".format(old_name, port.name()))
|
||||
|
||||
def delete(self):
|
||||
@@ -250,6 +258,10 @@ class Router(Node):
|
||||
"ram": ram,
|
||||
"image": image}
|
||||
|
||||
if self.server().isCloud():
|
||||
initial_settings["cloud_path"] = "images/IOS"
|
||||
params["image"] = os.path.basename(params["image"])
|
||||
|
||||
if router_id:
|
||||
params["router_id"] = router_id
|
||||
|
||||
@@ -262,6 +274,8 @@ class Router(Node):
|
||||
params["mac_addr"] = self._settings["mac_addr"] = initial_settings.pop("mac_addr")
|
||||
if "chassis" in initial_settings:
|
||||
params["chassis"] = self._settings["chassis"] = initial_settings.pop("chassis")
|
||||
if "cloud_path" in initial_settings:
|
||||
params["cloud_path"] = self._settings["cloud_path"] = initial_settings.pop("cloud_path")
|
||||
|
||||
# other initial settings will be applied when the router has been created
|
||||
if initial_settings:
|
||||
@@ -566,7 +580,7 @@ class Router(Node):
|
||||
try:
|
||||
port.startPacketCapture(result["capture_file_path"])
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
|
||||
self.updated_signal.emit()
|
||||
break
|
||||
|
||||
@@ -931,14 +945,14 @@ class Router(Node):
|
||||
for port in self._ports:
|
||||
ports.append(port.dump())
|
||||
|
||||
# make the IOU path relative
|
||||
# make the IOS path relative
|
||||
image_path = router["properties"]["image"]
|
||||
if self.server().isLocal():
|
||||
if os.path.commonprefix([image_path, self._module.imageFilesDir()]) == self._module.imageFilesDir():
|
||||
# save only the image name if it is stored the images directory
|
||||
router["properties"]["image"] = os.path.basename(image_path)
|
||||
else:
|
||||
router["properties"]["image"] = os.path.basename(image_path)
|
||||
router["properties"]["image"] = image_path
|
||||
|
||||
return router
|
||||
|
||||
@@ -1000,7 +1014,50 @@ class Router(Node):
|
||||
self._inital_settings = None
|
||||
self._loading = False
|
||||
|
||||
def exportConfigs(self, directory):
|
||||
def exportConfig(self, startup_config_export_path, private_config_export_path):
|
||||
"""
|
||||
Exports the startup-config.
|
||||
|
||||
:param startup_config_export_path: export path for the startup-config
|
||||
:param private_config_export_path: export path for the private-config
|
||||
"""
|
||||
|
||||
self._startup_config_export_path = startup_config_export_path
|
||||
self._private_config_export_path = private_config_export_path
|
||||
self._server.send_message("dynamips.vm.export_config", {"id": self._router_id}, self._exportConfigCallback)
|
||||
|
||||
def _exportConfigCallback(self, result, error=False):
|
||||
"""
|
||||
Callback for exportConfig.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while exporting {} configs: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["code"], result["message"])
|
||||
else:
|
||||
|
||||
if "startup_config_base64" in result and self._startup_config_export_path:
|
||||
config = base64.decodebytes(result["startup_config_base64"].encode("utf-8"))
|
||||
try:
|
||||
with open(self._startup_config_export_path, "wb") as f:
|
||||
log.info("saving {} startup-config to {}".format(self.name(), self._startup_config_export_path))
|
||||
f.write(config)
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export startup-config to {}: {}".format(self._startup_config_export_path, e))
|
||||
|
||||
if "private_config_base64" in result and self._private_config_export_path:
|
||||
config = base64.decodebytes(result["private_config_base64"].encode("utf-8"))
|
||||
try:
|
||||
with open(self._private_config_export_path, "wb") as f:
|
||||
log.info("saving {} private-config to {}".format(self.name(), self._private_config_export_path))
|
||||
f.write(config)
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export private-config to {}: {}".format(self._private_config_export_path, e))
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the startup-config and private-config to a directory.
|
||||
|
||||
@@ -1008,11 +1065,11 @@ class Router(Node):
|
||||
"""
|
||||
|
||||
self._export_directory = directory
|
||||
self._server.send_message("dynamips.vm.export_config", {"id": self._router_id}, self._exportConfigsCallback)
|
||||
self._server.send_message("dynamips.vm.export_config", {"id": self._router_id}, self._exportConfigToDirectoryCallback)
|
||||
|
||||
def _exportConfigsCallback(self, result, error=False):
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False):
|
||||
"""
|
||||
Callback for exportConfigs.
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
@@ -1045,7 +1102,27 @@ class Router(Node):
|
||||
|
||||
self._export_directory = None
|
||||
|
||||
def importConfigs(self, directory):
|
||||
def importConfig(self, path):
|
||||
"""
|
||||
Imports a startup-config.
|
||||
|
||||
:param path: path to the startup-config
|
||||
"""
|
||||
|
||||
new_settings = {"startup_config": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importPrivateConfig(self, path):
|
||||
"""
|
||||
Imports a private-config.
|
||||
|
||||
:param path: path to the private-config
|
||||
"""
|
||||
|
||||
new_settings = {"private_config": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports a startup-config and a private-config from a directory.
|
||||
|
||||
@@ -1122,6 +1199,23 @@ class Router(Node):
|
||||
from ..pages.ios_router_configuration_page import IOSRouterConfigurationPage
|
||||
return IOSRouterConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def validateHostname(hostname):
|
||||
"""
|
||||
Checks if the hostname is valid.
|
||||
|
||||
:param hostname: hostname to check
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
# IOS names must start with a letter, end with a letter or digit, and
|
||||
# have as interior characters only letters, digits, and hyphens.
|
||||
# They must be 63 characters or fewer.
|
||||
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
|
||||
@@ -21,10 +21,8 @@ Configuration page for Dynamips IOS routers.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
from gns3.qt import QtGui
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.dialogs.node_configurator_dialog import ConfigurationError
|
||||
from ..ui.ios_router_configuration_page_ui import Ui_iosRouterConfigPageWidget
|
||||
from ..settings import CHASSIS, ADAPTER_MATRIX, WIC_MATRIX
|
||||
@@ -56,6 +54,31 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
self.uiPrivateConfigToolButton.clicked.connect(self._privateConfigBrowserSlot)
|
||||
self.uiIOSImageToolButton.clicked.connect(self._iosImageBrowserSlot)
|
||||
|
||||
self._idle_valid = False
|
||||
idle_pc_rgx = QtCore.QRegExp("^(0x[0-9a-fA-F]+)?$")
|
||||
validator = QtGui.QRegExpValidator(idle_pc_rgx)
|
||||
self.uiIdlepcLineEdit.setValidator(validator)
|
||||
self.uiIdlepcLineEdit.textChanged.connect(self._idlePCValidateSlot)
|
||||
self.uiIdlepcLineEdit.textChanged.emit(self.uiIdlepcLineEdit.text())
|
||||
|
||||
def _idlePCValidateSlot(self):
|
||||
"""
|
||||
Slot to validate the entered Idle-PC Value
|
||||
"""
|
||||
sender = self.sender()
|
||||
validator = sender.validator()
|
||||
state = validator.validate(sender.text(), 0)[0]
|
||||
if state == QtGui.QValidator.Acceptable:
|
||||
color = '#A2C964' # green
|
||||
self._idle_valid = True
|
||||
elif state == QtGui.QValidator.Intermediate:
|
||||
color = '#fff79a' # yellow
|
||||
self._idle_valid = False
|
||||
else:
|
||||
color = '#f6989d' # red
|
||||
self._idle_valid = False
|
||||
sender.setStyleSheet('QLineEdit { background-color: %s }' % color)
|
||||
|
||||
def _iosImageBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select an IOU image.
|
||||
@@ -98,10 +121,7 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
Slot to open a file browser and select a startup-config file.
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
|
||||
if not path:
|
||||
return
|
||||
@@ -118,10 +138,7 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
Slot to open a file browser and select a private-config file.
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select a private configuration", config_dir)
|
||||
if not path:
|
||||
return
|
||||
@@ -154,13 +171,14 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
if type(slot_adapters) == str:
|
||||
# only one default adapter for this slot.
|
||||
self._widget_slots[slot_number].addItem(slot_adapters)
|
||||
# elif platform == "c7200" and slot_number == 0 and settings["slot0"] != None:
|
||||
# # special case
|
||||
# self._widget_slots[slot_number].addItem(settings["slot0"])
|
||||
else:
|
||||
# list of adapters
|
||||
module_list = list(slot_adapters)
|
||||
self._widget_slots[slot_number].addItems([""] + module_list)
|
||||
if platform == "c7200" and slot_number == 0:
|
||||
# special case
|
||||
self._widget_slots[slot_number].addItems(module_list)
|
||||
else:
|
||||
self._widget_slots[slot_number].addItems([""] + module_list)
|
||||
|
||||
# set the combox box to the correct slot adapter if configured.
|
||||
if settings["slot" + str(slot_number)]:
|
||||
@@ -210,12 +228,6 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
self.uiAuxPortLabel.hide()
|
||||
self.uiAuxPortSpinBox.hide()
|
||||
|
||||
# load the startup-config
|
||||
self.uiStartupConfigLineEdit.setText(settings["startup_config"])
|
||||
|
||||
# load the private-config
|
||||
self.uiPrivateConfigLineEdit.setText(settings["private_config"])
|
||||
|
||||
# load the MAC address setting
|
||||
self.uiBaseMACLineEdit.setInputMask("HHHH.HHHH.HHHH;_")
|
||||
|
||||
@@ -235,12 +247,6 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
self.uiIOSImageLabel.hide()
|
||||
self.uiIOSImageLineEdit.hide()
|
||||
self.uiIOSImageToolButton.hide()
|
||||
self.uiStartupConfigLabel.hide()
|
||||
self.uiStartupConfigLineEdit.hide()
|
||||
self.uiStartupConfigToolButton.hide()
|
||||
self.uiPrivateConfigLabel.hide()
|
||||
self.uiPrivateConfigLineEdit.hide()
|
||||
self.uiPrivateConfigToolButton.hide()
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
self.uiAuxPortLabel.hide()
|
||||
@@ -248,6 +254,19 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
self.uiBaseMacLabel.hide()
|
||||
self.uiBaseMACLineEdit.hide()
|
||||
|
||||
if not node:
|
||||
# load the startup-config
|
||||
self.uiStartupConfigLineEdit.setText(settings["startup_config"])
|
||||
# load the private-config
|
||||
self.uiPrivateConfigLineEdit.setText(settings["private_config"])
|
||||
else:
|
||||
self.uiStartupConfigLabel.hide()
|
||||
self.uiStartupConfigLineEdit.hide()
|
||||
self.uiStartupConfigToolButton.hide()
|
||||
self.uiPrivateConfigLabel.hide()
|
||||
self.uiPrivateConfigLineEdit.hide()
|
||||
self.uiPrivateConfigToolButton.hide()
|
||||
|
||||
# show the platform and chassis if applicable
|
||||
platform = settings["platform"]
|
||||
self.uiPlatformTextLabel.setText(platform)
|
||||
@@ -420,14 +439,17 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
|
||||
if not group:
|
||||
|
||||
# Check if the Idle-PC value has been validated okay
|
||||
if not self._idle_valid:
|
||||
idle_pc = self.uiIdlepcLineEdit.text()
|
||||
QtGui.QMessageBox.critical(self, "Idle-PC", "{} is not a valid Idle-PC value ".format(idle_pc))
|
||||
raise ConfigurationError()
|
||||
|
||||
# set the device name
|
||||
name = self.uiNameLineEdit.text()
|
||||
if not name:
|
||||
QtGui.QMessageBox.critical(self, "Name", "IOS router name cannot be empty!")
|
||||
elif node and not re.search(r"""^[\-\w]+$""", name):
|
||||
# IOS names must start with a letter, end with a letter or digit, and
|
||||
# have as interior characters only letters, digits, and hyphens.
|
||||
# They must be 63 characters or fewer.
|
||||
elif node and not node.validateHostname(name):
|
||||
QtGui.QMessageBox.critical(self, "Name", "Invalid name detected for IOS router: {}".format(name))
|
||||
else:
|
||||
settings["name"] = name
|
||||
@@ -435,20 +457,6 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
settings["console"] = self.uiConsolePortSpinBox.value()
|
||||
settings["aux"] = self.uiAuxPortSpinBox.value()
|
||||
|
||||
startup_config = self.uiStartupConfigLineEdit.text()
|
||||
if startup_config != settings["startup_config"]:
|
||||
if os.access(startup_config, os.R_OK):
|
||||
settings["startup_config"] = startup_config
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
|
||||
|
||||
private_config = self.uiPrivateConfigLineEdit.text()
|
||||
if private_config != settings["private_config"]:
|
||||
if os.access(private_config, os.R_OK):
|
||||
settings["private_config"] = private_config
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
|
||||
|
||||
# check and save the base MAC address
|
||||
#mac = self.uiBaseMACLineEdit.text()
|
||||
#if mac and not re.search(r"""^([0-9a-fA-F]{4}\.){2}[0-9a-fA-F]{4}$""", mac):
|
||||
@@ -470,6 +478,21 @@ class IOSRouterConfigurationPage(QtGui.QWidget, Ui_iosRouterConfigPageWidget):
|
||||
del settings["private_config"]
|
||||
del settings["image"]
|
||||
|
||||
if not node:
|
||||
startup_config = self.uiStartupConfigLineEdit.text()
|
||||
if startup_config != settings["startup_config"]:
|
||||
if os.access(startup_config, os.R_OK):
|
||||
settings["startup_config"] = startup_config
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Startup-config", "Cannot read the startup-config file")
|
||||
|
||||
private_config = self.uiPrivateConfigLineEdit.text()
|
||||
if private_config != settings["private_config"]:
|
||||
if os.access(private_config, os.R_OK):
|
||||
settings["private_config"] = private_config
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Private-config", "Cannot read the private-config file")
|
||||
|
||||
# get the platform and chassis if applicable
|
||||
platform = settings["platform"]
|
||||
if "chassis" in settings:
|
||||
|
||||
@@ -22,26 +22,31 @@ Configuration page for IOS router preferences.
|
||||
import os
|
||||
import copy
|
||||
import sys
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import math
|
||||
import zipfile
|
||||
import logging
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.node import Node
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.cloud.utils import UploadFilesThread
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.file_copy_thread import FileCopyThread
|
||||
|
||||
from .. import Dynamips
|
||||
from ..settings import IOS_ROUTER_SETTINGS
|
||||
from ..utils.decompress_ios import isIOSCompressed
|
||||
from ..utils.decompress_ios_thread import DecompressIOSThread
|
||||
from .. import Dynamips
|
||||
from ..ui.ios_router_preferences_page_ui import Ui_IOSRouterPreferencesPageWidget
|
||||
from ..pages.ios_router_configuration_page import IOSRouterConfigurationPage
|
||||
from ..dialogs.ios_router_wizard import IOSRouterWizard
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for IOS routers.
|
||||
@@ -86,48 +91,43 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
Creates a new IOS router.
|
||||
"""
|
||||
|
||||
wizard = IOSRouterWizard(parent=self)
|
||||
wizard = IOSRouterWizard(self._ios_routers, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
|
||||
ios_settings = wizard.getSettings()
|
||||
key = "{server}:{name}".format(server=ios_settings["server"], name=ios_settings["name"])
|
||||
|
||||
# set the default base startup-config
|
||||
resource_name = "configs/ios_base_startup-config.txt"
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
startup_config = os.path.normpath(resource_name)
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
ios_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
|
||||
startup_config = os.path.normpath(ios_base_config_path)
|
||||
self._ios_routers[key] = IOS_ROUTER_SETTINGS.copy()
|
||||
self._ios_routers[key].update(ios_settings)
|
||||
|
||||
# set the default base private-config
|
||||
resource_name = "configs/ios_base_private-config.txt"
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
private_config = os.path.normpath(resource_name)
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
ios_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
|
||||
private_config = os.path.normpath(ios_base_config_path)
|
||||
if ios_settings["server"] == 'cloud':
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
self._ios_routers[key] = {"name": ios_settings["name"],
|
||||
"path": ios_settings["path"],
|
||||
"image": ios_settings["image"],
|
||||
"default_symbol": ":/symbols/router.normal.svg",
|
||||
"hover_symbol": ":/symbols/router.selected.svg",
|
||||
"category": Node.routers,
|
||||
"startup_config": startup_config,
|
||||
"private_config": private_config,
|
||||
"platform": ios_settings["platform"],
|
||||
"chassis": ios_settings["chassis"],
|
||||
"idlepc": ios_settings["idlepc"],
|
||||
"ram": ios_settings["ram"],
|
||||
"nvram": 256,
|
||||
"mac_addr": "",
|
||||
"disk0": 1,
|
||||
"disk1": 0,
|
||||
"confreg": "0x2102",
|
||||
"system_id": "FTX0945W0MY",
|
||||
"server": ios_settings["server"]}
|
||||
log.debug(ios_settings["image"])
|
||||
# Start uploading the image to cloud files
|
||||
|
||||
self._upload_image_progress_dialog = QtGui.QProgressDialog("Uploading image file {}".format(ios_settings['image']), "Cancel", 0, 0, parent=self)
|
||||
self._upload_image_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self._upload_image_progress_dialog.setWindowTitle("IOS image upload")
|
||||
self._upload_image_progress_dialog.show()
|
||||
try:
|
||||
upload_thread = UploadFilesThread(
|
||||
self,
|
||||
cloud_settings=MainWindow.instance().cloudSettings(),
|
||||
files_to_upload=[(
|
||||
self._ios_routers[key]["path"],
|
||||
'images/' + os.path.relpath(self._ios_routers[key]["path"],
|
||||
self._main_window.settings()["images_path"])
|
||||
)]
|
||||
)
|
||||
upload_thread.completed.connect(self._imageUploadComplete)
|
||||
upload_thread.start()
|
||||
except Exception as e:
|
||||
self._upload_image_progress_dialog.reject()
|
||||
log.error(e)
|
||||
QtGui.QMessageBox.critical(self, "IOS image upload", "Error uploading IOS image: {}".format(e))
|
||||
|
||||
if ios_settings["platform"] == "c7200":
|
||||
self._ios_routers[key]["midplane"] = "vxr"
|
||||
@@ -153,6 +153,11 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
self._items.append(item)
|
||||
self.uiIOSRoutersTreeWidget.setCurrentItem(item)
|
||||
|
||||
def _imageUploadComplete(self):
|
||||
if self._upload_image_progress_dialog.wasCanceled():
|
||||
return
|
||||
self._upload_image_progress_dialog.accept()
|
||||
|
||||
def _iosRouterEditSlot(self):
|
||||
"""
|
||||
Edits an IOS router.
|
||||
@@ -166,11 +171,18 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
if ios_router["name"] != item.text(0):
|
||||
if "{}:{}".format(ios_router["server"], ios_router["name"]) in self._ios_routers:
|
||||
# FIXME: bug when changing name
|
||||
QtGui.QMessageBox.critical(self, "New IOS router", "IOS router name {} already exists".format(ios_router["name"]))
|
||||
# rename the IOS router
|
||||
new_key = "{server}:{name}".format(server=ios_router["server"], name=ios_router["name"])
|
||||
if new_key in self._ios_routers:
|
||||
QtGui.QMessageBox.critical(self, "IOS router", "IOS router name {} already exists for server {}".format(ios_router["name"],
|
||||
ios_router["server"]))
|
||||
ios_router["name"] = item.text(0)
|
||||
return
|
||||
self._ios_routers[new_key] = self._ios_routers[key]
|
||||
del self._ios_routers[key]
|
||||
item.setText(0, ios_router["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
|
||||
self._refreshInfo(ios_router)
|
||||
|
||||
def _iosRouterDeleteSlot(self):
|
||||
@@ -181,8 +193,14 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
item = self.uiIOSRoutersTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ios_router = self._ios_routers[key]
|
||||
|
||||
del self._ios_routers[key]
|
||||
self.uiIOSRoutersTreeWidget.takeTopLevelItem(self.uiIOSRoutersTreeWidget.indexOfTopLevelItem(item))
|
||||
if self._ios_routers == {}:
|
||||
self.uiEditIOSRouterPushButton.setEnabled(False)
|
||||
self.uiDeleteIOSRouterPushButton.setEnabled(False)
|
||||
self.uiDecompressIOSPushButton.setEnabled(False)
|
||||
|
||||
@staticmethod
|
||||
def getIOSImage(parent):
|
||||
@@ -231,7 +249,7 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(parent, "IOS images directory", "Could not create the IOS images directory {}: {}".format(destination_directory, str(e)))
|
||||
QtGui.QMessageBox.critical(parent, "IOS images directory", "Could not create the IOS images directory {}: {}".format(destination_directory, e))
|
||||
return
|
||||
|
||||
if isIOSCompressed(path):
|
||||
@@ -249,21 +267,25 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
path = decompressed_image_path
|
||||
thread.wait()
|
||||
|
||||
if os.path.dirname(path) != destination_directory:
|
||||
if os.path.normpath(os.path.dirname(path)) != destination_directory:
|
||||
# the IOS image is not in the default images directory
|
||||
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
# try to create a symbolic link to it
|
||||
symlink_path = new_destination_path
|
||||
os.symlink(path, symlink_path)
|
||||
path = symlink_path
|
||||
except (OSError, NotImplementedError):
|
||||
# if unsuccessful, then copy the IOS image itself
|
||||
try:
|
||||
shutil.copyfile(path, new_destination_path)
|
||||
path = new_destination_path
|
||||
except OSError:
|
||||
pass
|
||||
reply = QtGui.QMessageBox.question(parent,
|
||||
"IOS image",
|
||||
"Would you like to copy {} to the default images directory".format(os.path.basename(path)),
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
thread = FileCopyThread(path, destination_path)
|
||||
progress_dialog = ProgressDialog(thread, "IOS image", "Copying {}".format(os.path.basename(path)), "Cancel", busy=True, parent=parent)
|
||||
thread.deleteLater()
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
errors = progress_dialog.errors()
|
||||
if errors:
|
||||
QtGui.QMessageBox.critical(parent, "IOS image", "{}".format("".join(errors)))
|
||||
else:
|
||||
path = destination_path
|
||||
|
||||
return path
|
||||
|
||||
@@ -293,46 +315,6 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
# round up to the closest multiple of 32 (step of the RAM SpinBox)
|
||||
return math.ceil(decompressed_size / 32) * 32
|
||||
|
||||
def _startupConfigBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a startup-config file.
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
QtGui.QMessageBox.critical(self, "Startup configuration", "Cannot read {}".format(path))
|
||||
return
|
||||
|
||||
self.uiStartupConfigLineEdit.clear()
|
||||
self.uiStartupConfigLineEdit.setText(path)
|
||||
|
||||
def _privateConfigBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a private-config file.
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select a private configuration", config_dir)
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
QtGui.QMessageBox.critical(self, "Private configuration", "Cannot read {}".format(path))
|
||||
return
|
||||
|
||||
self.uiPrivateConfigLineEdit.clear()
|
||||
self.uiPrivateConfigLineEdit.setText(path)
|
||||
|
||||
def _decompressIOSSlot(self):
|
||||
"""
|
||||
Slot to decompress an IOS image.
|
||||
@@ -457,16 +439,16 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
|
||||
Change a symbol for an IOS router.
|
||||
"""
|
||||
|
||||
dialog = SymbolSelectionDialog(self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item = self.uiIOSRoutersTreeWidget.currentItem()
|
||||
if item:
|
||||
item = self.uiIOSRoutersTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ios_router = self._ios_routers[key]
|
||||
dialog = SymbolSelectionDialog(self, symbol=ios_router["default_symbol"], category=ios_router["category"])
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item.setIcon(0, QtGui.QIcon(normal_symbol))
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
ios_router = self._ios_routers[key]
|
||||
ios_router["default_symbol"] = normal_symbol
|
||||
ios_router["hover_symbol"] = selected_symbol
|
||||
ios_router["category"] = category
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
Default Dynamips settings.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import sys
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
# default path to Dynamips executable
|
||||
@@ -81,13 +82,67 @@ DYNAMIPS_SETTING_TYPES = {
|
||||
"mmap_support": bool,
|
||||
}
|
||||
|
||||
IOS_ROUTER_SETTINGS = {
|
||||
"name": "",
|
||||
"path": "",
|
||||
"image": "",
|
||||
"default_symbol": ":/symbols/router.normal.svg",
|
||||
"hover_symbol": ":/symbols/router.selected.svg",
|
||||
"category": Node.routers,
|
||||
"startup_config": "",
|
||||
"private_config": "",
|
||||
"platform": "",
|
||||
"chassis": "",
|
||||
"idlepc": "",
|
||||
"idlemax": 500,
|
||||
"idlesleep": 30,
|
||||
"exec_area": 64,
|
||||
"mmap": True,
|
||||
"sparsemem": True,
|
||||
"ram": 128,
|
||||
"nvram": 256,
|
||||
"mac_addr": "",
|
||||
"disk0": 1,
|
||||
"disk1": 0,
|
||||
"confreg": "0x2102",
|
||||
"system_id": "FTX0945W0MY",
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
IOS_ROUTER_SETTING_TYPES = {
|
||||
"name": str,
|
||||
"path": str,
|
||||
"image": str,
|
||||
"default_symbol": str,
|
||||
"hover_symbol": str,
|
||||
"category": int,
|
||||
"startup_config": str,
|
||||
"private_config": str,
|
||||
"platform": str,
|
||||
"chassis": str,
|
||||
"idlepc": str,
|
||||
"idlemax": int,
|
||||
"idlesleep": int,
|
||||
"exec_area": int,
|
||||
"mmap": bool,
|
||||
"sparsemem": bool,
|
||||
"ram": int,
|
||||
"nvram": int,
|
||||
"mac_addr": str,
|
||||
"disk0": int,
|
||||
"disk1": int,
|
||||
"confreg": str,
|
||||
"system_id": str,
|
||||
"server": str
|
||||
}
|
||||
|
||||
# supported platforms with the default RAM value
|
||||
PLATFORMS_DEFAULT_RAM = {"c1700": 64,
|
||||
"c2600": 64,
|
||||
"c2691": 128,
|
||||
"c3600": 128,
|
||||
PLATFORMS_DEFAULT_RAM = {"c1700": 128,
|
||||
"c2600": 128,
|
||||
"c2691": 192,
|
||||
"c3600": 192,
|
||||
"c3725": 128,
|
||||
"c3745": 128,
|
||||
"c3745": 256,
|
||||
"c7200": 512}
|
||||
|
||||
# platforms with supported chassis
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>419</width>
|
||||
<height>522</height>
|
||||
<width>435</width>
|
||||
<height>510</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -65,7 +65,7 @@
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="uiIOSImageLabel">
|
||||
<property name="text">
|
||||
<string>IOS image:</string>
|
||||
<string>IOS image path:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -89,7 +89,7 @@
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="uiStartupConfigLabel">
|
||||
<property name="text">
|
||||
<string>Startup-config:</string>
|
||||
<string>Initial startup-config:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -113,7 +113,7 @@
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="uiPrivateConfigLabel">
|
||||
<property name="text">
|
||||
<string>Private-config:</string>
|
||||
<string>Initial private-config:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_configuration_page.ui'
|
||||
#
|
||||
# Created: Fri Oct 10 10:43:48 2014
|
||||
# Created: Wed Dec 24 17:35:25 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -26,7 +26,7 @@ except AttributeError:
|
||||
class Ui_iosRouterConfigPageWidget(object):
|
||||
def setupUi(self, iosRouterConfigPageWidget):
|
||||
iosRouterConfigPageWidget.setObjectName(_fromUtf8("iosRouterConfigPageWidget"))
|
||||
iosRouterConfigPageWidget.resize(419, 522)
|
||||
iosRouterConfigPageWidget.resize(435, 510)
|
||||
self.vboxlayout = QtGui.QVBoxLayout(iosRouterConfigPageWidget)
|
||||
self.vboxlayout.setObjectName(_fromUtf8("vboxlayout"))
|
||||
self.uiTabWidget = QtGui.QTabWidget(iosRouterConfigPageWidget)
|
||||
@@ -566,11 +566,11 @@ class Ui_iosRouterConfigPageWidget(object):
|
||||
self.uiNameLabel.setText(_translate("iosRouterConfigPageWidget", "Name:", None))
|
||||
self.uiPlatformLabel.setText(_translate("iosRouterConfigPageWidget", "Platform:", None))
|
||||
self.uiChassisLabel.setText(_translate("iosRouterConfigPageWidget", "Chassis:", None))
|
||||
self.uiIOSImageLabel.setText(_translate("iosRouterConfigPageWidget", "IOS image:", None))
|
||||
self.uiIOSImageLabel.setText(_translate("iosRouterConfigPageWidget", "IOS image path:", None))
|
||||
self.uiIOSImageToolButton.setText(_translate("iosRouterConfigPageWidget", "&Browse...", None))
|
||||
self.uiStartupConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Startup-config:", None))
|
||||
self.uiStartupConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Initial startup-config:", None))
|
||||
self.uiStartupConfigToolButton.setText(_translate("iosRouterConfigPageWidget", "&Browse...", None))
|
||||
self.uiPrivateConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Private-config:", None))
|
||||
self.uiPrivateConfigLabel.setText(_translate("iosRouterConfigPageWidget", "Initial private-config:", None))
|
||||
self.uiPrivateConfigToolButton.setText(_translate("iosRouterConfigPageWidget", "&Browse...", None))
|
||||
self.uiConsolePortLabel.setText(_translate("iosRouterConfigPageWidget", "Console port:", None))
|
||||
self.uiAuxPortLabel.setText(_translate("iosRouterConfigPageWidget", "Aux port:", None))
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>12</pointsize>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/Users/jseutter/projects/gns3-gui/gns3/modules/dynamips/ui/ios_router_preferences_page.ui'
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_preferences_page.ui'
|
||||
#
|
||||
# Created: Tue Oct 14 11:35:46 2014
|
||||
# Created: Wed Nov 19 18:57:20 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -37,7 +37,7 @@ class Ui_IOSRouterPreferencesPageWidget(object):
|
||||
self.uiIOSRoutersTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiIOSRoutersTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(12)
|
||||
font.setPointSize(11)
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.uiIOSRoutersTreeWidget.setFont(font)
|
||||
|
||||
@@ -183,6 +183,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiEtherSwitchCheckBox">
|
||||
<property name="text">
|
||||
<string>This is an EtherSwitch router</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWizardPage" name="uiMemoryWizardPage">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/dynamips/ui/ios_router_wizard.ui'
|
||||
#
|
||||
# Created: Wed Oct 22 16:46:37 2014
|
||||
# Created: Wed Dec 31 11:23:11 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -111,6 +111,9 @@ class Ui_IOSRouterWizard(object):
|
||||
self.uiChassisLabel = QtGui.QLabel(self.uiNamePlatformWizardPage)
|
||||
self.uiChassisLabel.setObjectName(_fromUtf8("uiChassisLabel"))
|
||||
self.gridLayout.addWidget(self.uiChassisLabel, 2, 0, 1, 1)
|
||||
self.uiEtherSwitchCheckBox = QtGui.QCheckBox(self.uiNamePlatformWizardPage)
|
||||
self.uiEtherSwitchCheckBox.setObjectName(_fromUtf8("uiEtherSwitchCheckBox"))
|
||||
self.gridLayout.addWidget(self.uiEtherSwitchCheckBox, 3, 0, 1, 2)
|
||||
IOSRouterWizard.addPage(self.uiNamePlatformWizardPage)
|
||||
self.uiMemoryWizardPage = QtGui.QWizardPage()
|
||||
self.uiMemoryWizardPage.setObjectName(_fromUtf8("uiMemoryWizardPage"))
|
||||
@@ -299,6 +302,7 @@ class Ui_IOSRouterWizard(object):
|
||||
self.uiTypeLabel.setText(_translate("IOSRouterWizard", "Platform:", None))
|
||||
self.uiNameLabel.setText(_translate("IOSRouterWizard", "Name:", None))
|
||||
self.uiChassisLabel.setText(_translate("IOSRouterWizard", "Chassis:", None))
|
||||
self.uiEtherSwitchCheckBox.setText(_translate("IOSRouterWizard", "This is an EtherSwitch router", None))
|
||||
self.uiMemoryWizardPage.setTitle(_translate("IOSRouterWizard", "Memory", None))
|
||||
self.uiMemoryWizardPage.setSubTitle(_translate("IOSRouterWizard", "Please check the amount of memory (RAM) that you allocate to IOS. Not enough RAM could prevent IOS to start.", None))
|
||||
self.uiRamLabel.setText(_translate("IOSRouterWizard", "Default RAM:", None))
|
||||
@@ -320,7 +324,7 @@ class Ui_IOSRouterWizard(object):
|
||||
self.uiWic1Label.setText(_translate("IOSRouterWizard", "wic 1:", None))
|
||||
self.uiWic2Label.setText(_translate("IOSRouterWizard", "wic 2:", None))
|
||||
self.uiIdlePCWizardPage.setTitle(_translate("IOSRouterWizard", "Idle-PC", None))
|
||||
self.uiIdlePCWizardPage.setSubTitle(_translate("IOSRouterWizard", "An Idle-PC value is necessary to prevent IOS to use 100% of your processor or one of its core.", None))
|
||||
self.uiIdlePCWizardPage.setSubTitle(_translate("IOSRouterWizard", "An idle-pc value is necessary to prevent IOS to use 100% of your processor or one of its core.", None))
|
||||
self.uiIdlepcLabel.setText(_translate("IOSRouterWizard", "Idle-PC:", None))
|
||||
self.uiIdlePCFinderPushButton.setText(_translate("IOSRouterWizard", "Idle-PC finder", None))
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .iou_device import IOUDevice
|
||||
from .settings import IOU_SETTINGS, IOU_SETTING_TYPES
|
||||
from .settings import IOU_DEVICE_SETTINGS, IOU_DEVICE_SETTING_TYPES
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -91,33 +92,14 @@ class IOU(Module):
|
||||
size = settings.beginReadArray("iou_device")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
name = settings.value("name", "IOU{}".format(index + 1))
|
||||
default_symbol = settings.value("default_symbol", ":/symbols/multilayer_switch.normal.svg")
|
||||
hover_symbol = settings.value("hover_symbol", ":/symbols/multilayer_switch.selected.svg")
|
||||
category = settings.value("category", Node.routers, type=int)
|
||||
path = settings.value("path", "")
|
||||
image = settings.value("image", "")
|
||||
initial_config = settings.value("initial_config", "")
|
||||
use_default_iou_values = settings.value("use_default_iou_values", True, type=bool)
|
||||
ram = settings.value("ram", 256, type=int)
|
||||
nvram = settings.value("nvram", 128, type=int)
|
||||
ethernet_adapters = settings.value("ethernet_adapters", 2, type=int)
|
||||
serial_adapters = settings.value("serial_adapters", 2, type=int)
|
||||
server = settings.value("server", "local")
|
||||
name = settings.value("name")
|
||||
server = settings.value("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
self._iou_devices[key] = {"name": name,
|
||||
"path": path,
|
||||
"default_symbol": default_symbol,
|
||||
"hover_symbol": hover_symbol,
|
||||
"category": category,
|
||||
"image": image,
|
||||
"initial_config": initial_config,
|
||||
"use_default_iou_values": use_default_iou_values,
|
||||
"ram": ram,
|
||||
"nvram": nvram,
|
||||
"ethernet_adapters": ethernet_adapters,
|
||||
"serial_adapters": serial_adapters,
|
||||
"server": server}
|
||||
if key in self._iou_devices or not name or not server:
|
||||
continue
|
||||
self._iou_devices[key] = {}
|
||||
for setting_name, default_value in IOU_DEVICE_SETTINGS.items():
|
||||
self._iou_devices[key][setting_name] = settings.value(setting_name, default_value, IOU_DEVICE_SETTING_TYPES[setting_name])
|
||||
|
||||
settings.endArray()
|
||||
settings.endGroup()
|
||||
@@ -286,8 +268,11 @@ class IOU(Module):
|
||||
|
||||
if params:
|
||||
if "iourc" in params:
|
||||
# encode the iourc file in base64
|
||||
params["iourc"] = self._base64iourc(params["iourc"])
|
||||
if os.path.isfile(params["iourc"]):
|
||||
# encode the iourc file in base64
|
||||
params["iourc"] = self._base64iourc(params["iourc"])
|
||||
else:
|
||||
del params["iourc"]
|
||||
for server in self._servers:
|
||||
# send the local working directory only if this is a local server
|
||||
if server.isLocal():
|
||||
@@ -314,8 +299,11 @@ class IOU(Module):
|
||||
log.info("sending IOU settings to server {}:{}".format(server.host, server.port))
|
||||
params = self._settings.copy()
|
||||
|
||||
# encode the iourc file in base64
|
||||
params["iourc"] = self._base64iourc(params["iourc"])
|
||||
if os.path.isfile(params["iourc"]):
|
||||
# encode the iourc file in base64
|
||||
params["iourc"] = self._base64iourc(params["iourc"])
|
||||
else:
|
||||
del params["iourc"]
|
||||
|
||||
# send the local working directory only if this is a local server
|
||||
if server.isLocal():
|
||||
@@ -339,7 +327,7 @@ class IOU(Module):
|
||||
|
||||
log.info("creating node {}".format(node_class))
|
||||
|
||||
if not self._settings["iourc"] or not os.path.isfile(self._settings["iourc"]):
|
||||
if server.isLocal() and (not self._settings["iourc"] or not os.path.isfile(self._settings["iourc"])):
|
||||
raise ModuleError("The path to IOURC must be configured")
|
||||
|
||||
if not server.connected():
|
||||
@@ -407,7 +395,12 @@ class IOU(Module):
|
||||
settings["nvram"] = self._iou_devices[iouimage]["nvram"]
|
||||
settings["ethernet_adapters"] = self._iou_devices[iouimage]["ethernet_adapters"]
|
||||
settings["serial_adapters"] = self._iou_devices[iouimage]["serial_adapters"]
|
||||
node.setup(iou_path, initial_settings=settings)
|
||||
|
||||
if node.server().isCloud():
|
||||
settings["cloud_path"] = "images/IOU"
|
||||
node.setup(self._iou_devices[iouimage]["image"], initial_settings=settings)
|
||||
else:
|
||||
node.setup(iou_path, initial_settings=settings)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
@@ -444,8 +437,8 @@ class IOU(Module):
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "exportConfig") and node.initialized():
|
||||
node.exportConfig(directory)
|
||||
if node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
@@ -455,8 +448,8 @@ class IOU(Module):
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "importConfig") and node.initialized():
|
||||
node.importConfig(directory)
|
||||
if node.initialized():
|
||||
node.importConfigFromDirectory(directory)
|
||||
|
||||
def findAlternativeIOUImage(self, image):
|
||||
"""
|
||||
|
||||
@@ -21,12 +21,14 @@ Wizard for IOU devices.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
from gns3.qt import QtGui
|
||||
from gns3.node import Node
|
||||
from gns3.servers import Servers
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
|
||||
from ....settings import ENABLE_CLOUD
|
||||
from ..ui.iou_device_wizard_ui import Ui_IOUDeviceWizard
|
||||
from .. import IOU
|
||||
|
||||
@@ -38,12 +40,15 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, iou_devices, parent):
|
||||
|
||||
QtGui.QWizard.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/multilayer_switch.normal.svg"))
|
||||
self.setWizardStyle(QtGui.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtGui.QWizard.NoDefaultButton)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
self.uiLoadBalanceCheckBox.toggled.connect(self._loadBalanceToggledSlot)
|
||||
@@ -61,12 +66,21 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
|
||||
self.uiNameImageWizardPage.registerField("name*", self.uiNameLineEdit)
|
||||
self.uiNameImageWizardPage.registerField("image*", self.uiIOUImageLineEdit)
|
||||
|
||||
self._iou_devices = iou_devices
|
||||
|
||||
if IOU.instance().settings()["use_local_server"]:
|
||||
# skip the server page if we use the local server
|
||||
self.setStartId(1)
|
||||
else:
|
||||
self.uiIOUImageToolButton.setEnabled(False)
|
||||
|
||||
if not ENABLE_CLOUD:
|
||||
self.uiCloudRadioButton.hide()
|
||||
|
||||
# location of the base config templates
|
||||
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_initial-config.txt"))
|
||||
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_initial-config.txt"))
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
@@ -138,11 +152,15 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
|
||||
Validates the server.
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
|
||||
#FIXME: prevent users to use "cloud"
|
||||
if self.uiCloudRadioButton.isChecked():
|
||||
QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!")
|
||||
if self.currentPage() == self.uiNameImageWizardPage:
|
||||
name = self.uiNameLineEdit.text()
|
||||
for iou_device in self._iou_devices.values():
|
||||
if iou_device["name"] == name:
|
||||
QtGui.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
|
||||
return False
|
||||
if self.currentPage() == self.uiServerWizardPage and self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in IOS on UNIX preferences")
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -155,45 +173,46 @@ class IOUDeviceWizard(QtGui.QWizard, Ui_IOUDeviceWizard):
|
||||
|
||||
path = self.uiIOUImageLineEdit.text()
|
||||
|
||||
initial_config = ""
|
||||
if self.uiTypeComboBox.currentText() == "L2 image":
|
||||
# set the default L2 base initial-config
|
||||
resource_name = "configs/iou_l2_base_initial-config.txt"
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
initial_config = os.path.normpath(resource_name)
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
|
||||
initial_config = os.path.normpath(iou_base_config_path)
|
||||
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
|
||||
if default_base_config:
|
||||
initial_config = default_base_config
|
||||
default_symbol = ":/symbols/multilayer_switch.normal.svg"
|
||||
hover_symbol = ":/symbols/multilayer_switch.selected.svg"
|
||||
category = Node.switches
|
||||
ethernet_adapters = 4
|
||||
serial_adapters = 0
|
||||
else:
|
||||
# set the default L3 base initial-config
|
||||
resource_name = "configs/iou_l3_base_initial-config.txt"
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
initial_config = os.path.normpath(resource_name)
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
|
||||
initial_config = os.path.normpath(iou_base_config_path)
|
||||
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
|
||||
if default_base_config:
|
||||
initial_config = default_base_config
|
||||
default_symbol = ":/symbols/router.normal.svg"
|
||||
hover_symbol = ":/symbols/router.selected.svg"
|
||||
category = Node.routers
|
||||
ethernet_adapters = 2
|
||||
serial_adapters = 2
|
||||
|
||||
if IOU.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
server = "local"
|
||||
elif self.uiLoadBalanceCheckBox.isChecked():
|
||||
server = next(iter(Servers.instance()))
|
||||
if not server:
|
||||
QtGui.QMessageBox.critical(self, "IOU device", "No remote server available!")
|
||||
return
|
||||
server = "{}:{}".format(server.host, server.port)
|
||||
else:
|
||||
server = self.uiRemoteServersComboBox.currentText()
|
||||
elif self.uiRemoteRadioButton.isChecked():
|
||||
if self.uiLoadBalanceCheckBox.isChecked():
|
||||
server = next(iter(Servers.instance()))
|
||||
server = "{}:{}".format(server.host, server.port)
|
||||
else:
|
||||
server = self.uiRemoteServersComboBox.currentText()
|
||||
else: # Cloud is selected
|
||||
server = "cloud"
|
||||
|
||||
settings = {
|
||||
"name": self.uiNameLineEdit.text(),
|
||||
"path": path,
|
||||
"image": os.path.basename(path),
|
||||
"initial_config": initial_config,
|
||||
"ethernet_adapters": ethernet_adapters,
|
||||
"serial_adapters": serial_adapters,
|
||||
"default_symbol": default_symbol,
|
||||
"category": category,
|
||||
"hover_symbol": hover_symbol,
|
||||
|
||||
@@ -20,12 +20,14 @@ IOU device implementation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import base64
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
from gns3.ports.serial_port import SerialPort
|
||||
from gns3.utils.normalize_filename import normalize_filename
|
||||
from .settings import IOU_DEVICE_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -54,11 +56,11 @@ class IOUDevice(Node):
|
||||
"path": "",
|
||||
"initial_config": "",
|
||||
"l1_keepalives": False,
|
||||
"use_default_iou_values": True,
|
||||
"ram": 256,
|
||||
"nvram": 128,
|
||||
"ethernet_adapters": 2,
|
||||
"serial_adapters": 2,
|
||||
"use_default_iou_values": IOU_DEVICE_SETTINGS["use_default_iou_values"],
|
||||
"ram": IOU_DEVICE_SETTINGS["ram"],
|
||||
"nvram": IOU_DEVICE_SETTINGS["nvram"],
|
||||
"ethernet_adapters": IOU_DEVICE_SETTINGS["ethernet_adapters"],
|
||||
"serial_adapters": IOU_DEVICE_SETTINGS["serial_adapters"],
|
||||
"console": None}
|
||||
|
||||
#self._occupied_slots = []
|
||||
@@ -85,7 +87,9 @@ class IOUDevice(Node):
|
||||
else:
|
||||
port = SerialPort
|
||||
port_name = port.longNameType() + str(slot_number) + "/" + str(port_number)
|
||||
short_name = port.shortNameType() + str(slot_number) + "/" + str(port_number)
|
||||
new_port = port(port_name)
|
||||
new_port.setShortName(short_name)
|
||||
new_port.setPortNumber(port_number)
|
||||
new_port.setSlotNumber(slot_number)
|
||||
new_port.setPacketCaptureSupported(True)
|
||||
@@ -133,6 +137,9 @@ class IOUDevice(Node):
|
||||
if console:
|
||||
params["console"] = self._settings["console"] = console
|
||||
|
||||
if "cloud_path" in initial_settings:
|
||||
params["cloud_path"] = self._settings["cloud_path"] = initial_settings.pop("cloud_path")
|
||||
|
||||
# other initial settings will be applied when the router has been created
|
||||
if initial_settings:
|
||||
self._inital_settings = initial_settings
|
||||
@@ -497,7 +504,7 @@ class IOUDevice(Node):
|
||||
try:
|
||||
port.startPacketCapture(result["capture_file_path"])
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
|
||||
self.updated_signal.emit()
|
||||
break
|
||||
|
||||
@@ -614,7 +621,7 @@ class IOUDevice(Node):
|
||||
# save only the image name if it is stored the images directory
|
||||
iou["properties"]["path"] = os.path.basename(image_path)
|
||||
else:
|
||||
iou["properties"]["path"] = os.path.basename(image_path)
|
||||
iou["properties"]["path"] = image_path
|
||||
|
||||
return iou
|
||||
|
||||
@@ -671,7 +678,39 @@ class IOUDevice(Node):
|
||||
self._inital_settings = None
|
||||
self._loading = False
|
||||
|
||||
def exportConfig(self, directory):
|
||||
def exportConfig(self, config_export_path):
|
||||
"""
|
||||
Exports the initial-config
|
||||
|
||||
:param config_export_path: export path for the initial-config
|
||||
"""
|
||||
|
||||
self._config_export_path = config_export_path
|
||||
self._server.send_message("iou.export_config", {"id": self._iou_id}, self._exportConfigCallback)
|
||||
|
||||
def _exportConfigCallback(self, result, error=False):
|
||||
"""
|
||||
Callback for exportConfig.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while exporting {} initial-config: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["code"], result["message"])
|
||||
else:
|
||||
|
||||
if "initial_config_base64" in result and self._config_export_path:
|
||||
config = base64.decodebytes(result["initial_config_base64"].encode("utf-8"))
|
||||
try:
|
||||
with open(self._config_export_path, "wb") as f:
|
||||
log.info("saving {} initial-config to {}".format(self.name(), self._config_export_path))
|
||||
f.write(config)
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export initial-config to {}: {}".format(self._config_export_path, e))
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the initial-config to a directory.
|
||||
|
||||
@@ -679,11 +718,11 @@ class IOUDevice(Node):
|
||||
"""
|
||||
|
||||
self._export_directory = directory
|
||||
self._server.send_message("iou.export_config", {"id": self._iou_id}, self._exportConfigCallback)
|
||||
self._server.send_message("iou.export_config", {"id": self._iou_id}, self._exportConfigToDirectoryCallback)
|
||||
|
||||
def _exportConfigCallback(self, result, error=False):
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False):
|
||||
"""
|
||||
Callback for exportConfigs.
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
@@ -706,7 +745,17 @@ class IOUDevice(Node):
|
||||
|
||||
self._export_directory = None
|
||||
|
||||
def importConfig(self, directory):
|
||||
def importConfig(self, path):
|
||||
"""
|
||||
Imports an initial-config.
|
||||
|
||||
:param path: path to the initial config
|
||||
"""
|
||||
|
||||
new_settings = {"initial_config": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports an initial-config from a directory.
|
||||
|
||||
@@ -769,6 +818,23 @@ class IOUDevice(Node):
|
||||
from .pages.iou_device_configuration_page import iouDeviceConfigurationPage
|
||||
return iouDeviceConfigurationPage
|
||||
|
||||
@staticmethod
|
||||
def validateHostname(hostname):
|
||||
"""
|
||||
Checks if the hostname is valid.
|
||||
|
||||
:param hostname: hostname to check
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
# IOS names must start with a letter, end with a letter or digit, and
|
||||
# have as interior characters only letters, digits, and hyphens.
|
||||
# They must be 63 characters or fewer.
|
||||
if re.search(r"""^[\-\w]+$""", hostname) and len(hostname) <= 63:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def defaultSymbol():
|
||||
"""
|
||||
|
||||
@@ -20,12 +20,11 @@ Configuration page for IOU devices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
from gns3.qt import QtGui
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.dialogs.node_configurator_dialog import ConfigurationError
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
from ..ui.iou_device_configuration_page_ui import Ui_iouDeviceConfigPageWidget
|
||||
|
||||
|
||||
@@ -43,6 +42,10 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
|
||||
self.uiDefaultValuesCheckBox.stateChanged.connect(self._useDefaultValuesSlot)
|
||||
self._current_iou_image = ""
|
||||
|
||||
# location of the base config templates
|
||||
self._base_iou_l2_config_template = get_resource(os.path.join("configs", "iou_l2_base_initial-config.txt"))
|
||||
self._base_iou_l3_config_template = get_resource(os.path.join("configs", "iou_l3_base_initial-config.txt"))
|
||||
|
||||
def _useDefaultValuesSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not the RAM and NVRAM spin boxes.
|
||||
@@ -67,15 +70,23 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
|
||||
self.uiIOUImageLineEdit.clear()
|
||||
self.uiIOUImageLineEdit.setText(path)
|
||||
|
||||
if "l2" in path:
|
||||
# set the default L2 base initial-config
|
||||
default_base_config = get_default_base_config(self._base_iou_l2_config_template)
|
||||
if default_base_config:
|
||||
self.uiInitialConfigLineEdit.setText(default_base_config)
|
||||
else:
|
||||
# set the default L3 base initial-config
|
||||
default_base_config = get_default_base_config(self._base_iou_l3_config_template)
|
||||
if default_base_config:
|
||||
self.uiInitialConfigLineEdit.setText(default_base_config)
|
||||
|
||||
def _initialConfigBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a initial-config file.
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select an initial configuration", config_dir)
|
||||
if not path:
|
||||
return
|
||||
@@ -105,15 +116,20 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
|
||||
# load the initial-config
|
||||
self.uiInitialConfigLineEdit.setText(settings["initial_config"])
|
||||
|
||||
# load the IOU image path
|
||||
self.uiIOUImageLineEdit.setText(settings["path"])
|
||||
|
||||
else:
|
||||
self.uiGeneralgroupBox.hide()
|
||||
|
||||
if not node:
|
||||
# load the initial-config
|
||||
self.uiInitialConfigLineEdit.setText(settings["initial_config"])
|
||||
else:
|
||||
self.uiInitialConfigLabel.hide()
|
||||
self.uiInitialConfigLineEdit.hide()
|
||||
self.uiInitialConfigToolButton.hide()
|
||||
|
||||
# load advanced settings
|
||||
if "l1_keepalives" in settings:
|
||||
self.uiL1KeepalivesCheckBox.setChecked(settings["l1_keepalives"])
|
||||
@@ -144,23 +160,13 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
|
||||
name = self.uiNameLineEdit.text()
|
||||
if not name:
|
||||
QtGui.QMessageBox.critical(self, "Name", "IOU device name cannot be empty!")
|
||||
elif node and not re.search(r"""^[\-\w]+$""", name):
|
||||
# IOS names must start with a letter, end with a letter or digit, and
|
||||
# have as interior characters only letters, digits, and hyphens.
|
||||
# They must be 63 characters or fewer.
|
||||
elif node and not node.validateHostname(name):
|
||||
QtGui.QMessageBox.critical(self, "Name", "Invalid name detected for IOU device: {}".format(name))
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
settings["console"] = self.uiConsolePortSpinBox.value()
|
||||
|
||||
initial_config = self.uiInitialConfigLineEdit.text()
|
||||
if initial_config != settings["initial_config"]:
|
||||
if os.access(initial_config, os.R_OK):
|
||||
settings["initial_config"] = initial_config
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Initial-config", "Cannot read the initial-config file")
|
||||
|
||||
# save the IOU image path
|
||||
ios_path = self.uiIOUImageLineEdit.text().strip()
|
||||
if ios_path:
|
||||
@@ -169,6 +175,14 @@ class iouDeviceConfigurationPage(QtGui.QWidget, Ui_iouDeviceConfigPageWidget):
|
||||
del settings["name"]
|
||||
del settings["console"]
|
||||
|
||||
if not node:
|
||||
initial_config = self.uiInitialConfigLineEdit.text()
|
||||
if initial_config != settings["initial_config"]:
|
||||
if os.access(initial_config, os.R_OK):
|
||||
settings["initial_config"] = initial_config
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Initial-config", "Cannot read the initial-config file")
|
||||
|
||||
# save advanced settings
|
||||
settings["l1_keepalives"] = self.uiL1KeepalivesCheckBox.isChecked()
|
||||
settings["use_default_iou_values"] = self.uiDefaultValuesCheckBox.isChecked()
|
||||
|
||||
@@ -21,16 +21,18 @@ Configuration page for IOU device preferences.
|
||||
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import stat
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.cloud.utils import UploadFilesThread
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.file_copy_thread import FileCopyThread
|
||||
|
||||
from .. import IOU
|
||||
from ..settings import IOU_DEVICE_SETTINGS
|
||||
from ..ui.iou_device_preferences_page_ui import Ui_IOUDevicePreferencesPageWidget
|
||||
from ..pages.iou_device_configuration_page import iouDeviceConfigurationPage
|
||||
from ..dialogs.iou_device_wizard import IOUDeviceWizard
|
||||
@@ -55,9 +57,6 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
|
||||
self.uiIOUDevicesTreeWidget.currentItemChanged.connect(self._iouDeviceChangedSlot)
|
||||
self.uiIOUDevicesTreeWidget.itemPressed.connect(self._iouDevicePressedSlot)
|
||||
|
||||
# self.uiIOUPathToolButton.clicked.connect(self._iouImageBrowserSlot)
|
||||
# self.uiInitialConfigToolButton.clicked.connect(self._initialConfigBrowserSlot)
|
||||
|
||||
def _iouDeviceChangedSlot(self, current, previous):
|
||||
"""
|
||||
Loads a selected an IOU device from the tree widget.
|
||||
@@ -81,28 +80,15 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
|
||||
Creates a new IOU device.
|
||||
"""
|
||||
|
||||
wizard = IOUDeviceWizard(parent=self)
|
||||
wizard = IOUDeviceWizard(self._iou_devices, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
|
||||
new_device_settings = wizard.getSettings()
|
||||
|
||||
key = "{server}:{name}".format(server=new_device_settings["server"], name=new_device_settings["name"])
|
||||
self._iou_devices[key] = {"name": new_device_settings["name"],
|
||||
"path": new_device_settings["path"],
|
||||
"default_symbol": new_device_settings["default_symbol"],
|
||||
"hover_symbol": new_device_settings["hover_symbol"],
|
||||
"category": new_device_settings["category"],
|
||||
"image": os.path.basename(new_device_settings["path"]),
|
||||
"initial_config": new_device_settings["initial_config"],
|
||||
"use_default_iou_values": True,
|
||||
"ram": 256,
|
||||
"nvram": 128,
|
||||
"ethernet_adapters": 2,
|
||||
"serial_adapters": 2,
|
||||
"server": new_device_settings["server"]}
|
||||
|
||||
self._iou_devices[key] = IOU_DEVICE_SETTINGS.copy()
|
||||
self._iou_devices[key].update(new_device_settings)
|
||||
|
||||
item = QtGui.QTreeWidgetItem(self.uiIOUDevicesTreeWidget)
|
||||
item.setText(0, self._iou_devices[key]["name"])
|
||||
item.setIcon(0, QtGui.QIcon(self._iou_devices[key]["default_symbol"]))
|
||||
@@ -110,6 +96,33 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
|
||||
self._items.append(item)
|
||||
self.uiIOUDevicesTreeWidget.setCurrentItem(item)
|
||||
|
||||
if new_device_settings["server"] == 'cloud':
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Start uploading the image to cloud files
|
||||
self._upload_image_progress_dialog = QtGui.QProgressDialog(
|
||||
"Uploading image file {}".format(new_device_settings['image']), "Cancel", 0, 0, parent=self)
|
||||
self._upload_image_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self._upload_image_progress_dialog.setWindowTitle("IOU image upload")
|
||||
self._upload_image_progress_dialog.show()
|
||||
try:
|
||||
src = self._iou_devices[key]['path']
|
||||
# Eg: images/IOU/i86.bin
|
||||
dst = 'images/IOU/{}'.format(self._iou_devices[key]['image'])
|
||||
upload_thread = UploadFilesThread(self, MainWindow.instance().cloudSettings(), [(src, dst)])
|
||||
upload_thread.completed.connect(self._imageUploadComplete)
|
||||
upload_thread.start()
|
||||
except Exception as e:
|
||||
self._upload_image_progress_dialog.reject()
|
||||
log.error(e)
|
||||
QtGui.QMessageBox.critical(self, "IOU image upload", "Error uploading IOU image: {}".format(e))
|
||||
|
||||
def _imageUploadComplete(self):
|
||||
if self._upload_image_progress_dialog.wasCanceled():
|
||||
return
|
||||
self._upload_image_progress_dialog.accept()
|
||||
|
||||
def _iouDeviceEditSlot(self):
|
||||
"""
|
||||
Edits an IOU device.
|
||||
@@ -123,11 +136,16 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
if iou_device["name"] != item.text(0):
|
||||
if "{}:{}".format(iou_device["server"], iou_device["name"]) in self._iou_devices:
|
||||
# FIXME: bug when changing name
|
||||
QtGui.QMessageBox.critical(self, "New IOU device", "IOU device name {} already exists".format(iou_device["name"]))
|
||||
new_key = "{server}:{name}".format(server=iou_device["server"], name=iou_device["name"])
|
||||
if new_key in self._iou_devices:
|
||||
QtGui.QMessageBox.critical(self, "IOU device", "IOU device name {} already exists for server {}".format(iou_device["name"],
|
||||
iou_device["server"]))
|
||||
iou_device["name"] = item.text(0)
|
||||
return
|
||||
self._iou_devices[new_key] = self._iou_devices[key]
|
||||
del self._iou_devices[key]
|
||||
item.setText(0, iou_device["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(iou_device)
|
||||
|
||||
def _iouDeviceDeleteSlot(self):
|
||||
@@ -216,83 +234,41 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
|
||||
QtGui.QMessageBox.critical(parent, "IOU image", "Sorry, this is not a valid IOU image!")
|
||||
return
|
||||
|
||||
if not os.access(path, os.X_OK):
|
||||
QtGui.QMessageBox.critical(parent, "IOU image", "{} is not executable".format(path))
|
||||
return
|
||||
|
||||
try:
|
||||
os.makedirs(destination_directory)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(parent, "IOU images directory", "Could not create the IOU images directory {}: {}".format(destination_directory, str(e)))
|
||||
QtGui.QMessageBox.critical(parent, "IOU images directory", "Could not create the IOU images directory {}: {}".format(destination_directory, e))
|
||||
return
|
||||
|
||||
if os.path.dirname(path) != destination_directory:
|
||||
if os.path.normpath(os.path.dirname(path)) != destination_directory:
|
||||
# the IOU image is not in the default images directory
|
||||
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
# try to create a symbolic link to it
|
||||
symlink_path = new_destination_path
|
||||
os.symlink(path, symlink_path)
|
||||
path = symlink_path
|
||||
except (OSError, NotImplementedError):
|
||||
# if unsuccessful, then copy the IOU image itself
|
||||
try:
|
||||
shutil.copyfile(path, new_destination_path)
|
||||
path = new_destination_path
|
||||
except OSError:
|
||||
pass
|
||||
reply = QtGui.QMessageBox.question(parent,
|
||||
"IOU image",
|
||||
"Would you like to copy {} to the default images directory".format(os.path.basename(path)),
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
thread = FileCopyThread(path, destination_path)
|
||||
progress_dialog = ProgressDialog(thread, "Project", "Copying {}".format(os.path.basename(path)), "Cancel", busy=True, parent=parent)
|
||||
thread.deleteLater()
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
errors = progress_dialog.errors()
|
||||
if errors:
|
||||
QtGui.QMessageBox.critical(parent, "IOS image", "{}".format("".join(errors)))
|
||||
else:
|
||||
path = destination_path
|
||||
mode = os.stat(path).st_mode
|
||||
os.chmod(path, mode | stat.S_IXUSR)
|
||||
|
||||
if not os.access(path, os.X_OK):
|
||||
QtGui.QMessageBox.warning(parent, "IOU image", "{} is not executable".format(path))
|
||||
|
||||
return path
|
||||
|
||||
def _iouImageBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select an IOU image.
|
||||
"""
|
||||
|
||||
path = self.getIOUImage(self)
|
||||
if not path:
|
||||
return
|
||||
self.uiIOUPathLineEdit.clear()
|
||||
self.uiIOUPathLineEdit.setText(path)
|
||||
|
||||
if "l2" in path:
|
||||
# set the default L2 base initial-config
|
||||
resource_name = "configs/iou_l2_base_initial-config.txt"
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
self.uiInitialConfigLineEdit.setText(os.path.normpath(resource_name))
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
|
||||
self.uiInitialConfigLineEdit.setText(os.path.normpath(iou_base_config_path))
|
||||
else:
|
||||
# set the default L3 base initial-config
|
||||
resource_name = "configs/iou_l3_base_initial-config.txt"
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
self.uiInitialConfigLineEdit.setText(os.path.normpath(resource_name))
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
iou_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
|
||||
self.uiInitialConfigLineEdit.setText(os.path.normpath(iou_base_config_path))
|
||||
|
||||
def _initialConfigBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a initial-config file.
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select an initial configuration", config_dir)
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
QtGui.QMessageBox.critical(self, "Initial configuration", "Cannot read {}".format(path))
|
||||
return
|
||||
|
||||
self.uiInitialConfigLineEdit.clear()
|
||||
self.uiInitialConfigLineEdit.setText(path)
|
||||
|
||||
def _iouDevicePressedSlot(self, item, column):
|
||||
"""
|
||||
Slot for item pressed.
|
||||
@@ -321,16 +297,16 @@ class IOUDevicePreferencesPage(QtGui.QWidget, Ui_IOUDevicePreferencesPageWidget)
|
||||
Change a symbol for an IOU device.
|
||||
"""
|
||||
|
||||
dialog = SymbolSelectionDialog(self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item = self.uiIOUDevicesTreeWidget.currentItem()
|
||||
if item:
|
||||
item = self.uiIOUDevicesTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
iou_device = self._iou_devices[key]
|
||||
dialog = SymbolSelectionDialog(self, symbol=iou_device["default_symbol"], category=iou_device["category"])
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item.setIcon(0, QtGui.QIcon(normal_symbol))
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
iou_device = self._iou_devices[key]
|
||||
iou_device["default_symbol"] = normal_symbol
|
||||
iou_device["hover_symbol"] = selected_symbol
|
||||
iou_device["category"] = category
|
||||
|
||||
@@ -15,13 +15,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/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
"""
|
||||
Default IOU settings.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
if sys.platform.startswith("linux"):
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for iouyap in the current working directory and $PATH
|
||||
@@ -59,3 +62,35 @@ IOU_SETTING_TYPES = {
|
||||
"udp_end_port_range": int,
|
||||
"use_local_server": bool,
|
||||
}
|
||||
|
||||
IOU_DEVICE_SETTINGS = {
|
||||
"name": "",
|
||||
"path": "",
|
||||
"default_symbol": ":/symbols/multilayer_switch.normal.svg",
|
||||
"hover_symbol": ":/symbols/multilayer_switch.selected.svg",
|
||||
"category": Node.routers,
|
||||
"image": "",
|
||||
"initial_config": "",
|
||||
"use_default_iou_values": True,
|
||||
"ram": 256,
|
||||
"nvram": 128,
|
||||
"ethernet_adapters": 2,
|
||||
"serial_adapters": 2,
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
IOU_DEVICE_SETTING_TYPES = {
|
||||
"name": str,
|
||||
"path": str,
|
||||
"default_symbol": str,
|
||||
"hover_symbol": str,
|
||||
"category": int,
|
||||
"image": str,
|
||||
"initial_config": str,
|
||||
"use_default_iou_values": bool,
|
||||
"ram": int,
|
||||
"nvram": int,
|
||||
"ethernet_adapters": int,
|
||||
"serial_adapters": int,
|
||||
"server": str
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>420</width>
|
||||
<height>489</height>
|
||||
<width>392</width>
|
||||
<height>473</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -46,7 +46,7 @@
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiIOUImageLabel">
|
||||
<property name="text">
|
||||
<string>IOU image:</string>
|
||||
<string>IOU image path:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -70,7 +70,7 @@
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiInitialConfigLabel">
|
||||
<property name="text">
|
||||
<string>Initial config:</string>
|
||||
<string>Initial startup-config:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_device_configuration_page.ui'
|
||||
#
|
||||
# Created: Fri Oct 10 10:43:47 2014
|
||||
# Created: Wed Dec 24 17:35:25 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -26,7 +26,7 @@ except AttributeError:
|
||||
class Ui_iouDeviceConfigPageWidget(object):
|
||||
def setupUi(self, iouDeviceConfigPageWidget):
|
||||
iouDeviceConfigPageWidget.setObjectName(_fromUtf8("iouDeviceConfigPageWidget"))
|
||||
iouDeviceConfigPageWidget.resize(420, 489)
|
||||
iouDeviceConfigPageWidget.resize(392, 473)
|
||||
self.verticalLayout = QtGui.QVBoxLayout(iouDeviceConfigPageWidget)
|
||||
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
|
||||
self.uiTabWidget = QtGui.QTabWidget(iouDeviceConfigPageWidget)
|
||||
@@ -177,9 +177,9 @@ class Ui_iouDeviceConfigPageWidget(object):
|
||||
iouDeviceConfigPageWidget.setWindowTitle(_translate("iouDeviceConfigPageWidget", "IOU device configuration", None))
|
||||
self.uiGeneralgroupBox.setTitle(_translate("iouDeviceConfigPageWidget", "General", None))
|
||||
self.uiNameLabel.setText(_translate("iouDeviceConfigPageWidget", "Name:", None))
|
||||
self.uiIOUImageLabel.setText(_translate("iouDeviceConfigPageWidget", "IOU image:", None))
|
||||
self.uiIOUImageLabel.setText(_translate("iouDeviceConfigPageWidget", "IOU image path:", None))
|
||||
self.uiIOUImageToolButton.setText(_translate("iouDeviceConfigPageWidget", "&Browse...", None))
|
||||
self.uiInitialConfigLabel.setText(_translate("iouDeviceConfigPageWidget", "Initial config:", None))
|
||||
self.uiInitialConfigLabel.setText(_translate("iouDeviceConfigPageWidget", "Initial startup-config:", None))
|
||||
self.uiInitialConfigToolButton.setText(_translate("iouDeviceConfigPageWidget", "&Browse...", None))
|
||||
self.uiConsolePortLabel.setText(_translate("iouDeviceConfigPageWidget", "Console port:", None))
|
||||
self.uiOtherSettingsGroupBox.setTitle(_translate("iouDeviceConfigPageWidget", "Other settings", None))
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>12</pointsize>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/iou/ui/iou_device_preferences_page.ui'
|
||||
#
|
||||
# Created: Tue Oct 7 15:39:24 2014
|
||||
# Created: Wed Nov 19 18:57:19 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -37,7 +37,7 @@ class Ui_IOUDevicePreferencesPageWidget(object):
|
||||
self.uiIOUDevicesTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiIOUDevicesTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(12)
|
||||
font.setPointSize(11)
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.uiIOUDevicesTreeWidget.setFont(font)
|
||||
|
||||
@@ -28,6 +28,7 @@ from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .qemu_vm import QemuVM
|
||||
from .settings import QEMU_SETTINGS, QEMU_SETTING_TYPES
|
||||
from .settings import QEMU_VM_SETTINGS, QEMU_VM_SETTING_TYPES
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -88,40 +89,14 @@ class Qemu(Module):
|
||||
size = settings.beginReadArray("VM")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
|
||||
name = settings.value("name", "")
|
||||
default_symbol = settings.value("default_symbol", ":/symbols/qemu_guest.normal.svg")
|
||||
hover_symbol = settings.value("hover_symbol", ":/symbols/qemu_guest.selected.svg")
|
||||
category = settings.value("category", Node.end_devices, type=int)
|
||||
qemu_path = settings.value("qemu_path", "")
|
||||
hda_disk_image = settings.value("hda_disk_image", "")
|
||||
hdb_disk_image = settings.value("hdb_disk_image", "")
|
||||
ram = settings.value("ram", 256, type=int)
|
||||
adapters = settings.value("adapters", 1, type=int)
|
||||
adapter_type = settings.value("adapter_type", "e1000")
|
||||
initrd = settings.value("initrd", "")
|
||||
kernel_image = settings.value("kernel_image", "")
|
||||
kernel_command_line = settings.value("kernel_command_line", "")
|
||||
options = settings.value("options", "")
|
||||
|
||||
server = settings.value("server", "local")
|
||||
|
||||
name = settings.value("name")
|
||||
server = settings.value("server")
|
||||
key = "{server}:{name}".format(server=server, name=name)
|
||||
self._qemu_vms[key] = {"name": name,
|
||||
"default_symbol": default_symbol,
|
||||
"hover_symbol": hover_symbol,
|
||||
"category": category,
|
||||
"qemu_path": qemu_path,
|
||||
"hda_disk_image": hda_disk_image,
|
||||
"hdb_disk_image": hdb_disk_image,
|
||||
"ram": ram,
|
||||
"adapters": adapters,
|
||||
"adapter_type": adapter_type,
|
||||
"options": options,
|
||||
"initrd": initrd,
|
||||
"kernel_image": kernel_image,
|
||||
"kernel_command_line": kernel_command_line,
|
||||
"server": server}
|
||||
if key in self._qemu_vms or not name or not server:
|
||||
continue
|
||||
self._qemu_vms[key] = {}
|
||||
for setting_name, default_value in QEMU_VM_SETTINGS.items():
|
||||
self._qemu_vms[key][setting_name] = settings.value(setting_name, default_value, QEMU_VM_SETTING_TYPES[setting_name])
|
||||
|
||||
settings.endArray()
|
||||
settings.endGroup()
|
||||
@@ -360,6 +335,7 @@ class Qemu(Module):
|
||||
"adapters": self._qemu_vms[vm]["adapters"],
|
||||
"adapter_type": self._qemu_vms[vm]["adapter_type"]}
|
||||
|
||||
#FIXME: this is ugly...
|
||||
if self._qemu_vms[vm]["hda_disk_image"]:
|
||||
settings["hda_disk_image"] = self._qemu_vms[vm]["hda_disk_image"]
|
||||
|
||||
@@ -375,11 +351,23 @@ class Qemu(Module):
|
||||
if self._qemu_vms[vm]["kernel_command_line"]:
|
||||
settings["kernel_command_line"] = self._qemu_vms[vm]["kernel_command_line"]
|
||||
|
||||
if self._qemu_vms[vm]["legacy_networking"]:
|
||||
settings["legacy_networking"] = self._qemu_vms[vm]["legacy_networking"]
|
||||
|
||||
settings["cpu_throttling"] = self._qemu_vms[vm]["cpu_throttling"]
|
||||
|
||||
if self._qemu_vms[vm]["process_priority"]:
|
||||
settings["process_priority"] = self._qemu_vms[vm]["process_priority"]
|
||||
|
||||
if self._qemu_vms[vm]["options"]:
|
||||
settings["options"] = self._qemu_vms[vm]["options"]
|
||||
|
||||
qemu_path = self._qemu_vms[vm]["qemu_path"]
|
||||
name = self._qemu_vms[vm]["name"]
|
||||
|
||||
if node.server().isCloud():
|
||||
settings["cloud_path"] = "images/qemu"
|
||||
|
||||
node.setup(qemu_path, initial_settings=settings, base_name=name)
|
||||
|
||||
def reset(self):
|
||||
|
||||
@@ -20,18 +20,18 @@ Wizard for QEMU VMs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.servers import Servers
|
||||
from gns3.node import Node
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.modules.module_error import ModuleError
|
||||
from gns3.utils.connect_to_server import ConnectToServer
|
||||
from gns3.settings import ENABLE_CLOUD
|
||||
|
||||
from ..ui.qemu_vm_wizard_ui import Ui_QemuVMWizard
|
||||
from .. import Qemu
|
||||
from ..ui.qemu_vm_wizard_ui import Ui_QemuVMWizard
|
||||
from ..pages.qemu_vm_configuration_page import QemuVMConfigurationPage
|
||||
from ..settings import QEMU_BINARIES_FOR_CLOUD
|
||||
|
||||
|
||||
class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
@@ -41,12 +41,15 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
:param parent: parent widget
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
def __init__(self, qemu_vms, parent):
|
||||
|
||||
QtGui.QWizard.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/qemu.svg"))
|
||||
self.setWizardStyle(QtGui.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtGui.QWizard.NoDefaultButton)
|
||||
|
||||
self.uiRemoteRadioButton.toggled.connect(self._remoteServerToggledSlot)
|
||||
self.uiHdaDiskImageToolButton.clicked.connect(self._hdaDiskImageBrowserSlot)
|
||||
@@ -56,7 +59,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
self.uiTypeComboBox.currentIndexChanged[str].connect(self._typeChangedSlot)
|
||||
|
||||
# Available types
|
||||
self.uiTypeComboBox.addItems(["Default", "ASA 8.4(2)", "IDS"])
|
||||
self.uiTypeComboBox.addItems(["Default", "IOSv", "IOSv-L2", "ASA 8.4(2)", "IDS"])
|
||||
|
||||
# Mandatory fields
|
||||
self.uiNameTypeWizardPage.registerField("vm_name*", self.uiNameLineEdit)
|
||||
@@ -65,6 +68,8 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
self.uiASAWizardPage.registerField("initrd*", self.uiInitrdLineEdit)
|
||||
self.uiASAWizardPage.registerField("kernel_image*", self.uiKernelImageLineEdit)
|
||||
|
||||
self._qemu_vms = qemu_vms
|
||||
|
||||
if Qemu.instance().settings()["use_local_server"]:
|
||||
# skip the server page if we use the local server
|
||||
self.setStartId(1)
|
||||
@@ -72,6 +77,9 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
# By default we use the local server
|
||||
self._server = Servers.instance().localServer()
|
||||
|
||||
if not ENABLE_CLOUD:
|
||||
self.uiCloudRadioButton.hide()
|
||||
|
||||
def _remoteServerToggledSlot(self, checked):
|
||||
"""
|
||||
Slot for when the remote server radio button is toggled.
|
||||
@@ -91,59 +99,32 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
:param vm_type: type of VM
|
||||
"""
|
||||
|
||||
if vm_type == "ASA 8.4(2)":
|
||||
if vm_type == "IOSv":
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/iosv_virl.normal.svg"))
|
||||
self.uiNameLineEdit.setText("vIOS")
|
||||
self.uiHdaDiskImageLabel.setText("IOSv VDMK file:")
|
||||
elif vm_type == "IOSv-L2":
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/iosv_l2_virl.normal.svg"))
|
||||
self.uiNameLineEdit.setText("vIOS-L2")
|
||||
self.uiHdaDiskImageLabel.setText("IOSv-L2 VDMK file:")
|
||||
elif vm_type == "ASA 8.4(2)":
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/asa.normal.svg"))
|
||||
self.uiNameLineEdit.setText("ASA")
|
||||
elif vm_type == "IDS":
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/symbols/ids.normal.svg"))
|
||||
self.uiNameLineEdit.setText("IDS")
|
||||
self.uiHdaDiskImageLabel.setText("Disk image (hda):")
|
||||
else:
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/qemu.svg"))
|
||||
|
||||
def _getDiskImage(self):
|
||||
|
||||
destination_directory = os.path.join(MainWindow.instance().settings()["images_path"], "QEMU")
|
||||
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
|
||||
"Select a QEMU disk image",
|
||||
destination_directory)
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
QtGui.QMessageBox.critical(self, "QEMU disk image", "Cannot read {}".format(path))
|
||||
return
|
||||
|
||||
try:
|
||||
os.makedirs(destination_directory)
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self, "QEMU disk images directory", "Could not create the QEMU disk images directory {}: {}".format(destination_directory,
|
||||
str(e)))
|
||||
return
|
||||
|
||||
if os.path.dirname(path) != destination_directory:
|
||||
# the QEMU disk image is not in the default images directory
|
||||
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
# try to create a symbolic link to it
|
||||
symlink_path = new_destination_path
|
||||
os.symlink(path, symlink_path)
|
||||
path = symlink_path
|
||||
except (OSError, NotImplementedError):
|
||||
# if unsuccessful, then copy the QEMU disk image itself
|
||||
try:
|
||||
shutil.copyfile(path, new_destination_path)
|
||||
path = new_destination_path
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return path
|
||||
self.uiHdaDiskImageLabel.setText("Disk image (hda):")
|
||||
self.uiNameLineEdit.setText("")
|
||||
|
||||
def _hdaDiskImageBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a QEMU hda disk image.
|
||||
"""
|
||||
|
||||
path = self._getDiskImage()
|
||||
path = QemuVMConfigurationPage.getDiskImage(self)
|
||||
if path:
|
||||
self.uiHdaDiskImageLineEdit.clear()
|
||||
self.uiHdaDiskImageLineEdit.setText(path)
|
||||
@@ -153,7 +134,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
Slot to open a file browser and select a QEMU hdb disk image.
|
||||
"""
|
||||
|
||||
path = self._getDiskImage()
|
||||
path = QemuVMConfigurationPage.getDiskImage(self)
|
||||
if path:
|
||||
self.uiHdbDiskImageLineEdit.clear()
|
||||
self.uiHdbDiskImageLineEdit.setText(path)
|
||||
@@ -163,7 +144,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
Slot to open a file browser and select a QEMU initrd.
|
||||
"""
|
||||
|
||||
path = self._getDiskImage()
|
||||
path = QemuVMConfigurationPage.getDiskImage(self)
|
||||
if path:
|
||||
self.uiInitrdLineEdit.clear()
|
||||
self.uiInitrdLineEdit.setText(path)
|
||||
@@ -173,7 +154,8 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
Slot to open a file browser and select a QEMU kernel image.
|
||||
"""
|
||||
|
||||
path = self._getDiskImage()
|
||||
from ..pages.qemu_vm_configuration_page import QemuVMConfigurationPage
|
||||
path = QemuVMConfigurationPage.getDiskImage(self)
|
||||
if path:
|
||||
self.uiKernelImageLineEdit.clear()
|
||||
self.uiKernelImageLineEdit.setText(path)
|
||||
@@ -184,19 +166,24 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
"""
|
||||
|
||||
if self.currentPage() == self.uiServerWizardPage:
|
||||
if not self.uiCloudRadioButton.isChecked():
|
||||
if Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
server = Servers.instance().localServer()
|
||||
elif self.uiRemoteRadioButton.isChecked():
|
||||
if not Servers.instance().remoteServers():
|
||||
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in QEMU preferences")
|
||||
return False
|
||||
server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
if not server.connected() and ConnectToServer(self, server) is False:
|
||||
return False
|
||||
self._server = server
|
||||
|
||||
#FIXME: prevent users to use "cloud"
|
||||
if self.uiCloudRadioButton.isChecked():
|
||||
QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!")
|
||||
return False
|
||||
|
||||
if Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
server = Servers.instance().localServer()
|
||||
else:
|
||||
server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
if not server.connected() and ConnectToServer(self, server) is False:
|
||||
return False
|
||||
self._server = server
|
||||
if self.currentPage() == self.uiNameTypeWizardPage:
|
||||
name = self.uiNameLineEdit.text()
|
||||
for qemu_vm in self._qemu_vms.values():
|
||||
if qemu_vm["name"] == name:
|
||||
QtGui.QMessageBox.critical(self, "Name", "{} is already used, please choose another name".format(name))
|
||||
return False
|
||||
|
||||
if self.currentPage() == self.uiBinaryMemoryWizardPage:
|
||||
if not self.uiQemuListComboBox.count():
|
||||
@@ -212,15 +199,23 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
for server in Servers.instance().remoteServers().values():
|
||||
self.uiRemoteServersComboBox.addItem("{}:{}".format(server.host, server.port), server)
|
||||
if self.page(page_id) == self.uiBinaryMemoryWizardPage:
|
||||
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
|
||||
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
|
||||
self._qemu_binaries_progress_dialog.show()
|
||||
try:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._server, self._getQemuBinariesFromServerCallback)
|
||||
except ModuleError as e:
|
||||
self._qemu_binaries_progress_dialog.reject()
|
||||
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
|
||||
if self.uiCloudRadioButton.isChecked():
|
||||
for binary in QEMU_BINARIES_FOR_CLOUD:
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=binary), binary)
|
||||
# Default to x86_64 for the user
|
||||
index = self.uiQemuListComboBox.findData("x86_64", flags=QtCore.Qt.MatchEndsWith)
|
||||
if index != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(index)
|
||||
else:
|
||||
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
|
||||
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
|
||||
self._qemu_binaries_progress_dialog.show()
|
||||
try:
|
||||
Qemu.instance().getQemuBinariesFromServer(self._server, self._getQemuBinariesFromServerCallback)
|
||||
except ModuleError as e:
|
||||
self._qemu_binaries_progress_dialog.reject()
|
||||
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
|
||||
|
||||
def _getQemuBinariesFromServerCallback(self, result, error=False):
|
||||
"""
|
||||
@@ -235,7 +230,7 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
self._qemu_binaries_progress_dialog.accept()
|
||||
|
||||
if error:
|
||||
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error: ".format(result["message"]))
|
||||
QtGui.QMessageBox.critical(self, "Qemu binaries", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiQemuListComboBox.clear()
|
||||
for qemu in result["qemus"]:
|
||||
@@ -244,13 +239,26 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
else:
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=qemu["path"]), qemu["path"])
|
||||
|
||||
#FIXME: use findData instead
|
||||
is_64bit = sys.maxsize > 2**32
|
||||
if sys.platform.startswith("win"):
|
||||
# default is qemu-system-x86_64w on Windows
|
||||
index = self.uiQemuListComboBox.findText("x86_64w", QtCore.Qt.MatchContains)
|
||||
if self.uiTypeComboBox.currentText() != "Default" and (Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked()):
|
||||
search_string = "qemu.exe"
|
||||
elif is_64bit:
|
||||
# default is qemu-system-x86_64w.exe on Windows 64-bit with a remote server
|
||||
search_string = "x86_64w.exe"
|
||||
else:
|
||||
# default is qemu-system-i386w.exe on Windows 32-bit with a remote server
|
||||
search_string = "i386w.exe"
|
||||
elif sys.platform.startswith("darwin") and hasattr(sys, "frozen") and (Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked()):
|
||||
search_string = "GNS3.app/Contents/Resources/qemu/bin/qemu-system-x86_64"
|
||||
elif is_64bit:
|
||||
# default is qemu-system-x86_64 on other 64-bit platforms
|
||||
search_string = "x86_64"
|
||||
else:
|
||||
# default is qemu-system-x86_64 on other platforms
|
||||
index = self.uiQemuListComboBox.findText("x86_64 ", QtCore.Qt.MatchContains) # the space after x86_64 must be present!
|
||||
# default is qemu-system-i386 on other platforms
|
||||
search_string = "i386"
|
||||
|
||||
index = self.uiQemuListComboBox.findData(search_string, flags=QtCore.Qt.MatchEndsWith)
|
||||
if index != -1:
|
||||
self.uiQemuListComboBox.setCurrentIndex(index)
|
||||
|
||||
@@ -263,8 +271,11 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
|
||||
if Qemu.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
server = "local"
|
||||
else:
|
||||
elif self.uiRemoteRadioButton.isChecked():
|
||||
server = self.uiRemoteServersComboBox.currentText()
|
||||
else: # Cloud is selected
|
||||
server = "cloud"
|
||||
|
||||
|
||||
qemu_path = self.uiQemuListComboBox.itemData(self.uiQemuListComboBox.currentIndex())
|
||||
settings = {
|
||||
@@ -274,12 +285,27 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
"server": server,
|
||||
}
|
||||
|
||||
if self.uiTypeComboBox.currentText() == "ASA 8.4(2)":
|
||||
settings["adapters"] = 6
|
||||
if self.uiTypeComboBox.currentText() == "IOSv":
|
||||
settings["adapters"] = 8
|
||||
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
|
||||
settings["default_symbol"] = ":/symbols/iosv_virl.normal.svg"
|
||||
settings["hover_symbol"] = ":/symbols/iosv_virl.selected.svg"
|
||||
settings["category"] = Node.routers
|
||||
elif self.uiTypeComboBox.currentText() == "IOSv-L2":
|
||||
settings["adapters"] = 8
|
||||
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
|
||||
settings["default_symbol"] = ":/symbols/iosv_l2_virl.normal.svg"
|
||||
settings["hover_symbol"] = ":/symbols/iosv_l2_virl.selected.svg"
|
||||
settings["category"] = Node.switches
|
||||
elif self.uiTypeComboBox.currentText() == "ASA 8.4(2)":
|
||||
settings["adapters"] = 4
|
||||
settings["initrd"] = self.uiInitrdLineEdit.text()
|
||||
settings["kernel_image"] = self.uiKernelImageLineEdit.text()
|
||||
settings["kernel_command_line"] = "ide_generic.probe_mask=0x01 ide_core.chs=0.0:980,16,32 auto nousb console=ttyS0,9600 bigphysarea=65536 ide1=noprobe no-hlt"
|
||||
settings["options"] = "-nographic -cpu coreduo -icount auto -hdachs 980,16,32"
|
||||
settings["options"] = "-icount auto -hdachs 980,16,32"
|
||||
if not sys.platform.startswith("darwin"):
|
||||
settings["cpu_throttling"] = 80 # limit to 80% CPU usage
|
||||
settings["process_priority"] = "low"
|
||||
settings["default_symbol"] = ":/symbols/asa.normal.svg"
|
||||
settings["hover_symbol"] = ":/symbols/asa.selected.svg"
|
||||
settings["category"] = Node.security_devices
|
||||
@@ -295,6 +321,16 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
|
||||
settings["category"] = Node.end_devices
|
||||
|
||||
if self.uiTypeComboBox.currentText() != "Default":
|
||||
if not "options" in settings:
|
||||
settings["options"] = ""
|
||||
if server == "local" and (sys.platform.startswith("win") and qemu_path.endswith("qemu.exe")) or (sys.platform.startswith("darwin") and "GNS3.app" in qemu_path):
|
||||
settings["options"] += " -vga none -vnc none"
|
||||
settings["legacy_networking"] = True
|
||||
else:
|
||||
settings["options"] += " -nographic"
|
||||
settings["options"] = settings["options"].strip()
|
||||
|
||||
return settings
|
||||
|
||||
def nextId(self):
|
||||
@@ -305,7 +341,9 @@ class QemuVMWizard(QtGui.QWizard, Ui_QemuVMWizard):
|
||||
current_id = self.currentId()
|
||||
if self.page(current_id) == self.uiNameTypeWizardPage:
|
||||
|
||||
if self.uiTypeComboBox.currentText() != "Default":
|
||||
if self.uiTypeComboBox.currentText().startswith("IOSv"):
|
||||
self.uiRamSpinBox.setValue(384)
|
||||
elif self.uiTypeComboBox.currentText() != "Default":
|
||||
self.uiRamSpinBox.setValue(1024)
|
||||
|
||||
elif self.page(current_id) == self.uiBinaryMemoryWizardPage:
|
||||
|
||||
@@ -68,6 +68,8 @@ class QemuPreferencesPage(QtGui.QWidget, Ui_QemuPreferencesPageWidget):
|
||||
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
|
||||
self.uiConsoleStartPortSpinBox.setValue(settings["console_start_port_range"])
|
||||
self.uiConsoleEndPortSpinBox.setValue(settings["console_end_port_range"])
|
||||
self.uiMonitorStartPortSpinBox.setValue(settings["monitor_start_port_range"])
|
||||
self.uiMonitorEndPortSpinBox.setValue(settings["monitor_end_port_range"])
|
||||
self.uiUDPStartPortSpinBox.setValue(settings["udp_start_port_range"])
|
||||
self.uiUDPEndPortSpinBox.setValue(settings["udp_end_port_range"])
|
||||
|
||||
@@ -108,6 +110,8 @@ class QemuPreferencesPage(QtGui.QWidget, Ui_QemuPreferencesPageWidget):
|
||||
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
|
||||
new_settings["console_start_port_range"] = self.uiConsoleStartPortSpinBox.value()
|
||||
new_settings["console_end_port_range"] = self.uiConsoleEndPortSpinBox.value()
|
||||
new_settings["monitor_start_port_range"] = self.uiMonitorStartPortSpinBox.value()
|
||||
new_settings["monitor_end_port_range"] = self.uiMonitorEndPortSpinBox.value()
|
||||
new_settings["udp_start_port_range"] = self.uiUDPStartPortSpinBox.value()
|
||||
new_settings["udp_end_port_range"] = self.uiUDPEndPortSpinBox.value()
|
||||
Qemu.instance().setSettings(new_settings)
|
||||
|
||||
@@ -20,17 +20,20 @@ Configuration page for QEMU VMs.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from functools import partial
|
||||
from collections import OrderedDict
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.servers import Servers
|
||||
from gns3.modules.module_error import ModuleError
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.node_configurator_dialog import ConfigurationError
|
||||
from gns3.utils.progress_dialog import ProgressDialog
|
||||
from gns3.utils.file_copy_thread import FileCopyThread
|
||||
|
||||
from ..ui.qemu_vm_configuration_page_ui import Ui_QemuVMConfigPageWidget
|
||||
from .. import Qemu
|
||||
from ..settings import QEMU_BINARIES_FOR_CLOUD
|
||||
|
||||
|
||||
class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
@@ -47,28 +50,45 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiHdbDiskImageToolButton.clicked.connect(self._hdbDiskImageBrowserSlot)
|
||||
self.uiInitrdToolButton.clicked.connect(self._initrdBrowserSlot)
|
||||
self.uiKernelImageToolButton.clicked.connect(self._kernelImageBrowserSlot)
|
||||
self.uiActivateCPUThrottlingCheckBox.stateChanged.connect(self._cpuThrottlingChangedSlot)
|
||||
|
||||
qemu_network_devices = OrderedDict([
|
||||
("e1000", "Intel Gigabit Ethernet"),
|
||||
("i82550", "Intel i82550 Ethernet"),
|
||||
("i82551", "Intel i82551 Ethernet"),
|
||||
("i82557a", "Intel i82557A Ethernet"),
|
||||
("i82557b", "Intel i82557B Ethernet"),
|
||||
("i82557c", "Intel i82557C Ethernet"),
|
||||
("i82558a", "Intel i82558A Ethernet"),
|
||||
("i82558b", "Intel i82558B Ethernet"),
|
||||
("i82559a", "Intel i82559A Ethernet"),
|
||||
("i82559b", "Intel i82559B Ethernet"),
|
||||
("i82559c", "Intel i82559C Ethernet"),
|
||||
("i82559er", "Intel i82559ER Ethernet"),
|
||||
("i82562", "Intel i82562 Ethernet"),
|
||||
("i82801", "Intel i82801 Ethernet"),
|
||||
("ne2k_pci", "NE2000 Ethernet"),
|
||||
("pcnet", "AMD PCNet Ethernet"),
|
||||
("rtl8139", "Realtek 8139 Ethernet"),
|
||||
("virtio-net-pci", "Paravirtualized Network I/O"),
|
||||
("vmxnet3", "VMWare Paravirtualized Ethernet v3")])
|
||||
|
||||
self.uiAdapterTypesComboBox.clear()
|
||||
self.uiAdapterTypesComboBox.addItems(["ne2k_pci",
|
||||
"i82551",
|
||||
"i82557b",
|
||||
"i82559er",
|
||||
"rtl8139",
|
||||
"e1000",
|
||||
"pcnet",
|
||||
"virtio"])
|
||||
for device_name, device_description in qemu_network_devices.items():
|
||||
self.uiAdapterTypesComboBox.addItem("{} ({})".format(device_description, device_name), device_name)
|
||||
|
||||
def _getDiskImage(self):
|
||||
@staticmethod
|
||||
def getDiskImage(parent):
|
||||
|
||||
destination_directory = os.path.join(MainWindow.instance().settings()["images_path"], "QEMU")
|
||||
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(self,
|
||||
path, _ = QtGui.QFileDialog.getOpenFileNameAndFilter(parent,
|
||||
"Select a QEMU disk image",
|
||||
destination_directory)
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
QtGui.QMessageBox.critical(self, "QEMU disk image", "Cannot read {}".format(path))
|
||||
QtGui.QMessageBox.critical(parent, "QEMU disk image", "Cannot read {}".format(path))
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -76,25 +96,28 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
except FileExistsError:
|
||||
pass
|
||||
except OSError as e:
|
||||
QtGui.QMessageBox.critical(self, "QEMU disk images directory", "Could not create the QEMU disk images directory {}: {}".format(destination_directory,
|
||||
str(e)))
|
||||
QtGui.QMessageBox.critical(parent, "QEMU disk images directory", "Could not create the QEMU disk images directory {}: {}".format(destination_directory, e))
|
||||
return
|
||||
|
||||
if os.path.dirname(path) != destination_directory:
|
||||
if os.path.normpath(os.path.dirname(path)) != destination_directory:
|
||||
# the QEMU disk image is not in the default images directory
|
||||
new_destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
try:
|
||||
# try to create a symbolic link to it
|
||||
symlink_path = new_destination_path
|
||||
os.symlink(path, symlink_path)
|
||||
path = symlink_path
|
||||
except (OSError, NotImplementedError):
|
||||
# if unsuccessful, then copy the QEMU disk image itself
|
||||
try:
|
||||
shutil.copyfile(path, new_destination_path)
|
||||
path = new_destination_path
|
||||
except OSError:
|
||||
pass
|
||||
reply = QtGui.QMessageBox.question(parent,
|
||||
"QEMU disk image",
|
||||
"Would you like to copy {} to the default images directory".format(os.path.basename(path)),
|
||||
QtGui.QMessageBox.Yes,
|
||||
QtGui.QMessageBox.No)
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
destination_path = os.path.join(destination_directory, os.path.basename(path))
|
||||
thread = FileCopyThread(path, destination_path)
|
||||
progress_dialog = ProgressDialog(thread, "QEMU disk image", "Copying {}".format(os.path.basename(path)), "Cancel", busy=True, parent=parent)
|
||||
thread.deleteLater()
|
||||
progress_dialog.show()
|
||||
progress_dialog.exec_()
|
||||
errors = progress_dialog.errors()
|
||||
if errors:
|
||||
QtGui.QMessageBox.critical(parent, "QEMU disk image", "{}".format("".join(errors)))
|
||||
else:
|
||||
path = destination_path
|
||||
|
||||
return path
|
||||
|
||||
@@ -103,7 +126,7 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
Slot to open a file browser and select a QEMU hda disk image.
|
||||
"""
|
||||
|
||||
path = self._getDiskImage()
|
||||
path = self.getDiskImage(self)
|
||||
if path:
|
||||
self.uiHdaDiskImageLineEdit.clear()
|
||||
self.uiHdaDiskImageLineEdit.setText(path)
|
||||
@@ -113,7 +136,7 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
Slot to open a file browser and select a QEMU hdb disk image.
|
||||
"""
|
||||
|
||||
path = self._getDiskImage()
|
||||
path = self.getDiskImage(self)
|
||||
if path:
|
||||
self.uiHdbDiskImageLineEdit.clear()
|
||||
self.uiHdbDiskImageLineEdit.setText(path)
|
||||
@@ -167,6 +190,16 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
QtGui.QMessageBox.critical(self, "Qemu", "Could not find {} in the Qemu binaries list".format(qemu_path))
|
||||
self.uiQemuListComboBox.clear()
|
||||
|
||||
def _cpuThrottlingChangedSlot(self, state):
|
||||
"""
|
||||
Slot to enable or not CPU throttling.
|
||||
"""
|
||||
|
||||
if state:
|
||||
self.uiCPUThrottlingSpinBox.setEnabled(True)
|
||||
else:
|
||||
self.uiCPUThrottlingSpinBox.setEnabled(False)
|
||||
|
||||
def loadSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
Loads the QEMU VM settings.
|
||||
@@ -182,19 +215,26 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
server = settings["server"]
|
||||
if server == "local":
|
||||
server = Servers.instance().localServer()
|
||||
elif ":" in server:
|
||||
host, port = server.rsplit(":")
|
||||
server = Servers.instance().getRemoteServer(host, port)
|
||||
|
||||
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
|
||||
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
|
||||
self._qemu_binaries_progress_dialog.show()
|
||||
if server == "cloud":
|
||||
for binary in QEMU_BINARIES_FOR_CLOUD:
|
||||
self.uiQemuListComboBox.addItem("{path}".format(path=binary), binary)
|
||||
else:
|
||||
self._qemu_binaries_progress_dialog = QtGui.QProgressDialog("Loading QEMU binaries", "Cancel", 0, 0, parent=self)
|
||||
self._qemu_binaries_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self._qemu_binaries_progress_dialog.setWindowTitle("QEMU binaries")
|
||||
self._qemu_binaries_progress_dialog.show()
|
||||
|
||||
callback = partial(self._getQemuBinariesFromServerCallback, qemu_path=settings["qemu_path"])
|
||||
try:
|
||||
Qemu.instance().getQemuBinariesFromServer(server, callback)
|
||||
except ModuleError as e:
|
||||
self._qemu_binaries_progress_dialog.reject()
|
||||
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
|
||||
self.uiQemuListComboBox.clear()
|
||||
callback = partial(self._getQemuBinariesFromServerCallback, qemu_path=settings["qemu_path"])
|
||||
try:
|
||||
Qemu.instance().getQemuBinariesFromServer(server, callback)
|
||||
except ModuleError as e:
|
||||
self._qemu_binaries_progress_dialog.reject()
|
||||
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
|
||||
self.uiQemuListComboBox.clear()
|
||||
|
||||
if not group:
|
||||
# set the device name
|
||||
@@ -204,6 +244,11 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
else:
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
if "monitor" in settings:
|
||||
self.uiMonitorPortSpinBox.setValue(settings["monitor"])
|
||||
else:
|
||||
self.uiMonitorPortLabel.hide()
|
||||
self.uiMonitorPortSpinBox.hide()
|
||||
self.uiHdaDiskImageLineEdit.setText(settings["hda_disk_image"])
|
||||
self.uiHdbDiskImageLineEdit.setText(settings["hdb_disk_image"])
|
||||
self.uiInitrdLineEdit.setText(settings["initrd"])
|
||||
@@ -213,6 +258,8 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
self.uiNameLineEdit.hide()
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
self.uiMonitorPortLabel.hide()
|
||||
self.uiMonitorPortSpinBox.hide()
|
||||
self.uiHdaDiskImageLabel.hide()
|
||||
self.uiHdaDiskImageLineEdit.hide()
|
||||
self.uiHdaDiskImageToolButton.hide()
|
||||
@@ -228,11 +275,21 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
|
||||
self.uiKernelCommandLineEdit.setText(settings["kernel_command_line"])
|
||||
self.uiAdaptersSpinBox.setValue(settings["adapters"])
|
||||
index = self.uiAdapterTypesComboBox.findText(settings["adapter_type"])
|
||||
index = self.uiAdapterTypesComboBox.findData(settings["adapter_type"])
|
||||
if index != -1:
|
||||
self.uiAdapterTypesComboBox.setCurrentIndex(index)
|
||||
|
||||
self.uiLegacyNetworkingCheckBox.setChecked(settings["legacy_networking"])
|
||||
self.uiRamSpinBox.setValue(settings["ram"])
|
||||
|
||||
if settings["cpu_throttling"]:
|
||||
self.uiActivateCPUThrottlingCheckBox.setChecked(True)
|
||||
self.uiCPUThrottlingSpinBox.setValue(settings["cpu_throttling"])
|
||||
else:
|
||||
self.uiActivateCPUThrottlingCheckBox.setChecked(False)
|
||||
|
||||
index = self.uiProcessPriorityComboBox.findText(settings["process_priority"], QtCore.Qt.MatchFixedString)
|
||||
if index != -1:
|
||||
self.uiProcessPriorityComboBox.setCurrentIndex(index)
|
||||
self.uiQemuOptionsLineEdit.setText(settings["options"])
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
@@ -256,6 +313,8 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
|
||||
if "console" in settings:
|
||||
settings["console"] = self.uiConsolePortSpinBox.value()
|
||||
if "monitor" in settings:
|
||||
settings["monitor"] = self.uiMonitorPortSpinBox.value()
|
||||
settings["hda_disk_image"] = self.uiHdaDiskImageLineEdit.text()
|
||||
settings["hdb_disk_image"] = self.uiHdbDiskImageLineEdit.text()
|
||||
settings["initrd"] = self.uiInitrdLineEdit.text()
|
||||
@@ -265,6 +324,8 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
del settings["name"]
|
||||
if "console" in settings:
|
||||
del settings["console"]
|
||||
if "monitor" in settings:
|
||||
del settings["monitor"]
|
||||
del settings["hda_disk_image"]
|
||||
del settings["hdb_disk_image"]
|
||||
del settings["initrd"]
|
||||
@@ -274,7 +335,7 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
qemu_path = self.uiQemuListComboBox.itemData(self.uiQemuListComboBox.currentIndex())
|
||||
settings["qemu_path"] = qemu_path
|
||||
|
||||
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()
|
||||
settings["adapter_type"] = self.uiAdapterTypesComboBox.itemData(self.uiAdapterTypesComboBox.currentIndex())
|
||||
settings["kernel_command_line"] = self.uiKernelCommandLineEdit.text()
|
||||
|
||||
adapters = self.uiAdaptersSpinBox.value()
|
||||
@@ -287,5 +348,11 @@ class QemuVMConfigurationPage(QtGui.QWidget, Ui_QemuVMConfigPageWidget):
|
||||
raise ConfigurationError()
|
||||
|
||||
settings["adapters"] = adapters
|
||||
settings["legacy_networking"] = self.uiLegacyNetworkingCheckBox.isChecked()
|
||||
settings["ram"] = self.uiRamSpinBox.value()
|
||||
if self.uiActivateCPUThrottlingCheckBox.isChecked():
|
||||
settings["cpu_throttling"] = self.uiCPUThrottlingSpinBox.value()
|
||||
else:
|
||||
settings["cpu_throttling"] = 0
|
||||
settings["process_priority"] = self.uiProcessPriorityComboBox.currentText().lower()
|
||||
settings["options"] = self.uiQemuOptionsLineEdit.text()
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
Configuration page for QEMU VM preferences.
|
||||
"""
|
||||
|
||||
import ntpath
|
||||
import os
|
||||
import copy
|
||||
|
||||
@@ -27,13 +28,16 @@ from gns3.node import Node
|
||||
from gns3.main_window import MainWindow
|
||||
from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
from gns3.cloud.utils import UploadFilesThread
|
||||
|
||||
from .. import Qemu
|
||||
from ..settings import QEMU_VM_SETTINGS
|
||||
from ..ui.qemu_vm_preferences_page_ui import Ui_QemuVMPreferencesPageWidget
|
||||
from ..pages.qemu_vm_configuration_page import QemuVMConfigurationPage
|
||||
from ..dialogs.qemu_vm_wizard import QemuVMWizard
|
||||
|
||||
|
||||
|
||||
class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
"""
|
||||
QWidget preference page for QEMU VM preferences.
|
||||
@@ -97,6 +101,14 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
if qemu_vm["kernel_command_line"]:
|
||||
QtGui.QTreeWidgetItem(section_item, ["Kernel command line:", qemu_vm["kernel_command_line"]])
|
||||
|
||||
# performance section
|
||||
section_item = self._createSectionItem("Optimizations")
|
||||
if qemu_vm["cpu_throttling"]:
|
||||
QtGui.QTreeWidgetItem(section_item, ["CPU throttling:", "{}%".format(qemu_vm["cpu_throttling"])])
|
||||
else:
|
||||
QtGui.QTreeWidgetItem(section_item, ["CPU throttling:", "disabled"])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Process priority:", qemu_vm["process_priority"]])
|
||||
|
||||
# fill out the Additional options section
|
||||
if qemu_vm["options"]:
|
||||
section_item = self._createSectionItem("Additional options")
|
||||
@@ -129,34 +141,18 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
Creates a new VM.
|
||||
"""
|
||||
|
||||
wizard = QemuVMWizard(parent=self)
|
||||
wizard = QemuVMWizard(self._qemu_vms, parent=self)
|
||||
wizard.show()
|
||||
if wizard.exec_():
|
||||
|
||||
new_vm_settings = wizard.getSettings()
|
||||
|
||||
key = "{server}:{name}".format(server=new_vm_settings["server"], name=new_vm_settings["name"])
|
||||
|
||||
if key in self._qemu_vms:
|
||||
QtGui.QMessageBox.critical(self, "New QEMU VM", "VM name {} already exists".format(new_vm_settings["name"]))
|
||||
return
|
||||
|
||||
self._qemu_vms[key] = {"name": "",
|
||||
"default_symbol": ":/symbols/qemu_guest.normal.svg",
|
||||
"hover_symbol": ":/symbols/qemu_guest.selected.svg",
|
||||
"category": Node.end_devices,
|
||||
"qemu_path": "default",
|
||||
"hda_disk_image": "",
|
||||
"hdb_disk_image": "",
|
||||
"ram": 256,
|
||||
"adapters": 1,
|
||||
"adapter_type": "e1000",
|
||||
"options": "",
|
||||
"initrd": "",
|
||||
"kernel_image": "",
|
||||
"kernel_command_line": "",
|
||||
"server": "local"}
|
||||
|
||||
self._qemu_vms[key] = QEMU_VM_SETTINGS.copy()
|
||||
self._qemu_vms[key].update(new_vm_settings)
|
||||
|
||||
item = QtGui.QTreeWidgetItem(self.uiQemuVMsTreeWidget)
|
||||
item.setText(0, self._qemu_vms[key]["name"])
|
||||
item.setIcon(0, QtGui.QIcon(self._qemu_vms[key]["default_symbol"]))
|
||||
@@ -164,6 +160,62 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
self._items.append(item)
|
||||
self.uiQemuVMsTreeWidget.setCurrentItem(item)
|
||||
|
||||
if self._qemu_vms[key]["server"] == 'cloud':
|
||||
self._qemu_vms[key]["options"] = "-nographic"
|
||||
self._uploadImages(new_vm_settings)
|
||||
|
||||
def _imageUploadComplete(self):
|
||||
if self._upload_image_progress_dialog.wasCanceled():
|
||||
return
|
||||
self._upload_image_progress_dialog.accept()
|
||||
|
||||
def _uploadImages(self, qemu_vm):
|
||||
"""
|
||||
Upload hard drive images to Cloud Files.
|
||||
"""
|
||||
|
||||
# Start uploading the image to cloud files
|
||||
self._upload_image_progress_dialog = QtGui.QProgressDialog(
|
||||
"Uploading image file(s)", "Cancel", 0, 0, parent=self)
|
||||
self._upload_image_progress_dialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self._upload_image_progress_dialog.setWindowTitle("Qemu image upload")
|
||||
self._upload_image_progress_dialog.show()
|
||||
|
||||
try:
|
||||
uploads = []
|
||||
src = qemu_vm.get("hda_disk_image", None)
|
||||
if src:
|
||||
_, filename = ntpath.split(src)
|
||||
dst = "images/qemu/{}".format(filename)
|
||||
uploads.append((src, dst))
|
||||
|
||||
src = qemu_vm.get("hdb_disk_image", None)
|
||||
if src:
|
||||
_, filename = ntpath.split(src)
|
||||
dst = "images/qemu/{}".format(filename)
|
||||
uploads.append((src, dst))
|
||||
src = qemu_vm.get("initrd", None)
|
||||
if src:
|
||||
_, filename = ntpath.split(src)
|
||||
dst = "images/qemu/{}".format(filename)
|
||||
uploads.append((src, dst))
|
||||
|
||||
src = qemu_vm.get("kernel_image", None)
|
||||
if src:
|
||||
_, filename = ntpath.split(src)
|
||||
dst = "images/qemu/{}".format(filename)
|
||||
uploads.append((src, dst))
|
||||
|
||||
upload_thread = UploadFilesThread(self, MainWindow.instance().cloudSettings(), uploads)
|
||||
upload_thread.completed.connect(self._imageUploadComplete)
|
||||
upload_thread.start()
|
||||
except Exception as e:
|
||||
self._upload_image_progress_dialog.reject()
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
log.error(e)
|
||||
QtGui.QMessageBox.critical(self, "Qemu image upload", "Error uploading Qemu image: {}".format(e))
|
||||
|
||||
def _qemuVMEditSlot(self):
|
||||
"""
|
||||
Edits a QEMU VM.
|
||||
@@ -177,11 +229,20 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
if qemu_vm["name"] != item.text(0):
|
||||
if "{}:{}".format(qemu_vm["server"], qemu_vm["name"]) in self._qemu_vms:
|
||||
# FIXME: bug when changing name
|
||||
QtGui.QMessageBox.critical(self, "New QEMU VM", "VM name {} already exists".format(qemu_vm["name"]))
|
||||
new_key = "{server}:{name}".format(server=qemu_vm["server"], name=qemu_vm["name"])
|
||||
if new_key in self._qemu_vms:
|
||||
QtGui.QMessageBox.critical(self, "QEMU VM", "QEMU VM name {} already exists for server {}".format(qemu_vm["name"],
|
||||
qemu_vm["server"]))
|
||||
qemu_vm["name"] = item.text(0)
|
||||
return
|
||||
self._qemu_vms[new_key] = self._qemu_vms[key]
|
||||
del self._qemu_vms[key]
|
||||
item.setText(0, qemu_vm["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
|
||||
if qemu_vm["server"] == 'cloud':
|
||||
self._uploadImages(qemu_vm)
|
||||
|
||||
self._refreshInfo(qemu_vm)
|
||||
|
||||
def _qemuVMDeleteSlot(self):
|
||||
@@ -220,16 +281,16 @@ class QemuVMPreferencesPage(QtGui.QWidget, Ui_QemuVMPreferencesPageWidget):
|
||||
Change a symbol for a QEMU VM.
|
||||
"""
|
||||
|
||||
dialog = SymbolSelectionDialog(self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item = self.uiQemuVMsTreeWidget.currentItem()
|
||||
if item:
|
||||
item = self.uiQemuVMsTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
qemu_vm = self._qemu_vms[key]
|
||||
dialog = SymbolSelectionDialog(self, symbol=qemu_vm["default_symbol"], category=qemu_vm["category"])
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item.setIcon(0, QtGui.QIcon(normal_symbol))
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
qemu_vm = self._qemu_vms[key]
|
||||
qemu_vm["default_symbol"] = normal_symbol
|
||||
qemu_vm["hover_symbol"] = selected_symbol
|
||||
qemu_vm["category"] = category
|
||||
|
||||
@@ -22,6 +22,7 @@ QEMU VM implementation.
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
from .settings import QEMU_VM_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -46,19 +47,23 @@ class QemuVM(Node):
|
||||
self._loading = False
|
||||
self._module = module
|
||||
self._ports = []
|
||||
|
||||
self._settings = {"name": "",
|
||||
"qemu_path": "",
|
||||
"hda_disk_image": "",
|
||||
"hdb_disk_image": "",
|
||||
"options": "",
|
||||
"ram": 256,
|
||||
"ram": QEMU_VM_SETTINGS["ram"],
|
||||
"console": None,
|
||||
"adapters": 1,
|
||||
"adapter_type": "e1000",
|
||||
"monitor": None,
|
||||
"adapters": QEMU_VM_SETTINGS["adapters"],
|
||||
"adapter_type": QEMU_VM_SETTINGS["adapter_type"],
|
||||
"legacy_networking": QEMU_VM_SETTINGS["legacy_networking"],
|
||||
"cpu_throttling": QEMU_VM_SETTINGS["cpu_throttling"],
|
||||
"process_priority": QEMU_VM_SETTINGS["process_priority"],
|
||||
"initrd": "",
|
||||
"kernel_image": "",
|
||||
"kernel_command_line": "",
|
||||
}
|
||||
"kernel_command_line": ""}
|
||||
|
||||
self._addAdapters(1)
|
||||
|
||||
@@ -74,12 +79,14 @@ class QemuVM(Node):
|
||||
|
||||
for port_number in range(0, adapters):
|
||||
port_name = EthernetPort.longNameType() + str(port_number)
|
||||
short_name = EthernetPort.shortNameType() + str(port_number)
|
||||
new_port = EthernetPort(port_name)
|
||||
new_port.setShortName(short_name)
|
||||
new_port.setPortNumber(port_number)
|
||||
self._ports.append(new_port)
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
|
||||
def setup(self, qemu_path, name=None, console=None, qemu_id=None, initial_settings={}, base_name=None):
|
||||
def setup(self, qemu_path, name=None, console=None, monitor=None, qemu_id=None, initial_settings={}, base_name=None):
|
||||
"""
|
||||
Setups this QEMU VM.
|
||||
|
||||
@@ -88,7 +95,7 @@ class QemuVM(Node):
|
||||
|
||||
# let's create a unique name if none has been chosen
|
||||
if not name:
|
||||
name = self.allocateName(base_name)
|
||||
name = self.allocateName(base_name + "-")
|
||||
|
||||
if not name:
|
||||
self.error_signal.emit(self.id(), "could not allocate a name for this QEMU VM")
|
||||
@@ -100,6 +107,9 @@ class QemuVM(Node):
|
||||
if console:
|
||||
params["console"] = self._settings["console"] = console
|
||||
|
||||
if monitor:
|
||||
params["monitor"] = self._settings["monitor"] = monitor
|
||||
|
||||
if qemu_id:
|
||||
params["qemu_id"] = qemu_id
|
||||
|
||||
@@ -189,6 +199,9 @@ class QemuVM(Node):
|
||||
if name in self._settings and self._settings[name] != value:
|
||||
params[name] = value
|
||||
|
||||
if "cloud_path" in new_settings:
|
||||
params["cloud_path"] = self._settings["cloud_path"] = new_settings.pop("cloud_path")
|
||||
|
||||
log.debug("{} is updating settings: {}".format(self.name(), params))
|
||||
self._server.send_message("qemu.update", params, self._updateCallback)
|
||||
|
||||
@@ -296,36 +309,36 @@ class QemuVM(Node):
|
||||
port.setStatus(Port.stopped)
|
||||
self.stopped_signal.emit()
|
||||
|
||||
# def suspend(self):
|
||||
# """
|
||||
# Suspends this QEMU VM instance.
|
||||
# """
|
||||
#
|
||||
# if self.status() == Node.suspended:
|
||||
# log.debug("{} is already suspended".format(self.name()))
|
||||
# return
|
||||
#
|
||||
# log.debug("{} is being suspended".format(self.name()))
|
||||
# self._server.send_message("qemu.suspend", {"id": self._qemu_id}, self._suspendCallback)
|
||||
#
|
||||
# def _suspendCallback(self, result, error=False):
|
||||
# """
|
||||
# Callback for suspend.
|
||||
#
|
||||
# :param result: server response
|
||||
# :param error: indicates an error (boolean)
|
||||
# """
|
||||
#
|
||||
# if error:
|
||||
# log.error("error while suspending {}: {}".format(self.name(), result["message"]))
|
||||
# self.server_error_signal.emit(self.id(), result["code"], result["message"])
|
||||
# else:
|
||||
# log.info("{} has suspended".format(self.name()))
|
||||
# self.setStatus(Node.suspended)
|
||||
# for port in self._ports:
|
||||
# # set ports as suspended
|
||||
# port.setStatus(Port.suspended)
|
||||
# self.suspended_signal.emit()
|
||||
def suspend(self):
|
||||
"""
|
||||
Suspends this QEMU VM instance.
|
||||
"""
|
||||
|
||||
if self.status() == Node.suspended:
|
||||
log.debug("{} is already suspended".format(self.name()))
|
||||
return
|
||||
|
||||
log.debug("{} is being suspended".format(self.name()))
|
||||
self._server.send_message("qemu.suspend", {"id": self._qemu_id}, self._suspendCallback)
|
||||
|
||||
def _suspendCallback(self, result, error=False):
|
||||
"""
|
||||
Callback for suspend.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while suspending {}: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["code"], result["message"])
|
||||
else:
|
||||
log.info("{} has suspended".format(self.name()))
|
||||
self.setStatus(Node.suspended)
|
||||
for port in self._ports:
|
||||
# set ports as suspended
|
||||
port.setStatus(Port.suspended)
|
||||
self.suspended_signal.emit()
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
@@ -450,11 +463,13 @@ class QemuVM(Node):
|
||||
info = """QEMU VM {name} is {state}
|
||||
Node ID is {id}, server's QEMU VM ID is {qemu_id}
|
||||
console is on port {console}
|
||||
monitor is on port {monitor}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
qemu_id=self._qemu_id,
|
||||
state=state,
|
||||
console=self._settings["console"])
|
||||
console=self._settings["console"],
|
||||
monitor=self._settings["monitor"])
|
||||
|
||||
port_info = ""
|
||||
for port in self._ports:
|
||||
@@ -507,13 +522,14 @@ class QemuVM(Node):
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
qemu_path = settings.pop("qemu_path")
|
||||
console = settings.pop("console")
|
||||
console = settings.pop("console", self._defaults["console"])
|
||||
monitor = settings.pop("monitor", self._defaults["monitor"])
|
||||
self.updated_signal.connect(self._updatePortSettings)
|
||||
# block the created signal, it will be triggered when loading is completely done
|
||||
self._loading = True
|
||||
log.info("QEMU VM {} is loading".format(name))
|
||||
self.setName(name)
|
||||
self.setup(qemu_path, name, console, qemu_id, settings)
|
||||
self.setup(qemu_path, name, console, monitor, qemu_id, settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
@@ -574,6 +590,15 @@ class QemuVM(Node):
|
||||
|
||||
return self._settings["console"]
|
||||
|
||||
def monitor(self):
|
||||
"""
|
||||
Returns the monitor port for this QEMU VM instance.
|
||||
|
||||
:returns: port (integer)
|
||||
"""
|
||||
|
||||
return self._settings["monitor"]
|
||||
|
||||
def configPage(self):
|
||||
"""
|
||||
Returns the configuration page widget to be used by the node configurator.
|
||||
|
||||
@@ -19,9 +19,13 @@
|
||||
Default QEMU settings.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
QEMU_SETTINGS = {
|
||||
"console_start_port_range": 5001,
|
||||
"console_end_port_range": 5500,
|
||||
"monitor_start_port_range": 5501,
|
||||
"monitor_end_port_range": 6000,
|
||||
"udp_start_port_range": 40001,
|
||||
"udp_end_port_range": 45500,
|
||||
"use_local_server": True,
|
||||
@@ -30,7 +34,83 @@ QEMU_SETTINGS = {
|
||||
QEMU_SETTING_TYPES = {
|
||||
"console_start_port_range": int,
|
||||
"console_end_port_range": int,
|
||||
"monitor_start_port_range": int,
|
||||
"monitor_end_port_range": int,
|
||||
"udp_start_port_range": int,
|
||||
"udp_end_port_range": int,
|
||||
"use_local_server": bool,
|
||||
}
|
||||
|
||||
QEMU_VM_SETTINGS = {
|
||||
"name": "",
|
||||
"default_symbol": ":/symbols/qemu_guest.normal.svg",
|
||||
"hover_symbol": ":/symbols/qemu_guest.selected.svg",
|
||||
"category": Node.end_devices,
|
||||
"qemu_path": "",
|
||||
"hda_disk_image": "",
|
||||
"hdb_disk_image": "",
|
||||
"ram": 256,
|
||||
"adapters": 1,
|
||||
"adapter_type": "e1000",
|
||||
"legacy_networking": False,
|
||||
"cpu_throttling": 0,
|
||||
"process_priority": "normal",
|
||||
"options": "",
|
||||
"initrd": "",
|
||||
"kernel_image": "",
|
||||
"kernel_command_line": "",
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
QEMU_VM_SETTING_TYPES = {
|
||||
"name": str,
|
||||
"default_symbol": str,
|
||||
"hover_symbol": str,
|
||||
"category": int,
|
||||
"qemu_path": str,
|
||||
"hda_disk_image": str,
|
||||
"hdb_disk_image": str,
|
||||
"ram": int,
|
||||
"adapters": int,
|
||||
"adapter_type": str,
|
||||
"legacy_networking": bool,
|
||||
"cpu_throttling": int,
|
||||
"process_priority": str,
|
||||
"options": str,
|
||||
"initrd": str,
|
||||
"kernel_image": str,
|
||||
"kernel_command_line": str,
|
||||
"server": str
|
||||
}
|
||||
|
||||
# Use a hardcoded list of binaries rather than a dynamic one so the user
|
||||
# doesn't require a running cloud instance to upload qemu images.
|
||||
QEMU_BINARIES_FOR_CLOUD = [
|
||||
"qemu-system-arm",
|
||||
"qemu-system-microblaze",
|
||||
"qemu-system-mipsel",
|
||||
"qemu-system-ppcemb",
|
||||
"qemu-system-sparc64",
|
||||
"qemu-system-cris",
|
||||
"qemu-system-microblazeel",
|
||||
"qemu-system-moxie",
|
||||
"qemu-system-s390x",
|
||||
"qemu-system-unicore32",
|
||||
"qemu-system-i386",
|
||||
"qemu-system-mips",
|
||||
"qemu-system-or32",
|
||||
"qemu-system-sh4",
|
||||
"qemu-system-x86_64",
|
||||
"qemu-system-lm32",
|
||||
"qemu-system-mips64",
|
||||
"qemu-system-ppc",
|
||||
"qemu-system-sh4eb",
|
||||
"qemu-system-xtensa",
|
||||
"qemu-system-alpha",
|
||||
"qemu-system-m68k",
|
||||
"qemu-system-mips64el",
|
||||
"qemu-system-ppc64",
|
||||
"qemu-system-sparc",
|
||||
"qemu-system-xtensaeb",
|
||||
]
|
||||
QEMU_BINARIES_FOR_CLOUD.sort()
|
||||
|
||||
@@ -135,6 +135,67 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="uiMonitorPortRangeGroupBox">
|
||||
<property name="title">
|
||||
<string>Monitor port range</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QSpinBox" name="uiMonitorStartPortSpinBox">
|
||||
<property name="suffix">
|
||||
<string notr="true"> TCP</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="uiMonitorPortRangeLabel">
|
||||
<property name="text">
|
||||
<string>to</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="uiMonitorEndPortSpinBox">
|
||||
<property name="suffix">
|
||||
<string notr="true"> TCP</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_1">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="uiUDPPortRangeGroupBox">
|
||||
<property name="title">
|
||||
<string>UDP tunneling port range</string>
|
||||
@@ -195,7 +256,7 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="4" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_preferences_page.ui'
|
||||
# Form implementation generated from reading ui file 'qemu_preferences_page.ui'
|
||||
#
|
||||
# Created: Sun Oct 19 11:35:54 2014
|
||||
# Created: Tue Dec 23 15:05:44 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -79,6 +79,30 @@ class Ui_QemuPreferencesPageWidget(object):
|
||||
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.horizontalLayout.addItem(spacerItem)
|
||||
self.gridLayout_2.addWidget(self.uiConsolePortRangeGroupBox, 0, 0, 1, 2)
|
||||
self.uiMonitorPortRangeGroupBox = QtGui.QGroupBox(self.uiAdvancedSettingsTabWidget)
|
||||
self.uiMonitorPortRangeGroupBox.setObjectName(_fromUtf8("uiMonitorPortRangeGroupBox"))
|
||||
self.horizontalLayout1 = QtGui.QHBoxLayout(self.uiMonitorPortRangeGroupBox)
|
||||
self.horizontalLayout1.setObjectName(_fromUtf8("horizontalLayout1"))
|
||||
self.uiMonitorStartPortSpinBox = QtGui.QSpinBox(self.uiMonitorPortRangeGroupBox)
|
||||
self.uiMonitorStartPortSpinBox.setSuffix(_fromUtf8(" TCP"))
|
||||
self.uiMonitorStartPortSpinBox.setMinimum(1)
|
||||
self.uiMonitorStartPortSpinBox.setMaximum(65535)
|
||||
self.uiMonitorStartPortSpinBox.setProperty("value", 1)
|
||||
self.uiMonitorStartPortSpinBox.setObjectName(_fromUtf8("uiMonitorStartPortSpinBox"))
|
||||
self.horizontalLayout1.addWidget(self.uiMonitorStartPortSpinBox)
|
||||
self.uiMonitorPortRangeLabel = QtGui.QLabel(self.uiMonitorPortRangeGroupBox)
|
||||
self.uiMonitorPortRangeLabel.setObjectName(_fromUtf8("uiMonitorPortRangeLabel"))
|
||||
self.horizontalLayout1.addWidget(self.uiMonitorPortRangeLabel)
|
||||
self.uiMonitorEndPortSpinBox = QtGui.QSpinBox(self.uiMonitorPortRangeGroupBox)
|
||||
self.uiMonitorEndPortSpinBox.setSuffix(_fromUtf8(" TCP"))
|
||||
self.uiMonitorEndPortSpinBox.setMinimum(1)
|
||||
self.uiMonitorEndPortSpinBox.setMaximum(65535)
|
||||
self.uiMonitorEndPortSpinBox.setProperty("value", 1)
|
||||
self.uiMonitorEndPortSpinBox.setObjectName(_fromUtf8("uiMonitorEndPortSpinBox"))
|
||||
self.horizontalLayout1.addWidget(self.uiMonitorEndPortSpinBox)
|
||||
spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.horizontalLayout1.addItem(spacerItem1)
|
||||
self.gridLayout_2.addWidget(self.uiMonitorPortRangeGroupBox, 1, 0, 1, 2)
|
||||
self.uiUDPPortRangeGroupBox = QtGui.QGroupBox(self.uiAdvancedSettingsTabWidget)
|
||||
self.uiUDPPortRangeGroupBox.setObjectName(_fromUtf8("uiUDPPortRangeGroupBox"))
|
||||
self.horizontalLayout_4 = QtGui.QHBoxLayout(self.uiUDPPortRangeGroupBox)
|
||||
@@ -100,17 +124,17 @@ class Ui_QemuPreferencesPageWidget(object):
|
||||
self.uiUDPEndPortSpinBox.setProperty("value", 1)
|
||||
self.uiUDPEndPortSpinBox.setObjectName(_fromUtf8("uiUDPEndPortSpinBox"))
|
||||
self.horizontalLayout_4.addWidget(self.uiUDPEndPortSpinBox)
|
||||
spacerItem1 = QtGui.QSpacerItem(147, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_4.addItem(spacerItem1)
|
||||
self.gridLayout_2.addWidget(self.uiUDPPortRangeGroupBox, 1, 0, 1, 2)
|
||||
spacerItem2 = QtGui.QSpacerItem(20, 304, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem2, 3, 0, 1, 1)
|
||||
spacerItem2 = QtGui.QSpacerItem(147, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_4.addItem(spacerItem2)
|
||||
self.gridLayout_2.addWidget(self.uiUDPPortRangeGroupBox, 2, 0, 1, 2)
|
||||
spacerItem3 = QtGui.QSpacerItem(20, 304, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout_2.addItem(spacerItem3, 4, 0, 1, 1)
|
||||
self.uiTabWidget.addTab(self.uiAdvancedSettingsTabWidget, _fromUtf8(""))
|
||||
self.verticalLayout.addWidget(self.uiTabWidget)
|
||||
self.horizontalLayout_2 = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2"))
|
||||
spacerItem3 = QtGui.QSpacerItem(254, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem3)
|
||||
spacerItem4 = QtGui.QSpacerItem(254, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem4)
|
||||
self.uiRestoreDefaultsPushButton = QtGui.QPushButton(QemuPreferencesPageWidget)
|
||||
self.uiRestoreDefaultsPushButton.setObjectName(_fromUtf8("uiRestoreDefaultsPushButton"))
|
||||
self.horizontalLayout_2.addWidget(self.uiRestoreDefaultsPushButton)
|
||||
@@ -129,6 +153,8 @@ class Ui_QemuPreferencesPageWidget(object):
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiServerSettingsTabWidget), _translate("QemuPreferencesPageWidget", "Server settings", None))
|
||||
self.uiConsolePortRangeGroupBox.setTitle(_translate("QemuPreferencesPageWidget", "Console port range", None))
|
||||
self.uiConsolePortRangeLabel.setText(_translate("QemuPreferencesPageWidget", "to", None))
|
||||
self.uiMonitorPortRangeGroupBox.setTitle(_translate("QemuPreferencesPageWidget", "Monitor port range", None))
|
||||
self.uiMonitorPortRangeLabel.setText(_translate("QemuPreferencesPageWidget", "to", None))
|
||||
self.uiUDPPortRangeGroupBox.setTitle(_translate("QemuPreferencesPageWidget", "UDP tunneling port range", None))
|
||||
self.uiUDPPortRangeLabel.setText(_translate("QemuPreferencesPageWidget", "to", None))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiAdvancedSettingsTabWidget), _translate("QemuPreferencesPageWidget", "Advanced settings", None))
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>492</width>
|
||||
<height>399</height>
|
||||
<width>486</width>
|
||||
<height>407</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -34,7 +34,7 @@
|
||||
<item row="0" column="2">
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="2">
|
||||
<item row="5" column="1" colspan="2">
|
||||
<spacer name="spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -71,6 +71,20 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QSpinBox" name="uiMonitorPortSpinBox">
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QLabel" name="uiMonitorPortLabel">
|
||||
<property name="text">
|
||||
<string>Monitor port:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiNameLabel">
|
||||
<property name="text">
|
||||
@@ -175,7 +189,7 @@
|
||||
<attribute name="title">
|
||||
<string>Network</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_8">
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="uiAdaptersLabel">
|
||||
<property name="text">
|
||||
@@ -192,10 +206,10 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>8</number>
|
||||
<number>32</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -206,7 +220,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="2">
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="uiAdapterTypesComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
@@ -216,7 +230,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiLegacyNetworkingCheckBox">
|
||||
<property name="text">
|
||||
<string>Use the legacy networking mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -235,8 +256,8 @@
|
||||
<attribute name="title">
|
||||
<string>Advanced settings</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="uiLinuxBootGroupBox">
|
||||
<property name="title">
|
||||
<string>Linux boot specific settings</string>
|
||||
@@ -295,7 +316,99 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="uiOptimizationGroupBox">
|
||||
<property name="title">
|
||||
<string>Optimizations</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiActivateCPUThrottlingCheckBox">
|
||||
<property name="text">
|
||||
<string>Activate CPU throttling</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiCPUThrottlingLabel">
|
||||
<property name="text">
|
||||
<string>Percentage of CPU allowed:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="uiCPUThrottlingSpinBox">
|
||||
<property name="suffix">
|
||||
<string> %</string>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>800</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiProcessPriorityLabel">
|
||||
<property name="text">
|
||||
<string>Process priority:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="uiProcessPriorityComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>3</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Realtime</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Very high</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>High</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Normal</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Low</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Very low</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Aditional settings</string>
|
||||
@@ -316,7 +429,7 @@
|
||||
<zorder>uiQemuOptionsLabel</zorder>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_configuration_page.ui'
|
||||
#
|
||||
# Created: Fri Oct 10 10:43:48 2014
|
||||
# Created: Sat Jan 3 14:50:40 2015
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -26,7 +26,7 @@ except AttributeError:
|
||||
class Ui_QemuVMConfigPageWidget(object):
|
||||
def setupUi(self, QemuVMConfigPageWidget):
|
||||
QemuVMConfigPageWidget.setObjectName(_fromUtf8("QemuVMConfigPageWidget"))
|
||||
QemuVMConfigPageWidget.resize(492, 399)
|
||||
QemuVMConfigPageWidget.resize(486, 407)
|
||||
self.verticalLayout = QtGui.QVBoxLayout(QemuVMConfigPageWidget)
|
||||
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
|
||||
self.uiQemutabWidget = QtGui.QTabWidget(QemuVMConfigPageWidget)
|
||||
@@ -42,7 +42,7 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiNameLineEdit.setObjectName(_fromUtf8("uiNameLineEdit"))
|
||||
self.gridLayout_4.addWidget(self.uiNameLineEdit, 0, 2, 1, 1)
|
||||
spacerItem = QtGui.QSpacerItem(263, 94, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout_4.addItem(spacerItem, 4, 1, 1, 2)
|
||||
self.gridLayout_4.addItem(spacerItem, 5, 1, 1, 2)
|
||||
self.uiQemuListComboBox = QtGui.QComboBox(self.tab)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@@ -58,6 +58,13 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiConsolePortLabel = QtGui.QLabel(self.tab)
|
||||
self.uiConsolePortLabel.setObjectName(_fromUtf8("uiConsolePortLabel"))
|
||||
self.gridLayout_4.addWidget(self.uiConsolePortLabel, 3, 0, 1, 2)
|
||||
self.uiMonitorPortSpinBox = QtGui.QSpinBox(self.tab)
|
||||
self.uiMonitorPortSpinBox.setMaximum(65535)
|
||||
self.uiMonitorPortSpinBox.setObjectName(_fromUtf8("uiMonitorPortSpinBox"))
|
||||
self.gridLayout_4.addWidget(self.uiMonitorPortSpinBox, 4, 2, 1, 1)
|
||||
self.uiMonitorPortLabel = QtGui.QLabel(self.tab)
|
||||
self.uiMonitorPortLabel.setObjectName(_fromUtf8("uiMonitorPortLabel"))
|
||||
self.gridLayout_4.addWidget(self.uiMonitorPortLabel, 4, 0, 1, 2)
|
||||
self.uiNameLabel = QtGui.QLabel(self.tab)
|
||||
self.uiNameLabel.setObjectName(_fromUtf8("uiNameLabel"))
|
||||
self.gridLayout_4.addWidget(self.uiNameLabel, 0, 0, 1, 1)
|
||||
@@ -106,24 +113,24 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiQemutabWidget.addTab(self.tab_3, _fromUtf8(""))
|
||||
self.tab_7 = QtGui.QWidget()
|
||||
self.tab_7.setObjectName(_fromUtf8("tab_7"))
|
||||
self.gridLayout_8 = QtGui.QGridLayout(self.tab_7)
|
||||
self.gridLayout_8.setObjectName(_fromUtf8("gridLayout_8"))
|
||||
self.gridLayout_5 = QtGui.QGridLayout(self.tab_7)
|
||||
self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5"))
|
||||
self.uiAdaptersLabel = QtGui.QLabel(self.tab_7)
|
||||
self.uiAdaptersLabel.setObjectName(_fromUtf8("uiAdaptersLabel"))
|
||||
self.gridLayout_8.addWidget(self.uiAdaptersLabel, 0, 0, 1, 1)
|
||||
self.gridLayout_5.addWidget(self.uiAdaptersLabel, 0, 0, 1, 1)
|
||||
self.uiAdaptersSpinBox = QtGui.QSpinBox(self.tab_7)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiAdaptersSpinBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiAdaptersSpinBox.setSizePolicy(sizePolicy)
|
||||
self.uiAdaptersSpinBox.setMinimum(1)
|
||||
self.uiAdaptersSpinBox.setMaximum(8)
|
||||
self.uiAdaptersSpinBox.setMinimum(0)
|
||||
self.uiAdaptersSpinBox.setMaximum(32)
|
||||
self.uiAdaptersSpinBox.setObjectName(_fromUtf8("uiAdaptersSpinBox"))
|
||||
self.gridLayout_8.addWidget(self.uiAdaptersSpinBox, 0, 1, 1, 1)
|
||||
self.gridLayout_5.addWidget(self.uiAdaptersSpinBox, 0, 1, 1, 1)
|
||||
self.uiAdapterTypesLabel = QtGui.QLabel(self.tab_7)
|
||||
self.uiAdapterTypesLabel.setObjectName(_fromUtf8("uiAdapterTypesLabel"))
|
||||
self.gridLayout_8.addWidget(self.uiAdapterTypesLabel, 1, 0, 1, 1)
|
||||
self.gridLayout_5.addWidget(self.uiAdapterTypesLabel, 1, 0, 1, 1)
|
||||
self.uiAdapterTypesComboBox = QtGui.QComboBox(self.tab_7)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@@ -131,14 +138,17 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
sizePolicy.setHeightForWidth(self.uiAdapterTypesComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiAdapterTypesComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiAdapterTypesComboBox.setObjectName(_fromUtf8("uiAdapterTypesComboBox"))
|
||||
self.gridLayout_8.addWidget(self.uiAdapterTypesComboBox, 1, 1, 1, 2)
|
||||
self.gridLayout_5.addWidget(self.uiAdapterTypesComboBox, 1, 1, 1, 1)
|
||||
self.uiLegacyNetworkingCheckBox = QtGui.QCheckBox(self.tab_7)
|
||||
self.uiLegacyNetworkingCheckBox.setObjectName(_fromUtf8("uiLegacyNetworkingCheckBox"))
|
||||
self.gridLayout_5.addWidget(self.uiLegacyNetworkingCheckBox, 2, 0, 1, 2)
|
||||
spacerItem2 = QtGui.QSpacerItem(20, 261, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout_8.addItem(spacerItem2, 2, 1, 1, 1)
|
||||
self.gridLayout_5.addItem(spacerItem2, 3, 1, 1, 1)
|
||||
self.uiQemutabWidget.addTab(self.tab_7, _fromUtf8(""))
|
||||
self.tab_2 = QtGui.QWidget()
|
||||
self.tab_2.setObjectName(_fromUtf8("tab_2"))
|
||||
self.gridLayout = QtGui.QGridLayout(self.tab_2)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.verticalLayout_2 = QtGui.QVBoxLayout(self.tab_2)
|
||||
self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2"))
|
||||
self.uiLinuxBootGroupBox = QtGui.QGroupBox(self.tab_2)
|
||||
self.uiLinuxBootGroupBox.setObjectName(_fromUtf8("uiLinuxBootGroupBox"))
|
||||
self.gridLayout_2 = QtGui.QGridLayout(self.uiLinuxBootGroupBox)
|
||||
@@ -169,7 +179,42 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiKernelCommandLineEdit = QtGui.QLineEdit(self.uiLinuxBootGroupBox)
|
||||
self.uiKernelCommandLineEdit.setObjectName(_fromUtf8("uiKernelCommandLineEdit"))
|
||||
self.gridLayout_2.addWidget(self.uiKernelCommandLineEdit, 2, 1, 1, 2)
|
||||
self.gridLayout.addWidget(self.uiLinuxBootGroupBox, 0, 0, 1, 1)
|
||||
self.verticalLayout_2.addWidget(self.uiLinuxBootGroupBox)
|
||||
self.uiOptimizationGroupBox = QtGui.QGroupBox(self.tab_2)
|
||||
self.uiOptimizationGroupBox.setObjectName(_fromUtf8("uiOptimizationGroupBox"))
|
||||
self.gridLayout = QtGui.QGridLayout(self.uiOptimizationGroupBox)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.uiActivateCPUThrottlingCheckBox = QtGui.QCheckBox(self.uiOptimizationGroupBox)
|
||||
self.uiActivateCPUThrottlingCheckBox.setChecked(True)
|
||||
self.uiActivateCPUThrottlingCheckBox.setObjectName(_fromUtf8("uiActivateCPUThrottlingCheckBox"))
|
||||
self.gridLayout.addWidget(self.uiActivateCPUThrottlingCheckBox, 0, 0, 1, 2)
|
||||
self.uiCPUThrottlingLabel = QtGui.QLabel(self.uiOptimizationGroupBox)
|
||||
self.uiCPUThrottlingLabel.setObjectName(_fromUtf8("uiCPUThrottlingLabel"))
|
||||
self.gridLayout.addWidget(self.uiCPUThrottlingLabel, 1, 0, 1, 1)
|
||||
self.uiCPUThrottlingSpinBox = QtGui.QSpinBox(self.uiOptimizationGroupBox)
|
||||
self.uiCPUThrottlingSpinBox.setMinimum(1)
|
||||
self.uiCPUThrottlingSpinBox.setMaximum(800)
|
||||
self.uiCPUThrottlingSpinBox.setProperty("value", 100)
|
||||
self.uiCPUThrottlingSpinBox.setObjectName(_fromUtf8("uiCPUThrottlingSpinBox"))
|
||||
self.gridLayout.addWidget(self.uiCPUThrottlingSpinBox, 1, 1, 1, 1)
|
||||
self.uiProcessPriorityLabel = QtGui.QLabel(self.uiOptimizationGroupBox)
|
||||
self.uiProcessPriorityLabel.setObjectName(_fromUtf8("uiProcessPriorityLabel"))
|
||||
self.gridLayout.addWidget(self.uiProcessPriorityLabel, 2, 0, 1, 1)
|
||||
self.uiProcessPriorityComboBox = QtGui.QComboBox(self.uiOptimizationGroupBox)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.uiProcessPriorityComboBox.sizePolicy().hasHeightForWidth())
|
||||
self.uiProcessPriorityComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiProcessPriorityComboBox.setObjectName(_fromUtf8("uiProcessPriorityComboBox"))
|
||||
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
|
||||
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
|
||||
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
|
||||
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
|
||||
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
|
||||
self.uiProcessPriorityComboBox.addItem(_fromUtf8(""))
|
||||
self.gridLayout.addWidget(self.uiProcessPriorityComboBox, 2, 1, 1, 1)
|
||||
self.verticalLayout_2.addWidget(self.uiOptimizationGroupBox)
|
||||
self.groupBox = QtGui.QGroupBox(self.tab_2)
|
||||
self.groupBox.setObjectName(_fromUtf8("groupBox"))
|
||||
self.gridLayout_3 = QtGui.QGridLayout(self.groupBox)
|
||||
@@ -180,20 +225,22 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiQemuOptionsLineEdit = QtGui.QLineEdit(self.groupBox)
|
||||
self.uiQemuOptionsLineEdit.setObjectName(_fromUtf8("uiQemuOptionsLineEdit"))
|
||||
self.gridLayout_3.addWidget(self.uiQemuOptionsLineEdit, 0, 2, 1, 1)
|
||||
self.gridLayout.addWidget(self.groupBox, 1, 0, 1, 1)
|
||||
self.verticalLayout_2.addWidget(self.groupBox)
|
||||
spacerItem3 = QtGui.QSpacerItem(20, 90, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem3, 2, 0, 1, 1)
|
||||
self.verticalLayout_2.addItem(spacerItem3)
|
||||
self.uiQemutabWidget.addTab(self.tab_2, _fromUtf8(""))
|
||||
self.verticalLayout.addWidget(self.uiQemutabWidget)
|
||||
|
||||
self.retranslateUi(QemuVMConfigPageWidget)
|
||||
self.uiQemutabWidget.setCurrentIndex(0)
|
||||
self.uiProcessPriorityComboBox.setCurrentIndex(3)
|
||||
QtCore.QMetaObject.connectSlotsByName(QemuVMConfigPageWidget)
|
||||
|
||||
def retranslateUi(self, QemuVMConfigPageWidget):
|
||||
QemuVMConfigPageWidget.setWindowTitle(_translate("QemuVMConfigPageWidget", "QEMU VM configuration", None))
|
||||
self.uiQemuListLabel.setText(_translate("QemuVMConfigPageWidget", "Qemu binary:", None))
|
||||
self.uiConsolePortLabel.setText(_translate("QemuVMConfigPageWidget", "Console port:", None))
|
||||
self.uiMonitorPortLabel.setText(_translate("QemuVMConfigPageWidget", "Monitor port:", None))
|
||||
self.uiNameLabel.setText(_translate("QemuVMConfigPageWidget", "VM name:", None))
|
||||
self.uiRamLabel.setText(_translate("QemuVMConfigPageWidget", "RAM:", None))
|
||||
self.uiRamSpinBox.setSuffix(_translate("QemuVMConfigPageWidget", " MB", None))
|
||||
@@ -205,6 +252,7 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.tab_3), _translate("QemuVMConfigPageWidget", "HDD", None))
|
||||
self.uiAdaptersLabel.setText(_translate("QemuVMConfigPageWidget", "Adapters:", None))
|
||||
self.uiAdapterTypesLabel.setText(_translate("QemuVMConfigPageWidget", "Type:", None))
|
||||
self.uiLegacyNetworkingCheckBox.setText(_translate("QemuVMConfigPageWidget", "Use the legacy networking mode", None))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.tab_7), _translate("QemuVMConfigPageWidget", "Network", None))
|
||||
self.uiLinuxBootGroupBox.setTitle(_translate("QemuVMConfigPageWidget", "Linux boot specific settings", None))
|
||||
self.uiKernelCommandLineLabel.setText(_translate("QemuVMConfigPageWidget", "Kernel command line:", None))
|
||||
@@ -212,6 +260,17 @@ class Ui_QemuVMConfigPageWidget(object):
|
||||
self.uiKernelImageLabel.setText(_translate("QemuVMConfigPageWidget", "Kernel image:", None))
|
||||
self.uiInitrdToolButton.setText(_translate("QemuVMConfigPageWidget", "&Browse...", None))
|
||||
self.uiKernelImageToolButton.setText(_translate("QemuVMConfigPageWidget", "&Browse...", None))
|
||||
self.uiOptimizationGroupBox.setTitle(_translate("QemuVMConfigPageWidget", "Optimizations", None))
|
||||
self.uiActivateCPUThrottlingCheckBox.setText(_translate("QemuVMConfigPageWidget", "Activate CPU throttling", None))
|
||||
self.uiCPUThrottlingLabel.setText(_translate("QemuVMConfigPageWidget", "Percentage of CPU allowed:", None))
|
||||
self.uiCPUThrottlingSpinBox.setSuffix(_translate("QemuVMConfigPageWidget", " %", None))
|
||||
self.uiProcessPriorityLabel.setText(_translate("QemuVMConfigPageWidget", "Process priority:", None))
|
||||
self.uiProcessPriorityComboBox.setItemText(0, _translate("QemuVMConfigPageWidget", "Realtime", None))
|
||||
self.uiProcessPriorityComboBox.setItemText(1, _translate("QemuVMConfigPageWidget", "Very high", None))
|
||||
self.uiProcessPriorityComboBox.setItemText(2, _translate("QemuVMConfigPageWidget", "High", None))
|
||||
self.uiProcessPriorityComboBox.setItemText(3, _translate("QemuVMConfigPageWidget", "Normal", None))
|
||||
self.uiProcessPriorityComboBox.setItemText(4, _translate("QemuVMConfigPageWidget", "Low", None))
|
||||
self.uiProcessPriorityComboBox.setItemText(5, _translate("QemuVMConfigPageWidget", "Very low", None))
|
||||
self.groupBox.setTitle(_translate("QemuVMConfigPageWidget", "Aditional settings", None))
|
||||
self.uiQemuOptionsLabel.setText(_translate("QemuVMConfigPageWidget", "Options:", None))
|
||||
self.uiQemutabWidget.setTabText(self.uiQemutabWidget.indexOf(self.tab_2), _translate("QemuVMConfigPageWidget", "Advanced settings", None))
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>12</pointsize>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/qemu/ui/qemu_vm_preferences_page.ui'
|
||||
#
|
||||
# Created: Fri Oct 3 14:05:58 2014
|
||||
# Created: Wed Nov 19 18:57:19 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -37,7 +37,7 @@ class Ui_QemuVMPreferencesPageWidget(object):
|
||||
self.uiQemuVMsTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiQemuVMsTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(12)
|
||||
font.setPointSize(11)
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.uiQemuVMsTreeWidget.setFont(font)
|
||||
|
||||
@@ -26,6 +26,7 @@ from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .virtualbox_vm import VirtualBoxVM
|
||||
from .settings import VBOX_SETTINGS, VBOX_SETTING_TYPES
|
||||
from .settings import VBOX_VM_SETTINGS, VBOX_VM_SETTING_TYPES
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -87,29 +88,14 @@ class VirtualBox(Module):
|
||||
size = settings.beginReadArray("VM")
|
||||
for index in range(0, size):
|
||||
settings.setArrayIndex(index)
|
||||
|
||||
vmname = settings.value("vmname", "")
|
||||
adapters = settings.value("adapters", 1, type=int)
|
||||
default_symbol = settings.value("default_symbol", ":/symbols/vbox_guest.normal.svg")
|
||||
hover_symbol = settings.value("hover_symbol", ":/symbols/vbox_guest.selected.svg")
|
||||
category = settings.value("category", Node.end_devices, type=int)
|
||||
adapter_start_index = settings.value("adapter_start_index", 0, type=int)
|
||||
adapter_type = settings.value("adapter_type", "Automatic")
|
||||
headless = settings.value("headless", False, type=bool)
|
||||
enable_console = settings.value("enable_console", False, type=bool)
|
||||
server = settings.value("server", "local")
|
||||
|
||||
vmname = settings.value("vmname")
|
||||
server = settings.value("server")
|
||||
key = "{server}:{vmname}".format(server=server, vmname=vmname)
|
||||
self._virtualbox_vms[key] = {"vmname": vmname,
|
||||
"default_symbol": default_symbol,
|
||||
"hover_symbol": hover_symbol,
|
||||
"category": category,
|
||||
"adapters": adapters,
|
||||
"adapter_start_index": adapter_start_index,
|
||||
"adapter_type": adapter_type,
|
||||
"headless": headless,
|
||||
"enable_console": enable_console,
|
||||
"server": server}
|
||||
if key in self._virtualbox_vms or not vmname or not server:
|
||||
continue
|
||||
self._virtualbox_vms[key] = {}
|
||||
for setting_name, default_value in VBOX_VM_SETTINGS.items():
|
||||
self._virtualbox_vms[key][setting_name] = settings.value(setting_name, default_value, VBOX_VM_SETTING_TYPES[setting_name])
|
||||
|
||||
settings.endArray()
|
||||
settings.endGroup()
|
||||
@@ -254,6 +240,8 @@ class VirtualBox(Module):
|
||||
if server.isLocal():
|
||||
params.update({"working_dir": self._working_dir})
|
||||
else:
|
||||
if "vboxmanage_path" in params:
|
||||
del params["vboxmanage_path"] # do not send VBoxManage path to remote servers
|
||||
project_name = os.path.basename(self._working_dir)
|
||||
if project_name.endswith("-files"):
|
||||
project_name = project_name[:-6]
|
||||
@@ -277,8 +265,8 @@ class VirtualBox(Module):
|
||||
if server.isLocal():
|
||||
params.update({"working_dir": self._working_dir})
|
||||
else:
|
||||
if "vboxwrapper_path" in params:
|
||||
del params["vboxwrapper_path"] # do not send Vboxwrapper path to remote servers
|
||||
if "vboxmanage_path" in params:
|
||||
del params["vboxmanage_path"] # do not send VBoxManage path to remote servers
|
||||
project_name = os.path.basename(self._working_dir)
|
||||
if project_name.endswith("-files"):
|
||||
project_name = project_name[:-6]
|
||||
@@ -347,19 +335,22 @@ class VirtualBox(Module):
|
||||
else:
|
||||
vm = selected_vms[0]
|
||||
|
||||
for other_node in self._nodes:
|
||||
if other_node.settings()["vmname"] == self._virtualbox_vms[vm]["vmname"] and \
|
||||
(self._virtualbox_vms[vm]["server"] == "local" and other_node.server().isLocal() or self._virtualbox_vms[vm]["server"] == other_node.server().host):
|
||||
raise ModuleError("Sorry a VirtualBox VM can only be used once in your topology (this will change in future versions)")
|
||||
linked_base = self._virtualbox_vms[vm]["linked_base"]
|
||||
|
||||
if not linked_base:
|
||||
for other_node in self._nodes:
|
||||
if other_node.settings()["vmname"] == self._virtualbox_vms[vm]["vmname"] and \
|
||||
(self._virtualbox_vms[vm]["server"] == "local" and other_node.server().isLocal() or self._virtualbox_vms[vm]["server"] == other_node.server().host):
|
||||
raise ModuleError("Sorry a VirtualBox VM can only be used once in your topology (this will change in future versions)")
|
||||
|
||||
settings = {"adapters": self._virtualbox_vms[vm]["adapters"],
|
||||
"adapter_start_index": self._virtualbox_vms[vm]["adapter_start_index"],
|
||||
"adapter_type": self._virtualbox_vms[vm]["adapter_type"],
|
||||
"headless": self._virtualbox_vms[vm]["headless"],
|
||||
"enable_console": self._virtualbox_vms[vm]["enable_console"]}
|
||||
"enable_remote_console": self._virtualbox_vms[vm]["enable_remote_console"]}
|
||||
|
||||
vmname = self._virtualbox_vms[vm]["vmname"]
|
||||
node.setup(vmname, initial_settings=settings)
|
||||
node.setup(vmname, linked_clone=linked_base, initial_settings=settings)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
@@ -404,8 +395,11 @@ class VirtualBox(Module):
|
||||
raise ModuleError("Could not connect to server {}:{}: {}".format(server.host,
|
||||
server.port,
|
||||
e))
|
||||
|
||||
server.send_message("virtualbox.vm_list", None, callback)
|
||||
params = {}
|
||||
if server.isLocal():
|
||||
params["vboxmanage_path"] = self._settings["vboxmanage_path"]
|
||||
params["vbox_user"] = self._settings["vbox_user"]
|
||||
server.send_message("virtualbox.vm_list", params, callback)
|
||||
|
||||
def getVirtualBoxVMList(self):
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
Wizard for VirtualBox VMs.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.servers import Servers
|
||||
from gns3.utils.connect_to_server import ConnectToServer
|
||||
@@ -42,6 +44,9 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
|
||||
self.setupUi(self)
|
||||
self.setPixmap(QtGui.QWizard.LogoPixmap, QtGui.QPixmap(":/icons/virtualbox.png"))
|
||||
self.setWizardStyle(QtGui.QWizard.ModernStyle)
|
||||
if sys.platform.startswith("darwin"):
|
||||
# we want to see the cancel button on OSX
|
||||
self.setOptions(QtGui.QWizard.NoDefaultButton)
|
||||
|
||||
if VirtualBox.instance().settings()["use_local_server"]:
|
||||
# skip the server page if we use the local server
|
||||
@@ -67,7 +72,7 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
|
||||
VirtualBox.instance().getVirtualBoxVMsFromServer(self._server, self._getVirtualBoxVMsFromServerCallback)
|
||||
except ModuleError as e:
|
||||
self._vbox_vms_progress_dialog.reject()
|
||||
QtGui.QMessageBox.critical(self, "Qemu binaries", "Error while getting the QEMU binaries: {}".format(e))
|
||||
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "Error while getting the VirtualBox VMs: {}".format(e))
|
||||
|
||||
def _getVirtualBoxVMsFromServerCallback(self, result, error=False):
|
||||
"""
|
||||
@@ -82,7 +87,7 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
|
||||
self._vbox_vms_progress_dialog.accept()
|
||||
|
||||
if error:
|
||||
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "Error: ".format(result["message"]))
|
||||
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "{}".format(result["message"]))
|
||||
else:
|
||||
self.uiVMListComboBox.clear()
|
||||
existing_vms = []
|
||||
@@ -108,10 +113,17 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
|
||||
if VirtualBox.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
server = Servers.instance().localServer()
|
||||
else:
|
||||
if not Servers.instance().remoteServers():
|
||||
QtGui.QMessageBox.critical(self, "Remote server", "There is no remote server registered in VirtualBox preferences")
|
||||
return False
|
||||
server = self.uiRemoteServersComboBox.itemData(self.uiRemoteServersComboBox.currentIndex())
|
||||
if not server.connected() and ConnectToServer(self, server) is False:
|
||||
return False
|
||||
self._server = server
|
||||
if self.currentPage() == self.uiVirtualBoxWizardPage:
|
||||
if not self.uiVMListComboBox.count():
|
||||
QtGui.QMessageBox.critical(self, "VirtualBox VMs", "There is no VirtualBox VM available!")
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSettings(self):
|
||||
@@ -121,9 +133,6 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
|
||||
:return: settings dict
|
||||
"""
|
||||
|
||||
if not self.uiVMListComboBox.count():
|
||||
return {}
|
||||
|
||||
if VirtualBox.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked():
|
||||
server = "local"
|
||||
else:
|
||||
@@ -134,6 +143,7 @@ class VirtualBoxVMWizard(QtGui.QWizard, Ui_VirtualBoxVMWizard):
|
||||
settings = {
|
||||
"vmname": vmname,
|
||||
"server": server,
|
||||
"linked_base": self.uiBaseVMCheckBox.isChecked()
|
||||
}
|
||||
|
||||
return settings
|
||||
|
||||
@@ -40,25 +40,25 @@ class VirtualBoxPreferencesPage(QtGui.QWidget, Ui_VirtualBoxPreferencesPageWidge
|
||||
# connect signals
|
||||
self.uiUseLocalServercheckBox.stateChanged.connect(self._useLocalServerSlot)
|
||||
self.uiRestoreDefaultsPushButton.clicked.connect(self._restoreDefaultsSlot)
|
||||
self.uiVboxWrapperPathToolButton.clicked.connect(self._vboxPathBrowserSlot)
|
||||
self.uiVboxManagePathToolButton.clicked.connect(self._vboxPathBrowserSlot)
|
||||
|
||||
#FIXME: temporally hide test button
|
||||
self.uiTestSettingsPushButton.hide()
|
||||
|
||||
def _vboxPathBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select VirtualBox wrapper.
|
||||
Slot to open a file browser and select VBoxManage.
|
||||
"""
|
||||
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select VirtualBox wrapper", ".")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select VBoxManage", ".")
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not os.access(path, os.X_OK):
|
||||
QtGui.QMessageBox.critical(self, "VirtualBox wrapper", "{} is not an executable".format(os.path.basename(path)))
|
||||
QtGui.QMessageBox.critical(self, "VBoxManage", "{} is not an executable".format(os.path.basename(path)))
|
||||
return
|
||||
|
||||
self.uiVboxWrapperPathLineEdit.setText(os.path.normpath(path))
|
||||
self.uiVboxManagePathLineEdit.setText(os.path.normpath(path))
|
||||
|
||||
def _restoreDefaultsSlot(self):
|
||||
"""
|
||||
@@ -84,7 +84,8 @@ class VirtualBoxPreferencesPage(QtGui.QWidget, Ui_VirtualBoxPreferencesPageWidge
|
||||
:param settings: VirtualBox settings
|
||||
"""
|
||||
|
||||
self.uiVboxWrapperPathLineEdit.setText(settings["vboxwrapper_path"])
|
||||
self.uiVboxManagePathLineEdit.setText(settings["vboxmanage_path"])
|
||||
self.uiVboxManageUserLineEdit.setText(settings["vbox_user"])
|
||||
self.uiUseLocalServercheckBox.setChecked(settings["use_local_server"])
|
||||
self.uiConsoleStartPortSpinBox.setValue(settings["console_start_port_range"])
|
||||
self.uiConsoleEndPortSpinBox.setValue(settings["console_end_port_range"])
|
||||
@@ -125,7 +126,8 @@ class VirtualBoxPreferencesPage(QtGui.QWidget, Ui_VirtualBoxPreferencesPageWidge
|
||||
"""
|
||||
|
||||
new_settings = {}
|
||||
new_settings["vboxwrapper_path"] = self.uiVboxWrapperPathLineEdit.text()
|
||||
new_settings["vboxmanage_path"] = self.uiVboxManagePathLineEdit.text()
|
||||
new_settings["vbox_user"] = self.uiVboxManageUserLineEdit.text()
|
||||
new_settings["use_local_server"] = self.uiUseLocalServercheckBox.isChecked()
|
||||
new_settings["console_start_port_range"] = self.uiConsoleStartPortSpinBox.value()
|
||||
new_settings["console_end_port_range"] = self.uiConsoleEndPortSpinBox.value()
|
||||
|
||||
@@ -36,8 +36,7 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
|
||||
self.setupUi(self)
|
||||
|
||||
self.uiAdapterTypesComboBox.clear()
|
||||
self.uiAdapterTypesComboBox.addItems(["Automatic",
|
||||
"PCnet-PCI II (Am79C970A)",
|
||||
self.uiAdapterTypesComboBox.addItems(["PCnet-PCI II (Am79C970A)",
|
||||
"PCNet-FAST III (Am79C973)",
|
||||
"Intel PRO/1000 MT Desktop (82540EM)",
|
||||
"Intel PRO/1000 T Server (82543GC)",
|
||||
@@ -72,6 +71,11 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
|
||||
if "linked_base" in settings:
|
||||
self.uiBaseVMCheckBox.setChecked(settings["linked_base"])
|
||||
else:
|
||||
self.uiBaseVMCheckBox.hide()
|
||||
|
||||
else:
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
@@ -86,7 +90,7 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
|
||||
if index != -1:
|
||||
self.uiAdapterTypesComboBox.setCurrentIndex(index)
|
||||
self.uiHeadlessModeCheckBox.setChecked(settings["headless"])
|
||||
self.uiEnableConsoleCheckBox.setChecked(settings["enable_console"])
|
||||
self.uiEnableConsoleCheckBox.setChecked(settings["enable_remote_console"])
|
||||
|
||||
def saveSettings(self, settings, node=None, group=False):
|
||||
"""
|
||||
@@ -108,13 +112,18 @@ class virtualBoxVMConfigurationPage(QtGui.QWidget, Ui_virtualBoxVMConfigPageWidg
|
||||
else:
|
||||
settings["name"] = name
|
||||
|
||||
settings["console"] = self.uiConsolePortSpinBox.value()
|
||||
settings["enable_console"] = self.uiEnableConsoleCheckBox.isChecked()
|
||||
if "console" in settings:
|
||||
settings["console"] = self.uiConsolePortSpinBox.value()
|
||||
|
||||
if "linked_base" in settings:
|
||||
settings["linked_base"] = self.uiBaseVMCheckBox.isChecked()
|
||||
|
||||
settings["enable_remote_console"] = self.uiEnableConsoleCheckBox.isChecked()
|
||||
|
||||
else:
|
||||
del settings["name"]
|
||||
del settings["console"]
|
||||
del settings["enable_console"]
|
||||
del settings["enable_remote_console"]
|
||||
|
||||
|
||||
settings["adapter_type"] = self.uiAdapterTypesComboBox.currentText()
|
||||
|
||||
@@ -28,6 +28,7 @@ from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog
|
||||
from gns3.dialogs.configuration_dialog import ConfigurationDialog
|
||||
|
||||
from .. import VirtualBox
|
||||
from ..settings import VBOX_VM_SETTINGS
|
||||
from ..ui.virtualbox_vm_preferences_page_ui import Ui_VirtualBoxVMPreferencesPageWidget
|
||||
from ..pages.virtualbox_vm_configuration_page import virtualBoxVMConfigurationPage
|
||||
from ..dialogs.virtualbox_vm_wizard import VirtualBoxVMWizard
|
||||
@@ -87,12 +88,14 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
|
||||
section_item = self._createSectionItem("General")
|
||||
QtGui.QTreeWidgetItem(section_item, ["VM name:", vbox_vm["vmname"]])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Server:", vbox_vm["server"]])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Console enabled:", "{}".format(vbox_vm["enable_console"])])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Remote console enabled:", "{}".format(vbox_vm["enable_remote_console"])])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Headless mode enabled:", "{}".format(vbox_vm["headless"])])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Linked base VM:", "{}".format(vbox_vm["linked_base"])])
|
||||
|
||||
# fill out the Network section
|
||||
section_item = self._createSectionItem("Network")
|
||||
QtGui.QTreeWidgetItem(section_item, ["Adapters:", str(vbox_vm["adapters"])])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Start at:", str(vbox_vm["adapter_start_index"])])
|
||||
QtGui.QTreeWidgetItem(section_item, ["Type:", vbox_vm["adapter_type"]])
|
||||
|
||||
self.uiVirtualBoxVMInfoTreeWidget.expandAll()
|
||||
@@ -109,22 +112,10 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
|
||||
if wizard.exec_():
|
||||
|
||||
new_vm_settings = wizard.getSettings()
|
||||
if not new_vm_settings:
|
||||
return
|
||||
|
||||
key = "{server}:{vmname}".format(server=new_vm_settings["server"], vmname=new_vm_settings["vmname"])
|
||||
self._virtualbox_vms[key] = {"vmname": "",
|
||||
"default_symbol": ":/symbols/vbox_guest.normal.svg",
|
||||
"hover_symbol": ":/symbols/vbox_guest.selected.svg",
|
||||
"category": Node.end_devices,
|
||||
"adapters": 1,
|
||||
"adapter_start_index": 0,
|
||||
"adapter_type": "Automatic",
|
||||
"headless": False,
|
||||
"enable_console": False,
|
||||
"server": "local"}
|
||||
|
||||
self._virtualbox_vms[key] = VBOX_VM_SETTINGS.copy()
|
||||
self._virtualbox_vms[key].update(new_vm_settings)
|
||||
|
||||
item = QtGui.QTreeWidgetItem(self.uiVirtualBoxVMsTreeWidget)
|
||||
item.setText(0, self._virtualbox_vms[key]["vmname"])
|
||||
item.setIcon(0, QtGui.QIcon(self._virtualbox_vms[key]["default_symbol"]))
|
||||
@@ -145,11 +136,16 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
if vbox_vm["vmname"] != item.text(0):
|
||||
if "{}:{}".format(vbox_vm["server"], vbox_vm["vmname"]) in self._virtualbox_vms:
|
||||
# FIXME: bug when changing name
|
||||
QtGui.QMessageBox.critical(self, "New VirtualBox VM", "VM name {} already exists".format(vbox_vm["vmname"]))
|
||||
new_key = "{server}:{vmname}".format(server=vbox_vm["server"], name=vbox_vm["vmname"])
|
||||
if new_key in self._virtualbox_vms:
|
||||
QtGui.QMessageBox.critical(self, "VirtualBox VM", "VirtualBox VM name {} already exists for server {}".format(vbox_vm["vmname"],
|
||||
vbox_vm["server"]))
|
||||
vbox_vm["vmname"] = item.text(0)
|
||||
return
|
||||
self._virtualbox_vms[new_key] = self._virtualbox_vms[key]
|
||||
del self._virtualbox_vms[key]
|
||||
item.setText(0, vbox_vm["vmname"])
|
||||
item.setData(0, QtCore.Qt.UserRole, new_key)
|
||||
self._refreshInfo(vbox_vm)
|
||||
|
||||
def _vboxVMDeleteSlot(self):
|
||||
@@ -191,16 +187,16 @@ class VirtualBoxVMPreferencesPage(QtGui.QWidget, Ui_VirtualBoxVMPreferencesPageW
|
||||
Change a symbol for a VirtualBox VM.
|
||||
"""
|
||||
|
||||
dialog = SymbolSelectionDialog(self)
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item = self.uiVirtualBoxVMsTreeWidget.currentItem()
|
||||
if item:
|
||||
item = self.uiVirtualBoxVMsTreeWidget.currentItem()
|
||||
if item:
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
vbox_vm = self._virtualbox_vms[key]
|
||||
dialog = SymbolSelectionDialog(self, symbol=vbox_vm["default_symbol"], category=vbox_vm["category"])
|
||||
dialog.show()
|
||||
if dialog.exec_():
|
||||
normal_symbol, selected_symbol = dialog.getSymbols()
|
||||
category = dialog.getCategory()
|
||||
item.setIcon(0, QtGui.QIcon(normal_symbol))
|
||||
key = item.data(0, QtCore.Qt.UserRole)
|
||||
vbox_vm = self._virtualbox_vms[key]
|
||||
vbox_vm["default_symbol"] = normal_symbol
|
||||
vbox_vm["hover_symbol"] = selected_symbol
|
||||
vbox_vm["category"] = category
|
||||
|
||||
@@ -19,26 +19,36 @@
|
||||
Default VirtualBox settings.
|
||||
"""
|
||||
|
||||
from gns3.node import Node
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# default path to VirtualBox wrapper executable
|
||||
if sys.platform.startswith("darwin") and hasattr(sys, "frozen"):
|
||||
DEFAULT_VBOXWRAPPER_PATH = os.path.join(os.getcwd(), "vboxwrapper")
|
||||
# default path to VirtualBox vboxmanage executable
|
||||
if sys.platform.startswith("win"):
|
||||
if "VBOX_INSTALL_PATH" in os.environ:
|
||||
DEFAULT_VBOXMANAGE_PATH = os.path.join(os.environ["VBOX_INSTALL_PATH"], "VBoxManage.exe")
|
||||
elif "VBOX_MSI_INSTALL_PATH" in os.environ:
|
||||
DEFAULT_VBOXMANAGE_PATH = os.path.join(os.environ["VBOX_MSI_INSTALL_PATH"], "VBoxManage.exe")
|
||||
else:
|
||||
DEFAULT_VBOXMANAGE_PATH = "VBoxManage.exe"
|
||||
elif sys.platform.startswith("darwin"):
|
||||
DEFAULT_VBOXMANAGE_PATH = "/Applications/VirtualBox.app/Contents/MacOS/VBoxManage"
|
||||
else:
|
||||
paths = [os.getcwd()] + os.environ["PATH"].split(os.pathsep)
|
||||
# look for VirtualBox wrapper in the current working directory and $PATH
|
||||
DEFAULT_VBOXWRAPPER_PATH = "vboxwrapper"
|
||||
# look for vboxmanage in the current working directory and $PATH
|
||||
DEFAULT_VBOXMANAGE_PATH = "vboxmanage"
|
||||
for path in paths:
|
||||
try:
|
||||
if "vboxwrapper" in os.listdir(path) and os.access(os.path.join(path, "vboxwrapper"), os.X_OK):
|
||||
DEFAULT_VBOXWRAPPER_PATH = os.path.join(path, "vboxwrapper")
|
||||
if "vboxmanage" in os.listdir(path) and os.access(os.path.join(path, "vboxmanage"), os.X_OK):
|
||||
DEFAULT_VBOXMANAGE_PATH = os.path.join(path, "vboxmanage")
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
VBOX_SETTINGS = {
|
||||
"vboxwrapper_path": DEFAULT_VBOXWRAPPER_PATH,
|
||||
"vboxmanage_path": DEFAULT_VBOXMANAGE_PATH,
|
||||
"vbox_user": "",
|
||||
"console_start_port_range": 3501,
|
||||
"console_end_port_range": 4000,
|
||||
"udp_start_port_range": 35001,
|
||||
@@ -47,10 +57,39 @@ VBOX_SETTINGS = {
|
||||
}
|
||||
|
||||
VBOX_SETTING_TYPES = {
|
||||
"vboxwrapper_path": str,
|
||||
"vboxmanage_path": str,
|
||||
"vbox_user": str,
|
||||
"console_start_port_range": int,
|
||||
"console_end_port_range": int,
|
||||
"udp_start_port_range": int,
|
||||
"udp_end_port_range": int,
|
||||
"use_local_server": bool,
|
||||
}
|
||||
|
||||
VBOX_VM_SETTINGS = {
|
||||
"vmname": "",
|
||||
"default_symbol": ":/symbols/vbox_guest.normal.svg",
|
||||
"hover_symbol": ":/symbols/vbox_guest.selected.svg",
|
||||
"category": Node.end_devices,
|
||||
"adapters": 1,
|
||||
"adapter_start_index": 0,
|
||||
"adapter_type": "Intel PRO/1000 MT Desktop (82540EM)",
|
||||
"headless": False,
|
||||
"enable_remote_console": False,
|
||||
"linked_base": False,
|
||||
"server": "local"
|
||||
}
|
||||
|
||||
VBOX_VM_SETTING_TYPES = {
|
||||
"vmname": str,
|
||||
"default_symbol": str,
|
||||
"hover_symbol": str,
|
||||
"category": int,
|
||||
"adapters": int,
|
||||
"adapter_start_index": int,
|
||||
"adapter_type": str,
|
||||
"headless": bool,
|
||||
"enable_remote_console": bool,
|
||||
"linked_base": bool,
|
||||
"server": str
|
||||
}
|
||||
|
||||
@@ -28,19 +28,19 @@
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiVboxWrapperPathLabel">
|
||||
<widget class="QLabel" name="uiVboxManagePathLabel">
|
||||
<property name="text">
|
||||
<string>Path to the VirtualBox wrapper (Linux/UNIX only):</string>
|
||||
<string>Path to VBoxManage:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiVboxWrapperPathLineEdit"/>
|
||||
<widget class="QLineEdit" name="uiVboxManagePathLineEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="uiVboxWrapperPathToolButton">
|
||||
<widget class="QToolButton" name="uiVboxManagePathToolButton">
|
||||
<property name="text">
|
||||
<string>&Browse...</string>
|
||||
</property>
|
||||
@@ -51,7 +51,17 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="uiVboxManageUserLabel">
|
||||
<property name="text">
|
||||
<string>Run VirtualBox as another user (GNS3 running as root):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLineEdit" name="uiVboxManageUserLineEdit"/>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<spacer name="spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_preferences_page.ui'
|
||||
#
|
||||
# Created: Sun Oct 19 11:35:54 2014
|
||||
# Created: Mon Jan 5 16:20:49 2015
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -36,21 +36,27 @@ class Ui_VirtualBoxPreferencesPageWidget(object):
|
||||
self.uiGeneralSettingsTabWidget.setObjectName(_fromUtf8("uiGeneralSettingsTabWidget"))
|
||||
self.gridLayout = QtGui.QGridLayout(self.uiGeneralSettingsTabWidget)
|
||||
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
|
||||
self.uiVboxWrapperPathLabel = QtGui.QLabel(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxWrapperPathLabel.setObjectName(_fromUtf8("uiVboxWrapperPathLabel"))
|
||||
self.gridLayout.addWidget(self.uiVboxWrapperPathLabel, 2, 0, 1, 1)
|
||||
self.uiVboxManagePathLabel = QtGui.QLabel(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxManagePathLabel.setObjectName(_fromUtf8("uiVboxManagePathLabel"))
|
||||
self.gridLayout.addWidget(self.uiVboxManagePathLabel, 2, 0, 1, 1)
|
||||
self.horizontalLayout_5 = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout_5.setObjectName(_fromUtf8("horizontalLayout_5"))
|
||||
self.uiVboxWrapperPathLineEdit = QtGui.QLineEdit(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxWrapperPathLineEdit.setObjectName(_fromUtf8("uiVboxWrapperPathLineEdit"))
|
||||
self.horizontalLayout_5.addWidget(self.uiVboxWrapperPathLineEdit)
|
||||
self.uiVboxWrapperPathToolButton = QtGui.QToolButton(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxWrapperPathToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiVboxWrapperPathToolButton.setObjectName(_fromUtf8("uiVboxWrapperPathToolButton"))
|
||||
self.horizontalLayout_5.addWidget(self.uiVboxWrapperPathToolButton)
|
||||
self.uiVboxManagePathLineEdit = QtGui.QLineEdit(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxManagePathLineEdit.setObjectName(_fromUtf8("uiVboxManagePathLineEdit"))
|
||||
self.horizontalLayout_5.addWidget(self.uiVboxManagePathLineEdit)
|
||||
self.uiVboxManagePathToolButton = QtGui.QToolButton(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxManagePathToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiVboxManagePathToolButton.setObjectName(_fromUtf8("uiVboxManagePathToolButton"))
|
||||
self.horizontalLayout_5.addWidget(self.uiVboxManagePathToolButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_5, 3, 0, 1, 2)
|
||||
self.uiVboxManageUserLabel = QtGui.QLabel(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxManageUserLabel.setObjectName(_fromUtf8("uiVboxManageUserLabel"))
|
||||
self.gridLayout.addWidget(self.uiVboxManageUserLabel, 5, 0, 1, 1)
|
||||
self.uiVboxManageUserLineEdit = QtGui.QLineEdit(self.uiGeneralSettingsTabWidget)
|
||||
self.uiVboxManageUserLineEdit.setObjectName(_fromUtf8("uiVboxManageUserLineEdit"))
|
||||
self.gridLayout.addWidget(self.uiVboxManageUserLineEdit, 6, 0, 1, 1)
|
||||
spacerItem = QtGui.QSpacerItem(390, 193, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem, 5, 0, 1, 2)
|
||||
self.gridLayout.addItem(spacerItem, 7, 0, 1, 2)
|
||||
self.uiTabWidget.addTab(self.uiGeneralSettingsTabWidget, _fromUtf8(""))
|
||||
self.uiServerSettingsTabWidget = QtGui.QWidget()
|
||||
self.uiServerSettingsTabWidget.setObjectName(_fromUtf8("uiServerSettingsTabWidget"))
|
||||
@@ -141,8 +147,9 @@ class Ui_VirtualBoxPreferencesPageWidget(object):
|
||||
|
||||
def retranslateUi(self, VirtualBoxPreferencesPageWidget):
|
||||
VirtualBoxPreferencesPageWidget.setWindowTitle(_translate("VirtualBoxPreferencesPageWidget", "VirtualBox", None))
|
||||
self.uiVboxWrapperPathLabel.setText(_translate("VirtualBoxPreferencesPageWidget", "Path to the VirtualBox wrapper (Linux/UNIX only):", None))
|
||||
self.uiVboxWrapperPathToolButton.setText(_translate("VirtualBoxPreferencesPageWidget", "&Browse...", None))
|
||||
self.uiVboxManagePathLabel.setText(_translate("VirtualBoxPreferencesPageWidget", "Path to VBoxManage:", None))
|
||||
self.uiVboxManagePathToolButton.setText(_translate("VirtualBoxPreferencesPageWidget", "&Browse...", None))
|
||||
self.uiVboxManageUserLabel.setText(_translate("VirtualBoxPreferencesPageWidget", "Run VirtualBox as another user (GNS3 running as root):", None))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.uiGeneralSettingsTabWidget), _translate("VirtualBoxPreferencesPageWidget", "General settings", None))
|
||||
self.uiUseLocalServercheckBox.setText(_translate("VirtualBoxPreferencesPageWidget", "Always use the local server", None))
|
||||
self.uiRemoteServersGroupBox.setTitle(_translate("VirtualBoxPreferencesPageWidget", "Remote servers", None))
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>514</width>
|
||||
<height>365</height>
|
||||
<width>509</width>
|
||||
<height>346</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -82,7 +82,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiBaseVMCheckBox">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use as a linked base VM (experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_vm_configuration_page.ui'
|
||||
#
|
||||
# Created: Wed Oct 22 22:07:00 2014
|
||||
# Created: Tue Dec 2 14:17:34 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -26,7 +26,7 @@ except AttributeError:
|
||||
class Ui_virtualBoxVMConfigPageWidget(object):
|
||||
def setupUi(self, virtualBoxVMConfigPageWidget):
|
||||
virtualBoxVMConfigPageWidget.setObjectName(_fromUtf8("virtualBoxVMConfigPageWidget"))
|
||||
virtualBoxVMConfigPageWidget.resize(514, 365)
|
||||
virtualBoxVMConfigPageWidget.resize(509, 346)
|
||||
self.verticalLayout = QtGui.QVBoxLayout(virtualBoxVMConfigPageWidget)
|
||||
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
|
||||
self.uiTabWidget = QtGui.QTabWidget(virtualBoxVMConfigPageWidget)
|
||||
@@ -66,8 +66,12 @@ class Ui_virtualBoxVMConfigPageWidget(object):
|
||||
self.uiHeadlessModeCheckBox.setChecked(False)
|
||||
self.uiHeadlessModeCheckBox.setObjectName(_fromUtf8("uiHeadlessModeCheckBox"))
|
||||
self.gridLayout.addWidget(self.uiHeadlessModeCheckBox, 4, 0, 1, 2)
|
||||
self.uiBaseVMCheckBox = QtGui.QCheckBox(self.tab)
|
||||
self.uiBaseVMCheckBox.setEnabled(True)
|
||||
self.uiBaseVMCheckBox.setObjectName(_fromUtf8("uiBaseVMCheckBox"))
|
||||
self.gridLayout.addWidget(self.uiBaseVMCheckBox, 5, 0, 1, 2)
|
||||
spacerItem = QtGui.QSpacerItem(20, 138, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem, 5, 1, 1, 1)
|
||||
self.gridLayout.addItem(spacerItem, 6, 1, 1, 1)
|
||||
self.uiTabWidget.addTab(self.tab, _fromUtf8(""))
|
||||
self.tab_2 = QtGui.QWidget()
|
||||
self.tab_2.setObjectName(_fromUtf8("tab_2"))
|
||||
@@ -117,6 +121,7 @@ class Ui_virtualBoxVMConfigPageWidget(object):
|
||||
self.uiConsolePortLabel.setText(_translate("virtualBoxVMConfigPageWidget", "Console port:", None))
|
||||
self.uiEnableConsoleCheckBox.setText(_translate("virtualBoxVMConfigPageWidget", "Enable remote console", None))
|
||||
self.uiHeadlessModeCheckBox.setText(_translate("virtualBoxVMConfigPageWidget", "Start VM in headless mode", None))
|
||||
self.uiBaseVMCheckBox.setText(_translate("virtualBoxVMConfigPageWidget", "Use as a linked base VM (experimental)", None))
|
||||
self.uiTabWidget.setTabText(self.uiTabWidget.indexOf(self.tab), _translate("virtualBoxVMConfigPageWidget", "General settings", None))
|
||||
self.uiAdaptersLabel.setText(_translate("virtualBoxVMConfigPageWidget", "Adapters:", None))
|
||||
self.uiAdapterStartIndexLabel.setText(_translate("virtualBoxVMConfigPageWidget", "Start at:", None))
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>12</pointsize>
|
||||
<pointsize>11</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_vm_preferences_page.ui'
|
||||
#
|
||||
# Created: Tue Oct 7 12:02:22 2014
|
||||
# Created: Wed Nov 19 18:57:20 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -57,7 +57,7 @@ class Ui_VirtualBoxVMPreferencesPageWidget(object):
|
||||
self.uiVirtualBoxVMsTreeWidget.setSizePolicy(sizePolicy)
|
||||
self.uiVirtualBoxVMsTreeWidget.setMaximumSize(QtCore.QSize(160, 16777215))
|
||||
font = QtGui.QFont()
|
||||
font.setPointSize(12)
|
||||
font.setPointSize(11)
|
||||
font.setBold(True)
|
||||
font.setWeight(75)
|
||||
self.uiVirtualBoxVMsTreeWidget.setFont(font)
|
||||
|
||||
@@ -126,6 +126,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="uiBaseVMCheckBox">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use as a linked base VM (experimental)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/virtualbox/ui/virtualbox_vm_wizard.ui'
|
||||
#
|
||||
# Created: Wed Oct 22 16:46:37 2014
|
||||
# Created: Tue Dec 2 14:17:34 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -82,6 +82,10 @@ class Ui_VirtualBoxVMWizard(object):
|
||||
self.uiVMListComboBox.setSizePolicy(sizePolicy)
|
||||
self.uiVMListComboBox.setObjectName(_fromUtf8("uiVMListComboBox"))
|
||||
self.gridLayout.addWidget(self.uiVMListComboBox, 0, 1, 1, 1)
|
||||
self.uiBaseVMCheckBox = QtGui.QCheckBox(self.uiVirtualBoxWizardPage)
|
||||
self.uiBaseVMCheckBox.setEnabled(True)
|
||||
self.uiBaseVMCheckBox.setObjectName(_fromUtf8("uiBaseVMCheckBox"))
|
||||
self.gridLayout.addWidget(self.uiBaseVMCheckBox, 1, 0, 1, 2)
|
||||
VirtualBoxVMWizard.addPage(self.uiVirtualBoxWizardPage)
|
||||
|
||||
self.retranslateUi(VirtualBoxVMWizard)
|
||||
@@ -100,4 +104,5 @@ class Ui_VirtualBoxVMWizard(object):
|
||||
self.uiVirtualBoxWizardPage.setTitle(_translate("VirtualBoxVMWizard", "VirtualBox Virtual Machine", None))
|
||||
self.uiVirtualBoxWizardPage.setSubTitle(_translate("VirtualBoxVMWizard", "Please choose a VirtualBox virtual machine from the list.", None))
|
||||
self.uiVMListLabel.setText(_translate("VirtualBoxVMWizard", "VM list:", None))
|
||||
self.uiBaseVMCheckBox.setText(_translate("VirtualBoxVMWizard", "Use as a linked base VM (experimental)", None))
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ VirtualBox VM implementation.
|
||||
from gns3.node import Node
|
||||
from gns3.ports.port import Port
|
||||
from gns3.ports.ethernet_port import EthernetPort
|
||||
from .settings import VBOX_VM_SETTINGS
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -40,6 +41,7 @@ class VirtualBoxVM(Node):
|
||||
|
||||
log.info("VirtualBox VM instance is being created")
|
||||
self._vbox_id = None
|
||||
self._linked_clone = False
|
||||
self._defaults = {}
|
||||
self._inital_settings = None
|
||||
self._export_directory = None
|
||||
@@ -49,11 +51,11 @@ class VirtualBoxVM(Node):
|
||||
self._settings = {"name": "",
|
||||
"vmname": "",
|
||||
"console": None,
|
||||
"adapters": 2,
|
||||
"adapter_start_index": 0,
|
||||
"adapter_type": "Automatic",
|
||||
"headless": False,
|
||||
"enable_console": False}
|
||||
"adapters": VBOX_VM_SETTINGS["adapters"],
|
||||
"adapter_start_index": VBOX_VM_SETTINGS["adapter_start_index"],
|
||||
"adapter_type": VBOX_VM_SETTINGS["adapter_type"],
|
||||
"headless": VBOX_VM_SETTINGS["headless"],
|
||||
"enable_remote_console": VBOX_VM_SETTINGS["enable_remote_console"]}
|
||||
|
||||
self._addAdapters(2)
|
||||
|
||||
@@ -71,31 +73,39 @@ class VirtualBoxVM(Node):
|
||||
if port_number < self._settings["adapter_start_index"]:
|
||||
continue
|
||||
port_name = EthernetPort.longNameType() + str(port_number)
|
||||
short_name = EthernetPort.shortNameType() + str(port_number)
|
||||
new_port = EthernetPort(port_name)
|
||||
new_port.setShortName(short_name)
|
||||
new_port.setPortNumber(port_number)
|
||||
new_port.setPacketCaptureSupported(True)
|
||||
self._ports.append(new_port)
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
|
||||
def setup(self, vmname, name=None, console=None, vbox_id=None, initial_settings={}):
|
||||
def setup(self, vmname, name=None, console=None, vbox_id=None, linked_clone=False, initial_settings={}):
|
||||
"""
|
||||
Setups this VirtualBox VM.
|
||||
|
||||
:param name: optional name
|
||||
"""
|
||||
|
||||
self._linked_clone = linked_clone
|
||||
|
||||
# let's create a unique name if none has been chosen
|
||||
if not name:
|
||||
name = vmname
|
||||
self.setName(name)
|
||||
#name = self.allocateName("VBOX")
|
||||
if linked_clone:
|
||||
name = self.allocateName(vmname + "-")
|
||||
else:
|
||||
name = vmname
|
||||
self.setName(name)
|
||||
|
||||
if not name:
|
||||
self.error_signal.emit(self.id(), "could not allocate a name for this VirtualBox VM")
|
||||
return
|
||||
|
||||
params = {"name": name,
|
||||
"vmname": vmname}
|
||||
"vmname": vmname,
|
||||
"linked_clone": linked_clone}
|
||||
|
||||
if console:
|
||||
params["console"] = self._settings["console"] = console
|
||||
|
||||
@@ -179,9 +189,13 @@ class VirtualBoxVM(Node):
|
||||
:param new_settings: settings dictionary
|
||||
"""
|
||||
|
||||
if "name" in new_settings and new_settings["name"] != self.name() and self.hasAllocatedName(new_settings["name"]):
|
||||
self.error_signal.emit(self.id(), 'Name "{}" is already used by another node'.format(new_settings["name"]))
|
||||
return
|
||||
if "name" in new_settings and new_settings["name"] != self.name():
|
||||
if self.hasAllocatedName(new_settings["name"]):
|
||||
self.error_signal.emit(self.id(), 'Name "{}" is already used by another node'.format(new_settings["name"]))
|
||||
return
|
||||
elif self._linked_clone:
|
||||
# forces the update of the VM name in VirtualBox.
|
||||
new_settings["vmname"] = new_settings["name"]
|
||||
|
||||
params = {"id": self._vbox_id}
|
||||
for name, value in new_settings.items():
|
||||
@@ -469,7 +483,7 @@ class VirtualBoxVM(Node):
|
||||
try:
|
||||
port.startPacketCapture(result["capture_file_path"])
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}".format(e))
|
||||
self.error_signal.emit(self.id(), "could not start the packet capture reader: {}: {}".format(e, e.filename))
|
||||
self.updated_signal.emit()
|
||||
break
|
||||
|
||||
@@ -520,11 +534,13 @@ class VirtualBoxVM(Node):
|
||||
|
||||
info = """VirtualBox VM {name} is {state}
|
||||
Node ID is {id}, server's VirtualBox VM ID is {vbox_id}
|
||||
VirtualBox name is "{vmname}"
|
||||
console is on port {console}
|
||||
""".format(name=self.name(),
|
||||
id=self.id(),
|
||||
vbox_id=self._vbox_id,
|
||||
state=state,
|
||||
vmname=self._settings["vmname"],
|
||||
console=self._settings["console"])
|
||||
|
||||
port_info = ""
|
||||
@@ -547,6 +563,7 @@ class VirtualBoxVM(Node):
|
||||
|
||||
vbox_vm = {"id": self.id(),
|
||||
"vbox_id": self._vbox_id,
|
||||
"linked_clone": self._linked_clone,
|
||||
"type": self.__class__.__name__,
|
||||
"description": str(self),
|
||||
"properties": {},
|
||||
@@ -575,6 +592,7 @@ class VirtualBoxVM(Node):
|
||||
|
||||
self.node_info = node_info
|
||||
vbox_id = node_info.get("vbox_id")
|
||||
linked_clone = node_info.get("linked_clone", False)
|
||||
settings = node_info["properties"]
|
||||
name = settings.pop("name")
|
||||
vmname = settings.pop("vmname")
|
||||
@@ -584,7 +602,7 @@ class VirtualBoxVM(Node):
|
||||
self._loading = True
|
||||
log.info("VirtualBox VM {} is loading".format(name))
|
||||
self.setName(name)
|
||||
self.setup(vmname, name, console, vbox_id, settings)
|
||||
self.setup(vmname, name, console, vbox_id, linked_clone, settings)
|
||||
|
||||
def _updatePortSettings(self):
|
||||
"""
|
||||
@@ -643,7 +661,7 @@ class VirtualBoxVM(Node):
|
||||
:return: boolean
|
||||
"""
|
||||
|
||||
if self._settings["enable_console"]:
|
||||
if self._settings["enable_remote_console"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -20,12 +20,23 @@ VPCS module implementation.
|
||||
"""
|
||||
|
||||
import os
|
||||
from gns3.qt import QtCore, QtGui
|
||||
import subprocess
|
||||
import sys
|
||||
import signal
|
||||
import socket
|
||||
import re
|
||||
import pkg_resources
|
||||
|
||||
from gns3.qt import QtCore
|
||||
from gns3.servers import Servers
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from gns3.utils.get_default_base_config import get_default_base_config
|
||||
|
||||
from ..module import Module
|
||||
from ..module_error import ModuleError
|
||||
from .vpcs_device import VPCSDevice
|
||||
from .settings import VPCS_SETTINGS, VPCS_SETTING_TYPES
|
||||
from ...settings import ENABLE_CLOUD
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -44,6 +55,9 @@ class VPCS(Module):
|
||||
self._servers = []
|
||||
self._working_dir = ""
|
||||
|
||||
self._vpcs_multi_host_process = None
|
||||
self._vpcs_multi_host_port = 0
|
||||
|
||||
# load the settings
|
||||
self._loadSettings()
|
||||
|
||||
@@ -59,6 +73,9 @@ class VPCS(Module):
|
||||
self._settings[name] = settings.value(name, value, type=VPCS_SETTING_TYPES[name])
|
||||
settings.endGroup()
|
||||
|
||||
if not self._settings["base_script_file"]:
|
||||
self._settings["base_script_file"] = get_default_base_config(get_resource(os.path.join("configs", "vpcs_base_config.txt")))
|
||||
|
||||
def _saveSettings(self):
|
||||
"""
|
||||
Saves the settings to the persistent settings file.
|
||||
@@ -171,6 +188,8 @@ class VPCS(Module):
|
||||
if server.isLocal():
|
||||
params.update({"working_dir": self._working_dir})
|
||||
else:
|
||||
if "path" in params:
|
||||
del params["path"] # do not send VPCS path to remote servers
|
||||
project_name = os.path.basename(self._working_dir)
|
||||
if project_name.endswith("-files"):
|
||||
project_name = project_name[:-6]
|
||||
@@ -252,6 +271,7 @@ class VPCS(Module):
|
||||
"""
|
||||
|
||||
log.info("vpcs module reset")
|
||||
self.stopMultiHostVPCS()
|
||||
for server in self._servers:
|
||||
if server.connected():
|
||||
server.send_notification("vpcs.reset")
|
||||
@@ -281,8 +301,8 @@ class VPCS(Module):
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "exportConfig") and node.initialized():
|
||||
node.exportConfig(directory)
|
||||
if node.initialized():
|
||||
node.exportConfigToDirectory(directory)
|
||||
|
||||
def importConfigs(self, directory):
|
||||
"""
|
||||
@@ -292,8 +312,81 @@ class VPCS(Module):
|
||||
"""
|
||||
|
||||
for node in self._nodes:
|
||||
if hasattr(node, "importConfig") and node.initialized():
|
||||
node.importConfig(directory)
|
||||
if node.initialized():
|
||||
node.importConfigFromDirectory(directory)
|
||||
|
||||
def _check_vpcs_version(self, working_dir):
|
||||
"""
|
||||
Checks if the VPCS executable version is >= 0.5b1.
|
||||
"""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output([self._settings["path"], "-v"], cwd=working_dir)
|
||||
match = re.search("Welcome to Virtual PC Simulator, version ([0-9a-z\.]+)", output.decode("utf-8"))
|
||||
if match:
|
||||
version = match.group(1)
|
||||
if pkg_resources.parse_version(version) < pkg_resources.parse_version("0.5b1"):
|
||||
raise ModuleError("VPCS executable version must be >= 0.5b1")
|
||||
else:
|
||||
raise ModuleError("Could not determine the VPCS version for {}".format(self._settings["path"]))
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise ModuleError("Error while looking for the VPCS version: {}".format(e))
|
||||
|
||||
def startMultiHostVPCS(self, working_dir):
|
||||
"""
|
||||
Starts a VPCS process for multi-host support.
|
||||
|
||||
:param working_dir: VPCS multi-host working directory
|
||||
:return: VPCS listening port
|
||||
"""
|
||||
|
||||
if self._vpcs_multi_host_process and self._vpcs_multi_host_process.poll() is None:
|
||||
return self._vpcs_multi_host_port
|
||||
|
||||
if not self._settings["path"]:
|
||||
raise ModuleError("No path to a VPCS executable has been set")
|
||||
|
||||
if not os.path.isfile(self._settings["path"]):
|
||||
raise ModuleError("VPCS program '{}' is not accessible".format(self._settings["path"]))
|
||||
|
||||
if not os.access(self._settings["path"], os.X_OK):
|
||||
raise ModuleError("VPCS program '{}' is not executable".format(self._settings["path"]))
|
||||
|
||||
self._check_vpcs_version(working_dir)
|
||||
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
self._vpcs_multi_host_port = sock.getsockname()[1]
|
||||
except OSError as e:
|
||||
raise ModuleError("Cannot get a free port: {}".format(e))
|
||||
|
||||
flags = 0
|
||||
if sys.platform.startswith("win32"):
|
||||
flags = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
try:
|
||||
vpcs_command = [self._settings["path"], "-p", str(self._vpcs_multi_host_port), "-F"]
|
||||
self._vpcs_multi_host_process = subprocess.Popen(vpcs_command, cwd=working_dir, creationflags=flags)
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise ModuleError("Could not start VPCS {}".format(e))
|
||||
|
||||
return self._vpcs_multi_host_port
|
||||
|
||||
def stopMultiHostVPCS(self):
|
||||
"""
|
||||
Stops the VPCS process for multi-host support.
|
||||
"""
|
||||
|
||||
if self._vpcs_multi_host_process and self._vpcs_multi_host_process.poll() is None:
|
||||
log.info("stopping VPCS multi-host instance PID={}".format(self._vpcs_multi_host_process.pid))
|
||||
if sys.platform.startswith("win32"):
|
||||
self._vpcs_multi_host_process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
self._vpcs_multi_host_process.terminate()
|
||||
|
||||
self._vpcs_multi_host_process.wait()
|
||||
|
||||
self._vpcs_multi_host_process = None
|
||||
|
||||
@staticmethod
|
||||
def getNodeClass(name):
|
||||
@@ -340,6 +433,15 @@ class VPCS(Module):
|
||||
"default_symbol": node_class.defaultSymbol(),
|
||||
"hover_symbol": node_class.hoverSymbol()}
|
||||
)
|
||||
if ENABLE_CLOUD:
|
||||
nodes.append(
|
||||
{"class": node_class.__name__,
|
||||
"name": node_class.symbolName() + " (cloud)",
|
||||
"server": "cloud",
|
||||
"categories": node_class.categories(),
|
||||
"default_symbol": node_class.defaultSymbol(),
|
||||
"hover_symbol": node_class.hoverSymbol()}
|
||||
)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -20,9 +20,9 @@ Configuration page for VPCS devices.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
from gns3.qt import QtGui
|
||||
from gns3.utils.get_resource import get_resource
|
||||
from ..ui.vpcs_device_configuration_page_ui import Ui_VPCSDeviceConfigPageWidget
|
||||
|
||||
|
||||
@@ -35,27 +35,24 @@ class VPCSDeviceConfigurationPage(QtGui.QWidget, Ui_VPCSDeviceConfigPageWidget):
|
||||
|
||||
QtGui.QWidget.__init__(self)
|
||||
self.setupUi(self)
|
||||
self.uiScriptFileToolButton.clicked.connect(self._scriptFileBrowserSlot)
|
||||
#self.uiScriptFileToolButton.clicked.connect(self._scriptFileBrowserSlot)
|
||||
|
||||
def _scriptFileBrowserSlot(self):
|
||||
"""
|
||||
Slot to open a file browser and select a script-file file.
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
|
||||
if not path:
|
||||
return
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
QtGui.QMessageBox.critical(self, "Startup configuration", "Cannot read {}".format(path))
|
||||
return
|
||||
|
||||
self.uiScriptFileLineEdit.clear()
|
||||
self.uiScriptFileLineEdit.setText(path)
|
||||
# def _scriptFileBrowserSlot(self):
|
||||
# """
|
||||
# Slot to open a file browser and select a script-file file.
|
||||
# """
|
||||
#
|
||||
# config_dir = get_resource("configs")
|
||||
# path = QtGui.QFileDialog.getOpenFileName(self, "Select a startup configuration", config_dir)
|
||||
# if not path:
|
||||
# return
|
||||
#
|
||||
# if not os.access(path, os.R_OK):
|
||||
# QtGui.QMessageBox.critical(self, "Startup configuration", "Cannot read {}".format(path))
|
||||
# return
|
||||
#
|
||||
# self.uiScriptFileLineEdit.clear()
|
||||
# self.uiScriptFileLineEdit.setText(path)
|
||||
|
||||
def loadSettings(self, settings, node, group=False):
|
||||
"""
|
||||
@@ -71,16 +68,16 @@ class VPCSDeviceConfigurationPage(QtGui.QWidget, Ui_VPCSDeviceConfigPageWidget):
|
||||
self.uiConsolePortSpinBox.setValue(settings["console"])
|
||||
|
||||
# load the script-file
|
||||
self.uiScriptFileLineEdit.setText(settings["script_file"])
|
||||
# self.uiScriptFileLineEdit.setText(settings["script_file"])
|
||||
|
||||
else:
|
||||
self.uiNameLabel.hide()
|
||||
self.uiNameLineEdit.hide()
|
||||
self.uiConsolePortLabel.hide()
|
||||
self.uiConsolePortSpinBox.hide()
|
||||
self.uiScriptFileLabel.hide()
|
||||
self.uiScriptFileLineEdit.hide()
|
||||
self.uiScriptFileToolButton.hide()
|
||||
#self.uiScriptFileLabel.hide()
|
||||
#self.uiScriptFileLineEdit.hide()
|
||||
#self.uiScriptFileToolButton.hide()
|
||||
|
||||
def saveSettings(self, settings, node, group=False):
|
||||
"""
|
||||
@@ -104,12 +101,12 @@ class VPCSDeviceConfigurationPage(QtGui.QWidget, Ui_VPCSDeviceConfigPageWidget):
|
||||
|
||||
settings["console"] = self.uiConsolePortSpinBox.value()
|
||||
|
||||
script_file = self.uiScriptFileLineEdit.text()
|
||||
if script_file != settings["script_file"]:
|
||||
if os.access(script_file, os.R_OK):
|
||||
settings["script_file"] = script_file
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Script-file", "Cannot read the script-file file")
|
||||
#script_file = self.uiScriptFileLineEdit.text()
|
||||
#if script_file != settings["script_file"]:
|
||||
# if os.access(script_file, os.R_OK):
|
||||
# settings["script_file"] = script_file
|
||||
# else:
|
||||
# QtGui.QMessageBox.critical(self, "Script-file", "Cannot read the script-file file")
|
||||
else:
|
||||
del settings["name"]
|
||||
del settings["console"]
|
||||
|
||||
@@ -21,9 +21,10 @@ Configuration page for VPCS preferences.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
from gns3.qt import QtGui
|
||||
|
||||
from gns3.qt import QtCore, QtGui
|
||||
from gns3.servers import Servers
|
||||
|
||||
from .. import VPCS
|
||||
from ..ui.vpcs_preferences_page_ui import Ui_VPCSPreferencesPageWidget
|
||||
from ..settings import VPCS_SETTINGS
|
||||
@@ -72,10 +73,7 @@ class VPCSPreferencesPage(QtGui.QWidget, Ui_VPCSPreferencesPageWidget):
|
||||
Slot to open a file browser and select a base script file for VPCS
|
||||
"""
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
config_dir = "configs"
|
||||
else:
|
||||
config_dir = pkg_resources.resource_filename("gns3", "configs")
|
||||
config_dir = os.path.join(os.path.dirname(QtCore.QSettings().fileName()), "base_configs")
|
||||
path = QtGui.QFileDialog.getOpenFileName(self, "Select a script file", config_dir)
|
||||
if not path:
|
||||
return
|
||||
@@ -122,14 +120,6 @@ class VPCSPreferencesPage(QtGui.QWidget, Ui_VPCSPreferencesPageWidget):
|
||||
self.uiUDPStartPortSpinBox.setValue(settings["udp_start_port_range"])
|
||||
self.uiUDPEndPortSpinBox.setValue(settings["udp_end_port_range"])
|
||||
|
||||
if not self.uiScriptFileEdit.text():
|
||||
resource_name = "configs/vpcs_base_config.txt"
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
self.uiScriptFileEdit.setText(os.path.normpath(resource_name))
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
vpcs_base_config_path = pkg_resources.resource_filename("gns3", resource_name)
|
||||
self.uiScriptFileEdit.setText(os.path.normpath(vpcs_base_config_path))
|
||||
|
||||
def _updateRemoteServersSlot(self):
|
||||
"""
|
||||
Adds/Updates the available remote servers.
|
||||
|
||||
@@ -25,44 +25,20 @@
|
||||
<widget class="QLineEdit" name="uiNameLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="uiScriptFileLabel">
|
||||
<property name="text">
|
||||
<string>Script-File:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="uiScriptFileLineEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="uiScriptFileToolButton">
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextOnly</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="uiConsolePortLabel">
|
||||
<property name="text">
|
||||
<string>Console port:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="uiConsolePortSpinBox">
|
||||
<property name="maximum">
|
||||
<number>65535</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="2" column="1">
|
||||
<spacer name="spacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file '/home/jbbowen/Desktop/Toptal/github/gns3-gui/gns3/modules/vpcs/ui/vpcs_device_configuration_page.ui'
|
||||
# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/modules/vpcs/ui/vpcs_device_configuration_page.ui'
|
||||
#
|
||||
# Created: Tue May 13 14:41:22 2014
|
||||
# Created: Wed Dec 24 17:44:48 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
@@ -35,28 +35,15 @@ class Ui_VPCSDeviceConfigPageWidget(object):
|
||||
self.uiNameLineEdit = QtGui.QLineEdit(VPCSDeviceConfigPageWidget)
|
||||
self.uiNameLineEdit.setObjectName(_fromUtf8("uiNameLineEdit"))
|
||||
self.gridLayout.addWidget(self.uiNameLineEdit, 0, 1, 1, 1)
|
||||
self.uiScriptFileLabel = QtGui.QLabel(VPCSDeviceConfigPageWidget)
|
||||
self.uiScriptFileLabel.setObjectName(_fromUtf8("uiScriptFileLabel"))
|
||||
self.gridLayout.addWidget(self.uiScriptFileLabel, 1, 0, 1, 1)
|
||||
self.horizontalLayout_4 = QtGui.QHBoxLayout()
|
||||
self.horizontalLayout_4.setObjectName(_fromUtf8("horizontalLayout_4"))
|
||||
self.uiScriptFileLineEdit = QtGui.QLineEdit(VPCSDeviceConfigPageWidget)
|
||||
self.uiScriptFileLineEdit.setObjectName(_fromUtf8("uiScriptFileLineEdit"))
|
||||
self.horizontalLayout_4.addWidget(self.uiScriptFileLineEdit)
|
||||
self.uiScriptFileToolButton = QtGui.QToolButton(VPCSDeviceConfigPageWidget)
|
||||
self.uiScriptFileToolButton.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
|
||||
self.uiScriptFileToolButton.setObjectName(_fromUtf8("uiScriptFileToolButton"))
|
||||
self.horizontalLayout_4.addWidget(self.uiScriptFileToolButton)
|
||||
self.gridLayout.addLayout(self.horizontalLayout_4, 1, 1, 1, 1)
|
||||
self.uiConsolePortLabel = QtGui.QLabel(VPCSDeviceConfigPageWidget)
|
||||
self.uiConsolePortLabel.setObjectName(_fromUtf8("uiConsolePortLabel"))
|
||||
self.gridLayout.addWidget(self.uiConsolePortLabel, 2, 0, 1, 1)
|
||||
self.gridLayout.addWidget(self.uiConsolePortLabel, 1, 0, 1, 1)
|
||||
self.uiConsolePortSpinBox = QtGui.QSpinBox(VPCSDeviceConfigPageWidget)
|
||||
self.uiConsolePortSpinBox.setMaximum(65535)
|
||||
self.uiConsolePortSpinBox.setObjectName(_fromUtf8("uiConsolePortSpinBox"))
|
||||
self.gridLayout.addWidget(self.uiConsolePortSpinBox, 2, 1, 1, 1)
|
||||
self.gridLayout.addWidget(self.uiConsolePortSpinBox, 1, 1, 1, 1)
|
||||
spacerItem = QtGui.QSpacerItem(263, 212, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
self.gridLayout.addItem(spacerItem, 3, 1, 1, 1)
|
||||
self.gridLayout.addItem(spacerItem, 2, 1, 1, 1)
|
||||
|
||||
self.retranslateUi(VPCSDeviceConfigPageWidget)
|
||||
QtCore.QMetaObject.connectSlotsByName(VPCSDeviceConfigPageWidget)
|
||||
@@ -64,7 +51,5 @@ class Ui_VPCSDeviceConfigPageWidget(object):
|
||||
def retranslateUi(self, VPCSDeviceConfigPageWidget):
|
||||
VPCSDeviceConfigPageWidget.setWindowTitle(_translate("VPCSDeviceConfigPageWidget", "VPCS device configuration", None))
|
||||
self.uiNameLabel.setText(_translate("VPCSDeviceConfigPageWidget", "Name:", None))
|
||||
self.uiScriptFileLabel.setText(_translate("VPCSDeviceConfigPageWidget", "Script-File:", None))
|
||||
self.uiScriptFileToolButton.setText(_translate("VPCSDeviceConfigPageWidget", "...", None))
|
||||
self.uiConsolePortLabel.setText(_translate("VPCSDeviceConfigPageWidget", "Console port:", None))
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ class VPCSDevice(Node):
|
||||
"console": None}
|
||||
|
||||
port_name = EthernetPort.longNameType() + str(0)
|
||||
short_name = EthernetPort.shortNameType() + str(0)
|
||||
port = EthernetPort(port_name)
|
||||
port.setShortName(short_name)
|
||||
port.setPortNumber(0)
|
||||
self._ports.append(port)
|
||||
log.debug("port {} has been added".format(port_name))
|
||||
@@ -505,7 +507,39 @@ class VPCSDevice(Node):
|
||||
self._inital_settings = None
|
||||
self._loading = False
|
||||
|
||||
def exportConfig(self, directory):
|
||||
def exportConfig(self, config_export_path):
|
||||
"""
|
||||
Exports the script file.
|
||||
|
||||
:param config_export_path: export path for the script file
|
||||
"""
|
||||
|
||||
self._config_export_path = config_export_path
|
||||
self._server.send_message("vpcs.export_config", {"id": self._vpcs_id}, self._exportConfigCallback)
|
||||
|
||||
def _exportConfigCallback(self, result, error=False):
|
||||
"""
|
||||
Callback for exportConfig.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
"""
|
||||
|
||||
if error:
|
||||
log.error("error while exporting {} configs: {}".format(self.name(), result["message"]))
|
||||
self.server_error_signal.emit(self.id(), result["code"], result["message"])
|
||||
else:
|
||||
|
||||
if "script_file_base64" in result and self._config_export_path:
|
||||
config = base64.decodebytes(result["script_file_base64"].encode("utf-8"))
|
||||
try:
|
||||
with open(self._config_export_path, "wb") as f:
|
||||
log.info("saving {} script file to {}".format(self.name(), self._config_export_path))
|
||||
f.write(config)
|
||||
except OSError as e:
|
||||
self.error_signal.emit(self.id(), "could not export the script file to {}: {}".format(self._config_export_path, e))
|
||||
|
||||
def exportConfigToDirectory(self, directory):
|
||||
"""
|
||||
Exports the script-file to a directory.
|
||||
|
||||
@@ -513,11 +547,11 @@ class VPCSDevice(Node):
|
||||
"""
|
||||
|
||||
self._export_directory = directory
|
||||
self._server.send_message("vpcs.export_config", {"id": self._vpcs_id}, self._exportConfigCallback)
|
||||
self._server.send_message("vpcs.export_config", {"id": self._vpcs_id}, self._exportConfigToDirectoryCallback)
|
||||
|
||||
def _exportConfigCallback(self, result, error=False):
|
||||
def _exportConfigToDirectoryCallback(self, result, error=False):
|
||||
"""
|
||||
Callback for exportConfigs.
|
||||
Callback for exportConfigToDirectory.
|
||||
|
||||
:param result: server response
|
||||
:param error: indicates an error (boolean)
|
||||
@@ -540,7 +574,17 @@ class VPCSDevice(Node):
|
||||
|
||||
self._export_directory = None
|
||||
|
||||
def importConfig(self, directory):
|
||||
def importConfig(self, path):
|
||||
"""
|
||||
Imports a script-file.
|
||||
|
||||
:param path: path to the script file
|
||||
"""
|
||||
|
||||
new_settings = {"script_file": path}
|
||||
self.update(new_settings)
|
||||
|
||||
def importConfigFromDirectory(self, directory):
|
||||
"""
|
||||
Imports an initial-config from a directory.
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pkg_resources
|
||||
|
||||
from .qt import QtGui, QtCore, QtWebKit
|
||||
from .ui.news_dock_widget_ui import Ui_NewsDockWidget
|
||||
from .utils.get_resource import get_resource
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -36,6 +36,8 @@ class NewsDockWidget(QtGui.QDockWidget, Ui_NewsDockWidget):
|
||||
QtGui.QDockWidget.__init__(self, parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self._visible = True
|
||||
self.visibilityChanged.connect(self._visibilityChangedSlot)
|
||||
self.uiWebView.page().setLinkDelegationPolicy(QtWebKit.QWebPage.DelegateAllLinks)
|
||||
self.uiWebView.linkClicked.connect(self._urlClickedSlot)
|
||||
self.uiWebView.loadFinished.connect(self._loadFinishedSlot)
|
||||
@@ -46,7 +48,32 @@ class NewsDockWidget(QtGui.QDockWidget, Ui_NewsDockWidget):
|
||||
self._timer.timeout.connect(self._loadFinishedSlot)
|
||||
self._timer.setSingleShot(True)
|
||||
self._timer.start(5000)
|
||||
self.uiWebView.load(QtCore.QUrl("http://as.gns3.com/software/docked.html"))
|
||||
if parent.settings()["default_local_news"]:
|
||||
self._loadFinishedSlot()
|
||||
else:
|
||||
self.uiWebView.load(QtCore.QUrl("http://as.gns3.com/software/docked_200x200.html"))
|
||||
|
||||
def _visibilityChangedSlot(self, visible):
|
||||
"""
|
||||
Slot for visibility changed signal.
|
||||
|
||||
:param visible: either the dock is visible or not
|
||||
"""
|
||||
|
||||
self._visible = visible
|
||||
|
||||
def isVisible(self):
|
||||
|
||||
return self._visible
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""
|
||||
You really cannot close that dock (using ATL+F4...)
|
||||
|
||||
:param event: closeEvent instance.
|
||||
"""
|
||||
|
||||
event.ignore()
|
||||
|
||||
def _refreshSlot(self):
|
||||
"""
|
||||
@@ -76,14 +103,12 @@ class NewsDockWidget(QtGui.QDockWidget, Ui_NewsDockWidget):
|
||||
self._timer.stop()
|
||||
self._timer.timeout.disconnect()
|
||||
if result is False:
|
||||
# load a local resource if the page is not available
|
||||
resource_name = os.path.join("static", "gns3_jungle.html")
|
||||
gns3_jungle = None
|
||||
if hasattr(sys, "frozen") and os.path.isfile(resource_name):
|
||||
gns3_jungle = os.path.normpath(resource_name)
|
||||
elif pkg_resources.resource_exists("gns3", resource_name):
|
||||
gns3_jungle_page = pkg_resources.resource_filename("gns3", resource_name)
|
||||
gns3_jungle = os.path.normpath(gns3_jungle_page)
|
||||
if gns3_jungle:
|
||||
self.uiWebView.load(QtCore.QUrl("file://{}".format(gns3_jungle)))
|
||||
self._refresh_timer.stop()
|
||||
# load a local resource if the page is not available
|
||||
gns3_jungle = get_resource(os.path.join("static", "gns3_jungle.html"))
|
||||
if gns3_jungle and not (sys.platform.startswith("win") and not sys.maxsize > 2 ** 32):
|
||||
# do not show the page on Windows 32-bit (crash when no Internet connection)
|
||||
url = QtCore.QUrl.fromLocalFile(gns3_jungle)
|
||||
self.uiWebView.load(url)
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
@@ -20,7 +20,7 @@ Nodes view that list all the available nodes to be dragged and dropped on the QG
|
||||
"""
|
||||
|
||||
import pickle
|
||||
from .qt import QtCore, QtGui
|
||||
from .qt import QtCore, QtGui, QtSvg
|
||||
from .modules import MODULES
|
||||
|
||||
|
||||
@@ -38,22 +38,32 @@ class NodesView(QtGui.QTreeWidget):
|
||||
# enables the possibility to drag items.
|
||||
self.setDragEnabled(True)
|
||||
|
||||
def populateNodesView(self, category):
|
||||
def populateNodesView(self, category, project_type):
|
||||
"""
|
||||
Populates the nodes view with the device list of the specified
|
||||
category (None = all devices).
|
||||
|
||||
:param category: category of device to list
|
||||
:param project_type: Filter devices for this type of project (cloud|local)
|
||||
"""
|
||||
|
||||
for module in MODULES:
|
||||
for node in module.instance().nodes():
|
||||
if category is not None and category not in node["categories"]:
|
||||
continue
|
||||
server_type = node.get("server", None)
|
||||
if server_type is not None and server_type != project_type:
|
||||
continue
|
||||
item = QtGui.QTreeWidgetItem(self)
|
||||
item.setIcon(0, QtGui.QIcon(node["default_symbol"]))
|
||||
item.setText(0, node["name"])
|
||||
item.setData(0, QtCore.Qt.UserRole, node)
|
||||
svg_renderer = QtSvg.QSvgRenderer(node["default_symbol"])
|
||||
image = QtGui.QImage(32, 32, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
svg_renderer.render(QtGui.QPainter(image))
|
||||
icon = QtGui.QIcon(QtGui.QPixmap.fromImage(image))
|
||||
item.setIcon(0, icon)
|
||||
|
||||
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ from ..ui.cloud_preferences_page_ui import Ui_CloudPreferencesPageWidget
|
||||
from ..settings import CLOUD_PROVIDERS
|
||||
from ..utils import import_from_string
|
||||
|
||||
from PyQt4 import QtGui
|
||||
from PyQt4 import Qt
|
||||
from ..qt import QtCore, QtGui
|
||||
|
||||
|
||||
class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
@@ -58,7 +57,7 @@ class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
return self.uiRememberAPIKeyRadioButton.isChecked()
|
||||
|
||||
def _terms_accepted(self):
|
||||
return self.uiTermsCheckBox.checkState() == Qt.Qt.Checked
|
||||
return self.uiTermsCheckBox.checkState() == QtCore.Qt.Checked
|
||||
|
||||
def _validate(self):
|
||||
"""
|
||||
@@ -105,11 +104,10 @@ class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
default_image = self.settings['default_image']
|
||||
default_flavor = self.settings['default_flavor']
|
||||
new_instance_flavor = self.settings['new_instance_flavor']
|
||||
gns3_ias_url = self.settings['gns3_ias_url']
|
||||
|
||||
# instance a provider controller and try to use it
|
||||
try:
|
||||
provider = self.provider_controllers[provider_id](username, apikey, gns3_ias_url)
|
||||
provider = self.provider_controllers[provider_id](username, apikey)
|
||||
if provider.authenticate():
|
||||
provider.set_region(region)
|
||||
# fill region combo box
|
||||
@@ -121,11 +119,9 @@ class CloudPreferencesPage(QtGui.QWidget, Ui_CloudPreferencesPageWidget):
|
||||
self.uiRegionComboBox.addItem(api_region_names[0])
|
||||
self.region_index_id.append(api_libcloud_names[0])
|
||||
# fill image template list
|
||||
self.image_index_id = [""]
|
||||
self.uiImageTemplateComboBox.addItem("Select default template...")
|
||||
for image_id, image_name in provider.list_images().items():
|
||||
self.uiImageTemplateComboBox.addItem(image_name)
|
||||
self.image_index_id.append(image_id)
|
||||
self.image_index_id = ["cc6e0096-84f9-4beb-a21e-d80a11a769d8"]
|
||||
self.uiImageTemplateComboBox.addItem("Ubuntu 14.04")
|
||||
self.uiImageTemplateComboBox.setCurrentIndex(0)
|
||||
# fill flavor comboboxes
|
||||
for id, name in provider.list_flavors().items():
|
||||
self.uiInstanceFlavorComboBox.addItem(name)
|
||||
|
||||
@@ -23,7 +23,7 @@ import os
|
||||
import shutil
|
||||
from gns3.qt import QtGui, QtCore
|
||||
from ..ui.general_preferences_page_ui import Ui_GeneralPreferencesPageWidget
|
||||
from ..settings import GRAPHICS_VIEW_SETTINGS, GENERAL_SETTINGS, PRECONFIGURED_TELNET_CONSOLE_COMMANDS, PRECONFIGURED_SERIAL_CONSOLE_COMMANDS
|
||||
from ..settings import GRAPHICS_VIEW_SETTINGS, GENERAL_SETTINGS, PRECONFIGURED_TELNET_CONSOLE_COMMANDS, PRECONFIGURED_SERIAL_CONSOLE_COMMANDS, STYLES
|
||||
|
||||
|
||||
class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
|
||||
@@ -58,6 +58,7 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
|
||||
self.uiDefaultLabelFontPushButton.clicked.connect(self._setDefaultLabelFontSlot)
|
||||
self.uiDefaultLabelColorPushButton.clicked.connect(self._setDefaultLabelColorSlot)
|
||||
self._default_label_color = QtGui.QColor(QtCore.Qt.black)
|
||||
self.uiStyleComboBox.addItems(STYLES)
|
||||
|
||||
def _projectsPathSlot(self):
|
||||
"""
|
||||
@@ -141,10 +142,9 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
|
||||
|
||||
QtGui.QMessageBox.information(self, "Configuration file", "Configuration file imported, default settings will be applied after a restart")
|
||||
|
||||
# restart the application
|
||||
from ..main_window import MainWindow
|
||||
main_window = MainWindow.instance()
|
||||
main_window.reboot_signal.emit()
|
||||
#TODO: implement restart
|
||||
#QtCore.QProcess.startDetached(QtGui.QApplication.arguments()[0], QtGui.QApplication.arguments())
|
||||
QtGui.QApplication.quit()
|
||||
|
||||
def _exportConfigurationFileSlot(self):
|
||||
"""
|
||||
@@ -195,11 +195,15 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
|
||||
self.uiProjectsPathLineEdit.setText(settings["projects_path"])
|
||||
self.uiImagesPathLineEdit.setText(settings["images_path"])
|
||||
self.uiTemporaryFilesPathLineEdit.setText(settings["temporary_files_path"])
|
||||
self.uiLaunchNewProjectDialogCheckBox.setChecked(settings["auto_launch_project_dialog"])
|
||||
self.uiCheckForUpdateCheckBox.setChecked(settings["check_for_update"])
|
||||
self.uiLinkManualModeCheckBox.setChecked(settings["link_manual_mode"])
|
||||
self.uiSlowStartAllSpinBox.setValue(settings["slow_device_start_all"])
|
||||
self.uiTelnetConsoleCommandLineEdit.setText(settings["telnet_console_command"])
|
||||
self.uiTelnetConsoleCommandLineEdit.setCursorPosition(0)
|
||||
index = self.uiStyleComboBox.findText(settings["style"])
|
||||
if index != -1:
|
||||
self.uiStyleComboBox.setCurrentIndex(index)
|
||||
index = self.uiTelnetConsolePreconfiguredCommandComboBox.findData(settings["telnet_console_command"])
|
||||
if index != -1:
|
||||
self.uiTelnetConsolePreconfiguredCommandComboBox.setCurrentIndex(index)
|
||||
@@ -210,7 +214,7 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
|
||||
self.uiSerialConsolePreconfiguredCommandComboBox.setCurrentIndex(index)
|
||||
self.uiCloseConsoleWindowsOnDeleteCheckBox.setChecked(settings["auto_close_console"])
|
||||
self.uiBringConsoleWindowToFrontCheckBox.setChecked(settings["bring_console_to_front"])
|
||||
self.uiSlowConsoleAllDoubleSpinBox.setValue(settings["slow_console_all"])
|
||||
self.uiDelayConsoleAllSpinBox.setValue(settings["delay_console_all"])
|
||||
|
||||
def _populateGraphicsViewSettingWidgets(self, settings):
|
||||
"""
|
||||
@@ -253,6 +257,8 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
|
||||
new_settings["projects_path"] = self.uiProjectsPathLineEdit.text()
|
||||
new_settings["images_path"] = self.uiImagesPathLineEdit.text()
|
||||
new_settings["temporary_files_path"] = self.uiTemporaryFilesPathLineEdit.text()
|
||||
new_settings["auto_launch_project_dialog"] = self.uiLaunchNewProjectDialogCheckBox.isChecked()
|
||||
new_settings["style"] = self.uiStyleComboBox.currentText()
|
||||
new_settings["check_for_update"] = self.uiCheckForUpdateCheckBox.isChecked()
|
||||
new_settings["link_manual_mode"] = self.uiLinkManualModeCheckBox.isChecked()
|
||||
new_settings["slow_device_start_all"] = self.uiSlowStartAllSpinBox.value()
|
||||
@@ -260,7 +266,7 @@ class GeneralPreferencesPage(QtGui.QWidget, Ui_GeneralPreferencesPageWidget):
|
||||
new_settings["serial_console_command"] = self.uiSerialConsoleCommandLineEdit.text()
|
||||
new_settings["auto_close_console"] = self.uiCloseConsoleWindowsOnDeleteCheckBox.isChecked()
|
||||
new_settings["bring_console_to_front"] = self.uiBringConsoleWindowToFrontCheckBox.isChecked()
|
||||
new_settings["slow_console_all"] = self.uiSlowConsoleAllDoubleSpinBox.value()
|
||||
new_settings["delay_console_all"] = self.uiDelayConsoleAllSpinBox.value()
|
||||
|
||||
from ..main_window import MainWindow
|
||||
MainWindow.instance().setSettings(new_settings)
|
||||
|
||||
@@ -179,6 +179,7 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
self.uiLocalServerPortSpinBox.setValue(local_server.port)
|
||||
self.uiLocalServerPathLineEdit.setText(servers.localServerPath())
|
||||
self.uiLocalServerAutoStartCheckBox.setChecked(servers.localServerAutoStart())
|
||||
self.uiConsoleConnectionsToAnyIPCheckBox.setChecked(servers.localServerAllowConsoleFromAnywhere())
|
||||
|
||||
# load remote server preferences
|
||||
self._remote_servers.clear()
|
||||
@@ -206,6 +207,7 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
local_server_port = self.uiLocalServerPortSpinBox.value()
|
||||
local_server_path = self.uiLocalServerPathLineEdit.text()
|
||||
local_server_auto_start = self.uiLocalServerAutoStartCheckBox.isChecked()
|
||||
local_server_allow_console_from_anywhere = self.uiConsoleConnectionsToAnyIPCheckBox.isChecked()
|
||||
|
||||
if local_server_path:
|
||||
if not os.path.isfile(local_server_path):
|
||||
@@ -214,7 +216,10 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
QtGui.QMessageBox.critical(self, "Local server", "{} is not an executable".format(local_server_path))
|
||||
else:
|
||||
server = servers.localServer()
|
||||
if servers.localServerPath() != local_server_path or server.host != local_server_host or server.port != local_server_port:
|
||||
if servers.localServerPath() != local_server_path or \
|
||||
server.host != local_server_host or \
|
||||
server.port != local_server_port or \
|
||||
servers.localServerAllowConsoleFromAnywhere() != local_server_allow_console_from_anywhere:
|
||||
|
||||
# first check if we have nodes on the local server
|
||||
local_nodes = []
|
||||
@@ -228,6 +233,12 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
MessageBox(self, "Local server", "Please close your project or delete all the nodes running on the local server before changing settings", nodes)
|
||||
return
|
||||
|
||||
servers.setLocalServer(local_server_path,
|
||||
local_server_host,
|
||||
local_server_port,
|
||||
local_server_auto_start,
|
||||
local_server_allow_console_from_anywhere)
|
||||
|
||||
# local server settings have changed, let's stop the current local server.
|
||||
if server.connected() and not sys.platform.startswith('win'):
|
||||
server.close_connection()
|
||||
@@ -240,8 +251,12 @@ class ServerPreferencesPage(QtGui.QWidget, Ui_ServerPreferencesPageWidget):
|
||||
dialog.exec_()
|
||||
else:
|
||||
QtGui.QMessageBox.critical(self, "Local server", "Could not start the local server process: {}".format(local_server_path))
|
||||
|
||||
servers.setLocalServer(local_server_path, local_server_host, local_server_port, local_server_auto_start)
|
||||
else:
|
||||
servers.setLocalServer(local_server_path,
|
||||
local_server_host,
|
||||
local_server_port,
|
||||
local_server_auto_start,
|
||||
local_server_allow_console_from_anywhere)
|
||||
|
||||
# save the remote server preferences
|
||||
servers.updateRemoteServers(self._remote_servers)
|
||||
|
||||
@@ -57,6 +57,7 @@ class Port(object):
|
||||
Port._instance_count += 1
|
||||
|
||||
self._name = name
|
||||
self._short_name = None
|
||||
self._port_number = None
|
||||
self._slot_number = None
|
||||
self._stub = stub
|
||||
@@ -128,6 +129,26 @@ class Port(object):
|
||||
|
||||
self._name = new_name
|
||||
|
||||
def shortName(self):
|
||||
"""
|
||||
Returns the short name of this port.
|
||||
|
||||
:returns: current short port name (string)
|
||||
"""
|
||||
|
||||
if not self._short_name:
|
||||
return self._name
|
||||
return self._short_name
|
||||
|
||||
def setShortName(self, short_name):
|
||||
"""
|
||||
Sets a new short name for this port.
|
||||
|
||||
:param short_name: short port name (string)
|
||||
"""
|
||||
|
||||
self._short_name = short_name
|
||||
|
||||
def status(self):
|
||||
"""
|
||||
Returns the status of this port.
|
||||
@@ -265,16 +286,21 @@ class Port(object):
|
||||
|
||||
self._link_id = link_id
|
||||
|
||||
def description(self):
|
||||
def description(self, short=False):
|
||||
"""
|
||||
Returns the text description of this port.
|
||||
|
||||
:param short: returns a shorter description.
|
||||
|
||||
:returns: description
|
||||
"""
|
||||
|
||||
if self._destination_node and self._destination_port:
|
||||
if short:
|
||||
return "<-> {port} {name}".format(port=self._destination_port.shortName(),
|
||||
name=self._destination_node.name())
|
||||
return "connected to {name} on port {port}".format(name=self._destination_node.name(),
|
||||
port=self._destination_port.name())
|
||||
port=self._destination_port.name())
|
||||
return ""
|
||||
|
||||
def setFree(self):
|
||||
|
||||
@@ -63,6 +63,6 @@ def serialConsole(vmname):
|
||||
# use arguments on other platforms
|
||||
args = shlex.split(command)
|
||||
subprocess.Popen(args)
|
||||
except (OSError, ValueError) as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warning('could not start serial console "{}": {}'.format(command, e))
|
||||
raise
|
||||
|
||||
@@ -19,13 +19,11 @@
|
||||
Keeps track of all the local and remote servers and their settings.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shlex
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import ssl
|
||||
|
||||
from .qt import QtCore
|
||||
from .websocket_client import WebSocketClient, SecureWebSocketClient
|
||||
from .settings import DEFAULT_LOCAL_SERVER_PATH
|
||||
@@ -53,6 +51,7 @@ class Servers(QtCore.QObject):
|
||||
self._cloud_servers = {}
|
||||
self._local_server_path = ""
|
||||
self._local_server_auto_start = True
|
||||
self._local_server_allow_console_from_anywhere = False
|
||||
self._local_server_proccess = None
|
||||
self._settings = self._loadSettings()
|
||||
self._remote_server_iter_pos = 0
|
||||
@@ -78,8 +77,13 @@ class Servers(QtCore.QObject):
|
||||
local_server_port = settings.value("local_server_port", DEFAULT_LOCAL_SERVER_PORT, type=int)
|
||||
local_server_path = settings.value("local_server_path", DEFAULT_LOCAL_SERVER_PATH)
|
||||
local_server_auto_start = settings.value("local_server_auto_start", True, type=bool)
|
||||
heartbeat_freq = settings.value("heartbeat_freq", DEFAULT_HEARTBEAT_FREQ, type=int)
|
||||
self.setLocalServer(local_server_path, local_server_host, local_server_port, local_server_auto_start, heartbeat_freq)
|
||||
local_server_allow_console_from_anywhere = settings.value("local_server_allow_console_from_anywhere", False, type=bool)
|
||||
|
||||
self.setLocalServer(local_server_path,
|
||||
local_server_host,
|
||||
local_server_port,
|
||||
local_server_auto_start,
|
||||
local_server_allow_console_from_anywhere)
|
||||
|
||||
# load the remote servers
|
||||
size = settings.beginReadArray("remote")
|
||||
@@ -109,6 +113,7 @@ class Servers(QtCore.QObject):
|
||||
settings.setValue("local_server_port", self._local_server.port)
|
||||
settings.setValue("local_server_path", self._local_server_path)
|
||||
settings.setValue("local_server_auto_start", self._local_server_auto_start)
|
||||
settings.setValue("local_server_allow_console_from_anywhere", self._local_server_allow_console_from_anywhere)
|
||||
|
||||
# save the remote servers
|
||||
settings.beginWriteArray("remote", len(self._remote_servers))
|
||||
@@ -131,6 +136,16 @@ class Servers(QtCore.QObject):
|
||||
|
||||
return self._local_server_auto_start
|
||||
|
||||
def localServerAllowConsoleFromAnywhere(self):
|
||||
"""
|
||||
Returns either the local server
|
||||
is allows console connections to any local IP address.
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
return self._local_server_allow_console_from_anywhere
|
||||
|
||||
def localServerPath(self):
|
||||
"""
|
||||
Returns the local server path.
|
||||
@@ -145,7 +160,10 @@ class Servers(QtCore.QObject):
|
||||
Starts the local server process.
|
||||
"""
|
||||
|
||||
command = '"{executable}" --host={host} --port={port}'.format(executable=path, host=host, port=port)
|
||||
command = '"{executable}" --host={host} --port={port} --console_bind_to_any={bind}'.format(executable=path,
|
||||
host=host,
|
||||
port=port,
|
||||
bind=self._local_server_allow_console_from_anywhere)
|
||||
|
||||
# settings_dir = os.path.dirname(QtCore.QSettings().fileName())
|
||||
# if os.path.isdir(settings_dir):
|
||||
@@ -172,7 +190,7 @@ class Servers(QtCore.QObject):
|
||||
# use arguments on other platforms
|
||||
args = shlex.split(command)
|
||||
self._local_server_proccess = subprocess.Popen(args)
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
log.warning('could not start local server "{}": {}'.format(command, e))
|
||||
return False
|
||||
|
||||
@@ -180,18 +198,18 @@ class Servers(QtCore.QObject):
|
||||
|
||||
def stopLocalServer(self, wait=False):
|
||||
|
||||
if self._local_server and self._local_server.connected() and not sys.platform.startswith('win'):
|
||||
# only gracefully disconnect if we are not on Windows
|
||||
self._local_server.close_connection()
|
||||
#if self._local_server and self._local_server.connected() and not sys.platform.startswith('win'):
|
||||
# # only gracefully disconnect if we are not on Windows
|
||||
# self._local_server.close_connection()
|
||||
if self._local_server_proccess and self._local_server_proccess.poll() is None:
|
||||
if sys.platform.startswith("win"):
|
||||
self._local_server_proccess.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
self._local_server_proccess.send_signal(signal.SIGINT)
|
||||
if wait:
|
||||
self._local_server_proccess.wait()
|
||||
self._local_server_proccess.wait(timeout=3000)
|
||||
|
||||
def setLocalServer(self, path, host, port, auto_start, heartbeat_freq=DEFAULT_HEARTBEAT_FREQ):
|
||||
def setLocalServer(self, path, host, port, auto_start, allow_console_from_anywhere):
|
||||
"""
|
||||
Sets the local server.
|
||||
|
||||
@@ -200,11 +218,12 @@ class Servers(QtCore.QObject):
|
||||
:param port: port of the server (integer)
|
||||
:param auto_start: either the local server should be
|
||||
automatically started on startup (boolean)
|
||||
:param heartbeat_freq: The interval to send heartbeats to the server
|
||||
:param allow_console_from_anywhere: allow console connections to any local IP address
|
||||
"""
|
||||
|
||||
self._local_server_path = path
|
||||
self._local_server_auto_start = auto_start
|
||||
self._local_server_allow_console_from_anywhere = allow_console_from_anywhere
|
||||
if self._local_server:
|
||||
if self._local_server.host == host and self._local_server.port == port:
|
||||
return
|
||||
@@ -215,7 +234,6 @@ class Servers(QtCore.QObject):
|
||||
url = "ws://{host}:{port}".format(host=host, port=port)
|
||||
self._local_server = WebSocketClient(url)
|
||||
self._local_server.setLocal(True)
|
||||
self._local_server.enableHeartbeatsAt(heartbeat_freq)
|
||||
log.info("new local server connection {} registered".format(url))
|
||||
|
||||
def localServer(self):
|
||||
@@ -227,7 +245,6 @@ class Servers(QtCore.QObject):
|
||||
|
||||
return self._local_server
|
||||
|
||||
|
||||
def _addRemoteServer(self, host, port, heartbeat_freq=DEFAULT_HEARTBEAT_FREQ):
|
||||
"""
|
||||
Adds a new remote server.
|
||||
@@ -299,7 +316,7 @@ class Servers(QtCore.QObject):
|
||||
|
||||
return self._remote_servers
|
||||
|
||||
def getCloudServer(self, host, port, ca_file):
|
||||
def getCloudServer(self, host, port, ca_file, auth_user, auth_password, ssh_pkey, instance_id):
|
||||
"""
|
||||
Return a websocket connection to the cloud server, creating one if none exists.
|
||||
|
||||
@@ -310,13 +327,15 @@ class Servers(QtCore.QObject):
|
||||
"""
|
||||
|
||||
for server in self._remote_servers.values():
|
||||
if server.host == host and server.port == port:
|
||||
if server.host == host and int(server.port) == int(port):
|
||||
return server
|
||||
|
||||
heartbeat_freq = self._settings.value("heartbeat_freq", DEFAULT_HEARTBEAT_FREQ)
|
||||
return self._addCloudServer(host, port, ca_file, heartbeat_freq)
|
||||
|
||||
def _addCloudServer(self, host, port, ca_file, heartbeat_freq):
|
||||
return self._addCloudServer(host, port, ca_file, auth_user, auth_password, ssh_pkey,
|
||||
heartbeat_freq, instance_id)
|
||||
|
||||
def _addCloudServer(self, host, port, ca_file, auth_user, auth_password, ssh_pkey,
|
||||
heartbeat_freq, instance_id):
|
||||
"""
|
||||
Create a websocket connection to the specified cloud server
|
||||
|
||||
@@ -331,12 +350,43 @@ class Servers(QtCore.QObject):
|
||||
url = "wss://{host}:{port}".format(host=host, port=port)
|
||||
log.debug('Starting SecureWebSocketClient url={}'.format(url))
|
||||
log.debug('Starting SecureWebSocketClient ca_file={}'.format(ca_file))
|
||||
server = SecureWebSocketClient(url, ca_file)
|
||||
log.debug('Starting SecureWebSocketClient ssh_pkey={}'.format(ssh_pkey))
|
||||
server = SecureWebSocketClient(url, instance_id=instance_id)
|
||||
server.setSecureOptions(ca_file, auth_user, auth_password, ssh_pkey)
|
||||
server.setCloud(True)
|
||||
server.enableHeartbeatsAt(heartbeat_freq)
|
||||
self._cloud_servers[host] = server
|
||||
log.info("new remote server connection {} registered".format(url))
|
||||
return server
|
||||
|
||||
def anyCloudServer(self):
|
||||
# Return the first server for now
|
||||
for key, value in self._cloud_servers.items():
|
||||
return value
|
||||
return None
|
||||
|
||||
def cloudServerById(self, instance_id):
|
||||
"""
|
||||
Return the server with the specified instance id, or None.
|
||||
"""
|
||||
for cs in self.cloud_servers.values():
|
||||
if cs.instance_id == instance_id:
|
||||
return cs
|
||||
return None
|
||||
|
||||
def removeCloudServer(self, server):
|
||||
try:
|
||||
cs = self.cloud_servers[server.host]
|
||||
cs.close_connection()
|
||||
del self.cloud_servers[server.host]
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def cloud_servers(self):
|
||||
return self._cloud_servers
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Creates a round-robin system to pick up a remote server.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user