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())