diff --git a/gns3/cloud/base_cloud_ctrl.py b/gns3/cloud/base_cloud_ctrl.py
index 7def0054..1b5ce475 100644
--- a/gns3/cloud/base_cloud_ctrl.py
+++ b/gns3/cloud/base_cloud_ctrl.py
@@ -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):
diff --git a/gns3/cloud/utils.py b/gns3/cloud/utils.py
index 31dc376e..b17de995 100644
--- a/gns3/cloud/utils.py
+++ b/gns3/cloud/utils.py
@@ -46,7 +46,10 @@ 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()
@@ -60,7 +63,7 @@ class ListInstancesThread(QtCore.QThread):
instancesReady = QtCore.pyqtSignal(object)
def __init__(self, parent, provider):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self._provider = provider
def run(self):
@@ -79,7 +82,7 @@ class CreateInstanceThread(QtCore.QThread):
instanceCreated = QtCore.pyqtSignal(object, object)
def __init__(self, parent, provider, name, flavor_id, image_id):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self._provider = provider
self._name = name
self._flavor_id = flavor_id
@@ -109,7 +112,7 @@ class DeleteInstanceThread(QtCore.QThread):
instanceDeleted = QtCore.pyqtSignal(object)
def __init__(self, parent, provider, instance):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self._provider = provider
self._instance = instance
@@ -171,7 +174,7 @@ killall python3 gns3server gns3dms
'''
def __init__(self, parent, host, private_key_string, server_id, username, api_key, region, dead_time):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self._host = host
self._private_key_string = private_key_string
self._server_id = server_id
@@ -250,7 +253,7 @@ class WSConnectThread(QtCore.QThread):
def __init__(self, parent, provider, server_id, host, port, ca_file,
auth_user, auth_password, ssh_pkey, instance_id):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self._provider = provider
self._server_id = server_id
self._host = host
@@ -290,7 +293,7 @@ class UploadProjectThread(QtCore.QThread):
update = QtCore.pyqtSignal(int)
def __init__(self, parent, cloud_settings, project_path, images_path):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self.cloud_settings = cloud_settings
self.project_path = project_path
self.images_path = images_path
@@ -356,26 +359,42 @@ class UploadProjectThread(QtCore.QThread):
class UploadFilesThread(QtCore.QThread):
"""
- Upload multiple files to cloud files
+ Uploads files to cloud files
- uploads - A list of 2-tuples of (local_src_path, remote_dst_path)
+ :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, uploads):
- super(QtCore.QThread, self).__init__(parent)
+ def __init__(self, parent, cloud_settings, files_to_upload):
+ super().__init__(parent)
self._cloud_settings = cloud_settings
- self._uploads = uploads
+ self._files_to_upload = files_to_upload
def run(self):
- for src, dst in self._uploads:
- log.debug('Upload from {} to {}'.format(src, dst))
- provider = get_provider(self._cloud_settings)
- provider.upload_file(src, dst)
- log.debug('Upload image completed')
+ 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(str(e)), True)
+
self.completed.emit()
+ def stop(self):
+ self.quit()
+
class DownloadProjectThread(QtCore.QThread):
"""
@@ -388,7 +407,7 @@ class DownloadProjectThread(QtCore.QThread):
update = QtCore.pyqtSignal(int)
def __init__(self, parent, cloud_project_file_name, project_dest_path, images_dest_path, cloud_settings):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self.project_name = cloud_project_file_name
self.project_dest_path = project_dest_path
self.images_dest_path = images_dest_path
@@ -445,7 +464,7 @@ class DeleteProjectThread(QtCore.QThread):
update = QtCore.pyqtSignal(int)
def __init__(self, parent, project_file_name, cloud_settings):
- super(QtCore.QThread, self).__init__(parent)
+ super().__init__(parent)
self.project_file_name = project_file_name
self.cloud_settings = cloud_settings
diff --git a/gns3/cloud_builder.py b/gns3/cloud_builder.py
new file mode 100644
index 00000000..13b5fff8
--- /dev/null
+++ b/gns3/cloud_builder.py
@@ -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 .
+
+"""
+"""
+
+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)
diff --git a/gns3/cloud_inspector_view.py b/gns3/cloud_inspector_view.py
index f50b4165..48136711 100644
--- a/gns3/cloud_inspector_view.py
+++ b/gns3/cloud_inspector_view.py
@@ -1,17 +1,19 @@
# -*- coding: utf-8 -*-
-import ast
+from collections import namedtuple
import logging
import os
-from .qt import QtCore, QtGui
-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__)
@@ -196,8 +198,8 @@ class CloudInspectorView(QtGui.QWidget, Ui_CloudInspectorView):
# map flavor ids to combobox indexes
self.flavor_index_id = []
- # TODO: Delete me
- self._running = {}
+ # A dictionary of {image_id, CloudBuilder}
+ self._builders = {}
def _get_flavor_index(self, flavor_id):
try:
@@ -205,17 +207,17 @@ class CloudInspectorView(QtGui.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')
- 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._update_model)
@@ -310,95 +312,39 @@ class CloudInspectorView(QtGui.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.
"""
- # instance state transition: GNS3SERVER_STARTING --> GNS3SERVER_STARTED
- instance = self._model.getInstanceById(id)
- instance.state = RunningInstanceState.GNS3SERVER_STARTED
- self._model.updateInstanceFields(instance, ['state'])
-
- data = ast.literal_eval(start_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(host_ip)
- # TODO: Move this directory into projectSettings.
- ca_dir = os.path.join(self._main_window.projectSettings()["project_files_dir"], "keys")
- 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(id)
- top_instance.set_later_attributes(host_ip, port, ssl_cert, ca_file)
- ssh_pkey = top_instance.private_key
-
- log.debug('Cloud server gns3server started.')
- wss_thread = WSConnectThread(self, self._provider, id, host_ip, port, ca_file,
- username, password, ssh_pkey, id)
- wss_thread.established.connect(self._wss_connected_slot)
- wss_thread.start()
-
- def _wss_connected_slot(self, id):
- """
- This slot is called when the WSConnectThread successfully connected to
- the websocket on the remote host
- """
- # instance state transition: GNS3SERVER_STARTED --> WS_CONNECTED
instance = self._model.getInstanceById(id)
instance.state = RunningInstanceState.WS_CONNECTED
self._model.updateInstanceFields(instance, ['state'])
- 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
-
def _update_model(self, instances):
if not instances:
return
+ # Filter instances to only those in the current project
+ project_instances = [i for i in instances if i.id in self._project_instances_id]
+
# populate underlying model if this is the first call
- if self._model.rowCount() == 0 and len(instances) > 0:
- self._populate_model(instances)
+ 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)
- # filter instances to only those in the current project
- project_instances = [i for i in instances if i.id in self._project_instances_id]
- # cleanup removed 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()
+ # Update instance status
for i in project_instances:
# get the customized instance state from self._model
model_instance = self._model.getInstanceById(i.id)
@@ -407,31 +353,12 @@ class CloudInspectorView(QtGui.QWidget, Ui_CloudInspectorView):
if i.state != RunningInstanceState.RUNNING:
self._model.updateInstanceFields(i, ['state'])
- # start gns3server if needed
- if i.state == RunningInstanceState.RUNNING and (
- model_instance.state >= RunningInstanceState.RUNNING):
- # instance state transition: RUNNING --> GNS3SERVER_STARTING
- model_instance.state = RunningInstanceState.GNS3SERVER_STARTING
- self._model.updateInstanceFields(model_instance, ['state'])
-
- # start GNS3 server and deadman switch
- public_ip = self._get_public_ip(i.public_ips)
- instance_manager.update_host_for_instance(i.id, public_ip)
- topology_instance = instance_manager.get_instance(i.id)
- 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()
-
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):
@@ -445,10 +372,44 @@ class CloudInspectorView(QtGui.QWidget, Ui_CloudInspectorView):
"then wait for the instance to appear in the inspector.")
if ok:
- if not name.endswith("-gns3"):
- name += "-gns3"
+ self.createInstance(name, flavor_id, image_id)
- create_thread = CreateInstanceThread(self, self._provider, name, flavor_id, image_id)
- create_thread.instanceCreated.connect(self._main_window.add_instance_to_project)
- create_thread.instanceCreated.connect(CloudInstances.instance().add_instance)
- create_thread.start()
+ 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
diff --git a/gns3/cloud_instances.py b/gns3/cloud_instances.py
index 8839e820..2655d498 100644
--- a/gns3/cloud_instances.py
+++ b/gns3/cloud_instances.py
@@ -64,29 +64,38 @@ class CloudInstances(QtCore.QObject):
def add_instance(self, instance, keypair):
if instance is None:
return
- ti = TopologyInstance(instance.name, instance.id, instance.extra['flavorId'],
- instance.extra['imageId'], keypair.private_key, keypair.public_key)
- self._instances.append(ti)
- self.save()
+ 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 static in self._instances:
+ for stored in self._instances:
found = False
for dynamic in instances:
- if static.id == dynamic.id:
+ if stored.id == dynamic.id:
found = True
break
if not found:
- self._instances.remove(static)
+ self._instances.remove(stored)
save_needed = True
if save_needed:
self.save()
- def update_host_for_instance(self, instance_id, host):
+ 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:
diff --git a/gns3/main_window.py b/gns3/main_window.py
index 10e9f812..6aa08f20 100644
--- a/gns3/main_window.py
+++ b/gns3/main_window.py
@@ -31,6 +31,8 @@ import json
import glob
import logging
import functools
+import ast
+import posixpath
from pkg_resources import parse_version
@@ -58,7 +60,7 @@ from .items.shape_item import ShapeItem
from .items.image_item import ImageItem
from .items.note_item import NoteItem
from .topology import Topology, TopologyInstance
-from .cloud.utils import UploadProjectThread
+from .cloud.utils import UploadProjectThread, UploadFilesThread, ssh_client
from .cloud.rackspace_ctrl import get_provider
from .cloud.exceptions import KeyPairExists
from .cloud_instances import CloudInstances
@@ -274,7 +276,9 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.uiSaveProjectAction.triggered.connect(self._saveProjectActionSlot)
self.uiSaveProjectAsAction.triggered.connect(self._saveProjectAsActionSlot)
self.uiExportProjectAction.triggered.connect(self._exportProjectActionSlot)
- #self.uiImportProjectAction.triggered.connect(self._importProjectActionSlot)
+ self.uiImportProjectAction.triggered.connect(self._importProjectActionSlot)
+ self.uiMoveLocalProjectToCloudAction.triggered.connect(self._moveLocalProjectToCloudActionSlot)
+ self.uiMoveCloudProjectToLocalAction.triggered.connect(self._moveCloudProjectToLocalActionSlot)
self.uiImportExportConfigsAction.triggered.connect(self._importExportConfigsActionSlot)
self.uiScreenshotAction.triggered.connect(self._screenshotActionSlot)
self.uiSnapshotAction.triggered.connect(self._snapshotActionSlot)
@@ -376,7 +380,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def _createNewProject(self, new_project_settings):
"""
- Crates a new project.
+ Creates a new project.
:param new_project_settings: project settings (dict)
"""
@@ -1580,7 +1584,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
return
project_instances = json_topology["topology"]["instances"]
- self.CloudInspectorView.load(self, project_instances)
+ self.CloudInspectorView.load(self, [i["id"] for i in project_instances])
def add_instance_to_project(self, instance, keypair):
"""
@@ -1640,9 +1644,13 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
if self._temporary_project:
# do nothing if project is temporary
QtGui.QMessageBox.critical(
- self, "Export project server",
- "Cannot export temporary projects, please save current project first.")
+ self,
+ "Backup project",
+ "Cannot backup temporary projects, please save current project first."
+ )
return
+ if self.checkForUnsavedChanges():
+ self.saveProject(self._project_settings["project_path"])
upload_thread = UploadProjectThread(
self,
@@ -1650,7 +1658,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self._project_settings['project_path'],
self._settings['images_path']
)
- progress_dialog = ProgressDialog(upload_thread, "Exporting Project", "Uploading project files...", "Cancel",
+ progress_dialog = ProgressDialog(upload_thread, "Backing Up Project", "Uploading project files...", "Cancel",
parent=self)
progress_dialog.show()
progress_dialog.exec_()
@@ -1666,6 +1674,117 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
dialog.show()
dialog.exec_()
+ def _moveLocalProjectToCloudActionSlot(self):
+ if self._temporary_project:
+ # do nothing if project is temporary
+ QtGui.QMessageBox.critical(
+ self,
+ "Move project to Cloud",
+ "Cannot move temporary projects, please save current project first.")
+ return
+ if self._project_settings["project_type"] == "cloud":
+ # do nothing if project is already a cloud project
+ QtGui.QMessageBox.critical(
+ self,
+ "Move project to Cloud",
+ "This project is already a Cloud Project")
+ return
+ if not self.checkForUnsavedChanges():
+ # do nothing if project is already a cloud project
+ QtGui.QMessageBox.critical(
+ self,
+ "Unsaved changes",
+ "There are unsaved changes. Please save the project first.")
+ return
+
+ # Upload images to cloud storage
+ topology = Topology.instance()
+ images = set([
+ (
+ node.settings()['image'],
+ 'images/' + os.path.relpath(node.settings()['image'], self._settings["images_path"])
+ )
+ for node in topology.nodes() if 'image' in node.settings()
+ ])
+ log.debug('uploading images ' + str(images) + ' to cloud')
+ upload_thread = UploadFilesThread(self, self._cloud_settings, images)
+ upload_images_progress_dialog = ProgressDialog(upload_thread, "Uploading images", "Uploading image files...",
+ "Cancel", parent=self)
+ upload_images_progress_dialog.show()
+ upload_images_progress_dialog.exec_()
+
+ progress_dialog = QtGui.QProgressDialog("Moving project to cloud", "Cancel", 0, 100, self)
+ progress_dialog.show()
+
+ def buildComplete(server_id):
+ progress_dialog.setValue(80)
+ log.debug("websocket connected, server_id=" + str(server_id))
+
+ instance = topology.getInstance(server_id)
+ # copy nvram, config, and disk files to server
+ with ssh_client(instance.host, instance.private_key) as client:
+ log.debug('copying device files to cloud instance')
+ sftp = client.open_sftp()
+
+ def should_copy(filename):
+ return not filename.endswith('.ghost')
+
+ project_files_dir = os.path.join(
+ os.path.dirname(self._project_settings['project_path']),
+ os.path.basename(os.path.dirname(self._project_settings['project_path'])) + '-files'
+ )
+ dest_project_path = posixpath.join(
+ '/root/GNS3/projects',
+ os.path.basename(os.path.dirname(self._project_settings['project_path']))
+ )
+
+ for root, dirs, files in os.walk(project_files_dir):
+ directory = posixpath.normpath(posixpath.join(
+ dest_project_path,
+ os.path.relpath(root, project_files_dir).replace('\\', '/')
+ ))
+ sftp.mkdir(directory)
+ sftp.chdir(directory)
+
+ for file in files:
+ local_filepath = os.path.join(root, file)
+ if os.path.isfile(local_filepath) and should_copy(file):
+ log.debug('copying file ' + local_filepath)
+ sftp.put(local_filepath, file)
+ log.debug('copied file successfully')
+
+ sftp.close()
+
+ self._project_settings["project_type"] = "cloud"
+
+ server = Servers.instance().anyCloudServer()
+
+ for node in topology.nodes():
+ node._server = server
+
+ self.saveProject(self._project_settings["project_path"])
+ topology.reset()
+ self.loadProject(self._project_settings["project_path"])
+ progress_dialog.accept()
+
+ instances = CloudInstances.instance().instances
+ for instance in instances:
+ topology.addInstance2(instance)
+ self.CloudInspectorView.load(self, [i.id for i in topology.instances()])
+
+ # Create a new instance. At some point we could reuse an existing instance.
+ builder = self.CloudInspectorView.createInstance(
+ self._project_settings["project_name"],
+ self.cloudSettings()['default_flavor'],
+ self.cloudSettings()['default_image']
+ )
+ builder.buildComplete.connect(buildComplete)
+
+
+ def _moveCloudProjectToLocalActionSlot(self):
+ #TODO implement moving cloud project to local
+ print("move cloud project to local")
+
def _cloud_instance_selected(self, instance_id):
"""
Clear selection, then select all the nodes on the graphics view
diff --git a/gns3/modules/dynamips/__init__.py b/gns3/modules/dynamips/__init__.py
index d8243075..1dbf9bc8 100644
--- a/gns3/modules/dynamips/__init__.py
+++ b/gns3/modules/dynamips/__init__.py
@@ -488,11 +488,7 @@ class Dynamips(Module):
if wic in ios_router:
settings[wic] = ios_router[wic]
- if node.server().isCloud():
- settings["cloud_path"] = "images/IOS"
- node.setup(ios_router["image"], ios_router["ram"], initial_settings=settings)
- else:
- node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings)
+ node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings)
else:
node.setup()
diff --git a/gns3/modules/dynamips/nodes/router.py b/gns3/modules/dynamips/nodes/router.py
index 63315203..55b8196b 100644
--- a/gns3/modules/dynamips/nodes/router.py
+++ b/gns3/modules/dynamips/nodes/router.py
@@ -250,6 +250,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
diff --git a/gns3/modules/dynamips/pages/ios_router_preferences_page.py b/gns3/modules/dynamips/pages/ios_router_preferences_page.py
index 65834a7c..b08601ae 100644
--- a/gns3/modules/dynamips/pages/ios_router_preferences_page.py
+++ b/gns3/modules/dynamips/pages/ios_router_preferences_page.py
@@ -128,10 +128,15 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget)
self._upload_image_progress_dialog.setWindowTitle("IOS image upload")
self._upload_image_progress_dialog.show()
try:
- src = self._ios_routers[key]['path']
- # Eg: images/IOS/c3745.img
- dst = 'images/IOS/{}'.format(self._ios_routers[key]['image'])
- upload_thread = UploadFilesThread(self, MainWindow.instance().cloudSettings(), [(src, dst)])
+ 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:
diff --git a/gns3/topology.py b/gns3/topology.py
index d80cd622..a5394939 100644
--- a/gns3/topology.py
+++ b/gns3/topology.py
@@ -267,7 +267,6 @@ class Topology(object):
:param id: the instance id
:return: a TopologyInstance object
"""
-
for instance in self._instances:
if instance.id == id:
return instance
@@ -516,6 +515,8 @@ class Topology(object):
for topology_server in servers:
if "local" in topology_server and topology_server["local"]:
self._servers[topology_server["id"]] = server_manager.localServer()
+ elif "cloud" in topology_server and topology_server["cloud"]:
+ self._servers[topology_server["id"]] = server_manager.anyCloudServer()
else:
host = topology_server["host"]
port = topology_server["port"]
diff --git a/gns3/ui/main_window.ui b/gns3/ui/main_window.ui
index e2011022..2b17b800 100644
--- a/gns3/ui/main_window.ui
+++ b/gns3/ui/main_window.ui
@@ -85,8 +85,11 @@ background-none;
-
+
+
+
+
@@ -1109,12 +1112,22 @@ background-none;
- Export project
+ Backup project to cloud
- Import project
+ Restore backup from cloud
+
+
+
+
+ Move local project to cloud
+
+
+
+
+ Move cloud project to local
diff --git a/gns3/ui/main_window_ui.py b/gns3/ui/main_window_ui.py
index 0eeecf92..0ed7ea9b 100644
--- a/gns3/ui/main_window_ui.py
+++ b/gns3/ui/main_window_ui.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file '/home/grossmj/PycharmProjects/gns3-gui/gns3/ui/main_window.ui'
+# Form implementation generated from reading ui file 'gns3/ui/main_window.ui'
#
-# Created: Thu Dec 11 23:26:53 2014
+# Created: Wed Dec 10 15:30:27 2014
# by: PyQt4 UI code generator 4.10.4
#
# WARNING! All changes made in this file will be lost!
@@ -112,7 +112,7 @@ class Ui_MainWindow(object):
sizePolicy.setHeightForWidth(self.uiNodesView.sizePolicy().hasHeightForWidth())
self.uiNodesView.setSizePolicy(sizePolicy)
self.uiNodesView.setDragEnabled(False)
- self.uiNodesView.setIconSize(QtCore.QSize(32, 32))
+ self.uiNodesView.setIconSize(QtCore.QSize(24, 24))
self.uiNodesView.setRootIsDecorated(False)
self.uiNodesView.setObjectName(_fromUtf8("uiNodesView"))
self.uiNodesView.header().setVisible(False)
@@ -217,22 +217,22 @@ class Ui_MainWindow(object):
self.uiOnlineHelpAction.setObjectName(_fromUtf8("uiOnlineHelpAction"))
self.uiScreenshotAction = QtGui.QAction(MainWindow)
icon4 = QtGui.QIcon()
- icon4.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/camera-photo.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon4.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/camera-photo-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon4.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/camera-photo.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiScreenshotAction.setIcon(icon4)
self.uiScreenshotAction.setObjectName(_fromUtf8("uiScreenshotAction"))
self.uiStartAllAction = QtGui.QAction(MainWindow)
self.uiStartAllAction.setEnabled(True)
icon5 = QtGui.QIcon()
- icon5.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/start.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon5.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/start-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon5.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/start.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiStartAllAction.setIcon(icon5)
self.uiStartAllAction.setObjectName(_fromUtf8("uiStartAllAction"))
self.uiStopAllAction = QtGui.QAction(MainWindow)
self.uiStopAllAction.setEnabled(True)
icon6 = QtGui.QIcon()
- icon6.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/stop.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon6.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/stop-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon6.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/stop.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiStopAllAction.setIcon(icon6)
self.uiStopAllAction.setObjectName(_fromUtf8("uiStopAllAction"))
self.uiShowNamesAction = QtGui.QAction(MainWindow)
@@ -252,14 +252,14 @@ class Ui_MainWindow(object):
self.uiAboutQtAction.setObjectName(_fromUtf8("uiAboutQtAction"))
self.uiZoomInAction = QtGui.QAction(MainWindow)
icon9 = QtGui.QIcon()
- icon9.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/zoom-in.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon9.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/zoom-in-hover.png")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon9.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/zoom-in.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiZoomInAction.setIcon(icon9)
self.uiZoomInAction.setObjectName(_fromUtf8("uiZoomInAction"))
self.uiZoomOutAction = QtGui.QAction(MainWindow)
icon10 = QtGui.QIcon()
- icon10.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/zoom-out.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon10.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/zoom-out-hover.png")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon10.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/zoom-out.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiZoomOutAction.setIcon(icon10)
self.uiZoomOutAction.setObjectName(_fromUtf8("uiZoomOutAction"))
self.uiZoomResetAction = QtGui.QAction(MainWindow)
@@ -275,8 +275,8 @@ class Ui_MainWindow(object):
self.uiPreferencesAction.setObjectName(_fromUtf8("uiPreferencesAction"))
self.uiSuspendAllAction = QtGui.QAction(MainWindow)
icon12 = QtGui.QIcon()
- icon12.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/pause.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon12.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/pause-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon12.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/pause.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiSuspendAllAction.setIcon(icon12)
self.uiSuspendAllAction.setObjectName(_fromUtf8("uiSuspendAllAction"))
self.uiAddNoteAction = QtGui.QAction(MainWindow)
@@ -304,15 +304,15 @@ class Ui_MainWindow(object):
self.uiDrawRectangleAction = QtGui.QAction(MainWindow)
self.uiDrawRectangleAction.setCheckable(True)
icon17 = QtGui.QIcon()
- icon17.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/rectangle.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon17.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/rectangle-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon17.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/rectangle.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiDrawRectangleAction.setIcon(icon17)
self.uiDrawRectangleAction.setObjectName(_fromUtf8("uiDrawRectangleAction"))
self.uiDrawEllipseAction = QtGui.QAction(MainWindow)
self.uiDrawEllipseAction.setCheckable(True)
icon18 = QtGui.QIcon()
- icon18.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/ellipse.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon18.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/ellipse-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon18.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/ellipse.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiDrawEllipseAction.setIcon(icon18)
self.uiDrawEllipseAction.setObjectName(_fromUtf8("uiDrawEllipseAction"))
self.uiShowPortNamesAction = QtGui.QAction(MainWindow)
@@ -358,41 +358,41 @@ class Ui_MainWindow(object):
self.uiDefaultStyleAction.setObjectName(_fromUtf8("uiDefaultStyleAction"))
self.uiBrowseRoutersAction = QtGui.QAction(MainWindow)
icon24 = QtGui.QIcon()
- icon24.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/router.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon24.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/router-hover.png")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon24.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/router.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseRoutersAction.setIcon(icon24)
self.uiBrowseRoutersAction.setObjectName(_fromUtf8("uiBrowseRoutersAction"))
self.uiBrowseSwitchesAction = QtGui.QAction(MainWindow)
icon25 = QtGui.QIcon()
- icon25.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/switch.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon25.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/switch-hover.png")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon25.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/switch.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseSwitchesAction.setIcon(icon25)
self.uiBrowseSwitchesAction.setObjectName(_fromUtf8("uiBrowseSwitchesAction"))
self.uiBrowseEndDevicesAction = QtGui.QAction(MainWindow)
icon26 = QtGui.QIcon()
- icon26.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/PC.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon26.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/PC-hover.png")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon26.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/PC.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseEndDevicesAction.setIcon(icon26)
self.uiBrowseEndDevicesAction.setObjectName(_fromUtf8("uiBrowseEndDevicesAction"))
self.uiBrowseSecurityDevicesAction = QtGui.QAction(MainWindow)
icon27 = QtGui.QIcon()
- icon27.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/firewall.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon27.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/firewall-hover.png")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon27.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/firewall.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseSecurityDevicesAction.setIcon(icon27)
self.uiBrowseSecurityDevicesAction.setObjectName(_fromUtf8("uiBrowseSecurityDevicesAction"))
self.uiBrowseAllDevicesAction = QtGui.QAction(MainWindow)
icon28 = QtGui.QIcon()
- icon28.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/browse-all-icons.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon28.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/browse-all-icons-hover.png")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon28.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/browse-all-icons.png")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiBrowseAllDevicesAction.setIcon(icon28)
self.uiBrowseAllDevicesAction.setObjectName(_fromUtf8("uiBrowseAllDevicesAction"))
self.uiAddLinkAction = QtGui.QAction(MainWindow)
self.uiAddLinkAction.setCheckable(True)
icon29 = QtGui.QIcon()
- icon29.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/connection-new.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
- icon29.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/cancel-connection.svg")), QtGui.QIcon.Normal, QtGui.QIcon.On)
- icon29.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/connection-new-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
icon29.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/cancel-connection.svg")), QtGui.QIcon.Active, QtGui.QIcon.On)
+ icon29.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/connection-new-hover.svg")), QtGui.QIcon.Active, QtGui.QIcon.Off)
+ icon29.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/cancel-connection.svg")), QtGui.QIcon.Normal, QtGui.QIcon.On)
+ icon29.addPixmap(QtGui.QPixmap(_fromUtf8(":/icons/connection-new.svg")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
self.uiAddLinkAction.setIcon(icon29)
self.uiAddLinkAction.setObjectName(_fromUtf8("uiAddLinkAction"))
self.uiGettingStartedAction = QtGui.QAction(MainWindow)
@@ -405,6 +405,10 @@ class Ui_MainWindow(object):
self.uiExportProjectAction.setObjectName(_fromUtf8("uiExportProjectAction"))
self.uiImportProjectAction = QtGui.QAction(MainWindow)
self.uiImportProjectAction.setObjectName(_fromUtf8("uiImportProjectAction"))
+ self.uiMoveLocalProjectToCloudAction = QtGui.QAction(MainWindow)
+ self.uiMoveLocalProjectToCloudAction.setObjectName(_fromUtf8("uiMoveLocalProjectToCloudAction"))
+ self.uiMoveCloudProjectToLocalAction = QtGui.QAction(MainWindow)
+ self.uiMoveCloudProjectToLocalAction.setObjectName(_fromUtf8("uiMoveCloudProjectToLocalAction"))
self.uiDarkStyleAction = QtGui.QAction(MainWindow)
self.uiDarkStyleAction.setObjectName(_fromUtf8("uiDarkStyleAction"))
self.uiActionFullscreen = QtGui.QAction(MainWindow)
@@ -417,8 +421,11 @@ class Ui_MainWindow(object):
self.uiFileMenu.addAction(self.uiOpenProjectAction)
self.uiFileMenu.addAction(self.uiSaveProjectAction)
self.uiFileMenu.addAction(self.uiSaveProjectAsAction)
- self.uiFileMenu.addAction(self.uiImportProjectAction)
self.uiFileMenu.addAction(self.uiExportProjectAction)
+ self.uiFileMenu.addAction(self.uiImportProjectAction)
+ self.uiFileMenu.addSeparator()
+ self.uiFileMenu.addAction(self.uiMoveLocalProjectToCloudAction)
+ self.uiFileMenu.addAction(self.uiMoveCloudProjectToLocalAction)
self.uiFileMenu.addSeparator()
self.uiFileMenu.addAction(self.uiImportExportConfigsAction)
self.uiFileMenu.addAction(self.uiScreenshotAction)
@@ -647,15 +654,17 @@ class Ui_MainWindow(object):
self.uiGettingStartedAction.setToolTip(_translate("MainWindow", "Show GNS3 news", None))
self.uiLabInstructionsAction.setText(_translate("MainWindow", "Lab instructions", None))
self.uiFitInViewAction.setText(_translate("MainWindow", "Fit in view", None))
- self.uiExportProjectAction.setText(_translate("MainWindow", "Export project", None))
- self.uiImportProjectAction.setText(_translate("MainWindow", "Import project", None))
+ self.uiExportProjectAction.setText(_translate("MainWindow", "Backup project to cloud", None))
+ self.uiImportProjectAction.setText(_translate("MainWindow", "Restore backup from cloud", None))
+ self.uiMoveLocalProjectToCloudAction.setText(_translate("MainWindow", "Move local project to cloud", None))
+ self.uiMoveCloudProjectToLocalAction.setText(_translate("MainWindow", "Move cloud project to local", None))
self.uiDarkStyleAction.setText(_translate("MainWindow", "Dark Style", None))
self.uiActionFullscreen.setText(_translate("MainWindow", "Fullscreen", None))
self.uiActionFullscreen.setShortcut(_translate("MainWindow", "Ctrl+F", None))
-from ..cloud_inspector_view import CloudInspectorView
-from ..console_view import ConsoleView
+from ..topology_summary_view import TopologySummaryView
from ..nodes_view import NodesView
from ..graphics_view import GraphicsView
-from ..topology_summary_view import TopologySummaryView
+from ..cloud_inspector_view import CloudInspectorView
+from ..console_view import ConsoleView
from . import resources_rc
diff --git a/gns3/websocket_client.py b/gns3/websocket_client.py
index bfc1b7f1..1b28ad4f 100644
--- a/gns3/websocket_client.py
+++ b/gns3/websocket_client.py
@@ -355,7 +355,8 @@ class WebSocketClient(WebSocketBaseClient):
return {"id": self._id,
"host": self.host,
"port": self.port,
- "local": self._local}
+ "local": self._local,
+ "cloud": self._cloud}
def _heartbeat(self):
self.send_notification("deadman.heartbeat")
diff --git a/scripts/ssh_to_server.py b/scripts/ssh_to_server.py
index c810144c..4053e7e0 100644
--- a/scripts/ssh_to_server.py
+++ b/scripts/ssh_to_server.py
@@ -88,8 +88,9 @@ def read_cloud_settings():
host = settings.value('host')
private_key = settings.value('private_key')
public_key = settings.value('public_key')
+ uid = settings.value('id')
- instances.append((name, host, private_key, public_key))
+ instances.append((name, host, private_key, public_key, uid))
if len(instances) == 0:
raise Exception("Could not find any servers")
@@ -103,7 +104,7 @@ def main():
instances = read_cloud_settings()
if options['action'] == 'ssh':
- name, host, private_key, public_key = instances[int(options['server'])-1]
+ name, host, private_key, public_key, uid = instances[int(options['server'])-1]
print('Instance name: {}'.format(name))
print('Host ip: {}'.format(host))
@@ -119,13 +120,13 @@ def main():
print(cmd)
os.system(cmd)
elif options['action'] == 'list':
- print('ID Name')
+ print('ID Name IP UID')
for idx, info in enumerate(instances):
- name, host, private_key, public_key = info
- print('{:2d} {}'.format(idx+1, name))
+ name, host, private_key, public_key, uid = info
+ print('{:2d} {} {} {}'.format(idx+1, name, host, uid))
return 0
if __name__ == "__main__":
- sys.exit(main())
\ No newline at end of file
+ sys.exit(main())