mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-05-17 00:46:01 +03:00
Merge pull request: Move local project to cloud #168
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
253
gns3/cloud_builder.py
Normal file
253
gns3/cloud_builder.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2014 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
from PyQt4.QtCore import pyqtSignal
|
||||
from PyQt4.QtCore import QThread
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from .cloud.utils import ssh_client
|
||||
from .cloud.exceptions import KeyPairExists
|
||||
|
||||
from .servers import Servers
|
||||
from .topology import Topology
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudBuilder(QThread):
|
||||
"""
|
||||
"""
|
||||
# Notify with progress amount and instance_id
|
||||
progressUpdate = pyqtSignal(object, str)
|
||||
|
||||
# Notify with current state and instance_id
|
||||
stateChange = pyqtSignal(object, str)
|
||||
|
||||
# Notify when instance is ready with instance_id
|
||||
buildComplete = pyqtSignal(str)
|
||||
|
||||
# Notify when the instance has been created with instance and keypair
|
||||
instanceCreated = pyqtSignal(object, object)
|
||||
|
||||
# Notify when the public ip is available with ip and instance_id
|
||||
instanceHasIP = pyqtSignal(str, str)
|
||||
|
||||
# Notify when instance id exists with builder and instance_id
|
||||
instanceIdExists = pyqtSignal(object, str)
|
||||
|
||||
|
||||
def __init__(self, parent, cloud_provider, ca_dir):
|
||||
super(QThread, self).__init__(parent)
|
||||
# Store our parent so it can be passed to threads we spawn.
|
||||
self._parent = parent
|
||||
self._provider = cloud_provider
|
||||
self._ca_dir = ca_dir
|
||||
self._start_at_create = False
|
||||
self._start_at_setup = False
|
||||
self._instance = None
|
||||
|
||||
def startAtCreate(self, instance_name, flavor_id, image_id):
|
||||
self._start_at_create = True
|
||||
self._instance_name = instance_name
|
||||
self._flavor_id = flavor_id
|
||||
self._image_id = image_id
|
||||
|
||||
def startAtSetup(self, instance, keypair):
|
||||
self._start_at_setup = True
|
||||
self._instance = instance
|
||||
self._key_pair = keypair
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
log.debug('CloudBuilder.run')
|
||||
if self._start_at_create:
|
||||
log.debug('CloudBuilder._start_at_create')
|
||||
self._createInstance(self._provider, self._instance_name, self._flavor_id,
|
||||
self._image_id)
|
||||
log.debug('got here 3')
|
||||
if self._start_at_setup:
|
||||
log.debug('CloudBuilder start at setup')
|
||||
self._instanceCreated(self._instance, self._key_pair)
|
||||
except Exception:
|
||||
log.exception("CloudBuilder trapped an exception:")
|
||||
log.error('CloudBuilder stopped in error state.')
|
||||
|
||||
def _createInstance(self, provider, name, flavor_id, image_id):
|
||||
log.debug("Creating cloud keypair with name {}".format(name))
|
||||
key_pair = None
|
||||
while key_pair is None:
|
||||
try:
|
||||
key_pair = provider.create_key_pair(name)
|
||||
except KeyPairExists:
|
||||
log.debug("Deleting old key pair with name {}.".format(name))
|
||||
self._provider.delete_key_pair_by_name(name)
|
||||
except Exception as e:
|
||||
log.debug("create_key_pair exception {}".format(e))
|
||||
|
||||
log.debug("Creating cloud server with name {}".format(name))
|
||||
instance = None
|
||||
while instance is None:
|
||||
try:
|
||||
instance = self._provider.create_instance(name, flavor_id, image_id, key_pair)
|
||||
except Exception as e:
|
||||
log.debug("create_instance exception {}".format(e))
|
||||
log.debug("Cloud server {} created".format(name))
|
||||
self._instanceCreated(instance, key_pair)
|
||||
|
||||
def _instanceCreated(self, instance, key_pair):
|
||||
log.debug('CloudBuilder._instanceCreated {}'.format(instance.id))
|
||||
self._instance = instance
|
||||
self._instance_id = instance.id
|
||||
self._key_pair = key_pair
|
||||
self.instanceIdExists.emit(self, instance.id)
|
||||
self.instanceCreated.emit(instance, key_pair)
|
||||
self._waitForPublicIP()
|
||||
|
||||
|
||||
def _waitForPublicIP(self):
|
||||
public_ip = None
|
||||
while public_ip is None:
|
||||
time.sleep(10)
|
||||
try:
|
||||
instance = self._provider.get_instance(self._instance)
|
||||
# Look for public ip address
|
||||
for ip in instance.public_ips:
|
||||
# Don't use the ipv6 address
|
||||
if ':' not in ip:
|
||||
public_ip = ip
|
||||
break
|
||||
except Exception as e:
|
||||
log.debug('list_instances error: {}'.format(e))
|
||||
|
||||
# updated info, keep it.
|
||||
self._instance = instance
|
||||
self._public_ip = public_ip
|
||||
self.instanceHasIP.emit(self._public_ip, self._instance.id)
|
||||
time.sleep(60)
|
||||
self._startGNS3Server(1800)
|
||||
|
||||
def _startGNS3Server(self, dead_time):
|
||||
commands = '''
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --configure -a
|
||||
DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confnew" --force-yes -fuy dist-upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install git python3-setuptools python3-netifaces python3-pip python3-zmq dynamips qemu-system
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install libc6:i386 libstdc++6:i386 libssl1.0.0:i386
|
||||
ln -s /lib/i386-linux-gnu/libcrypto.so.1.0.0 /lib/i386-linux-gnu/libcrypto.so.4
|
||||
mkdir -p /opt/gns3
|
||||
cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git
|
||||
cd /opt/gns3/gns3-server; git checkout dev; git pull
|
||||
cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt
|
||||
cd /opt/gns3/gns3-server; python3 ./setup.py install
|
||||
ln -sf /usr/bin/dynamips /usr/local/bin/dynamips
|
||||
wget 'https://github.com/GNS3/iouyap/releases/download/0.95/iouyap.tar.gz'
|
||||
tar xzf iouyap.tar.gz -C /usr/local/bin
|
||||
python -c 'import struct; open("/etc/hostid", "w").write(struct.pack("i", 00000000))'
|
||||
hostname gns3-iouvm # set hostname for iou
|
||||
wget 'http://downloads.sourceforge.net/project/vpcs/0.6/vpcs_0.6_Linux64'
|
||||
cp vpcs_0.6_Linux64 /usr/local/bin/vpcs
|
||||
chmod a+x /usr/local/bin/vpcs
|
||||
killall python3 gns3server gns3dms
|
||||
'''
|
||||
def exec_command(client, cmd, wait_time=-1):
|
||||
|
||||
cmd += '; exit $?'
|
||||
|
||||
stdout_data = b''
|
||||
stderr_data = b''
|
||||
|
||||
log.debug('cmd: {}'.format(cmd))
|
||||
# Send the command (non-blocking)
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
|
||||
# Wait for the command to terminate
|
||||
wait = int(wait_time)
|
||||
while not stdout.channel.exit_status_ready() and wait != 0:
|
||||
time.sleep(1)
|
||||
wait -= 1
|
||||
|
||||
stdout_data = stdout.read()
|
||||
stderr_data = stderr.read()
|
||||
log.debug('exit status: {}'.format(stdout.channel.exit_status))
|
||||
log.debug('stdout: {}'.format(stdout_data.decode('utf-8')))
|
||||
log.debug('stderr: {}'.format(stderr_data.decode('utf-8')))
|
||||
return stdout_data, stderr_data
|
||||
|
||||
|
||||
# We might be attempting a connection before the instance is fully booted, so retry
|
||||
# when the ssh connection fails.
|
||||
ssh_connected = False
|
||||
response = None
|
||||
while not ssh_connected:
|
||||
with ssh_client(self._public_ip, self._key_pair.private_key) as client:
|
||||
if client is None:
|
||||
time.sleep(1)
|
||||
continue
|
||||
ssh_connected = True
|
||||
|
||||
for cmd in [l for l in commands.splitlines() if l.strip()]:
|
||||
exec_command(client, cmd)
|
||||
|
||||
data = {
|
||||
'instance_id': self._instance_id,
|
||||
'cloud_user_name': self._provider.username,
|
||||
'cloud_api_key': self._provider.api_key,
|
||||
'cloud_region': self._provider.region,
|
||||
'dead_time': dead_time,
|
||||
}
|
||||
# TODO: Properly escape the data portion of the command line
|
||||
start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._public_ip, data)
|
||||
stdout, stderr = exec_command(client, start_cmd, wait_time=15)
|
||||
response = stdout.decode('utf-8')
|
||||
|
||||
log.debug(response)
|
||||
data = ast.literal_eval(response)
|
||||
# TODO: have the server return the port it is running on
|
||||
port = 8000
|
||||
|
||||
username = data['WEB_USERNAME']
|
||||
password = data['WEB_PASSWORD']
|
||||
|
||||
ssl_cert = ''.join(data['SSL_CRT'])
|
||||
ca_filename = 'cloud_server_{}.crt'.format(self._public_ip)
|
||||
ca_dir = self._ca_dir
|
||||
ca_file = os.path.join(ca_dir, ca_filename)
|
||||
try:
|
||||
os.makedirs(ca_dir)
|
||||
except FileExistsError:
|
||||
pass
|
||||
with open(ca_file, 'wb') as ca_fh:
|
||||
ca_fh.write(ssl_cert.encode('utf-8'))
|
||||
|
||||
topology = Topology.instance()
|
||||
top_instance = topology.getInstance(self._instance_id)
|
||||
top_instance.set_later_attributes(self._public_ip, port, ssl_cert, ca_file)
|
||||
|
||||
servers = Servers.instance()
|
||||
server = servers.getCloudServer(self._public_ip, port, ca_file, username, password,
|
||||
self._key_pair.private_key, self._instance_id)
|
||||
servers.save()
|
||||
log.debug('Cloud server gns3server started.')
|
||||
self.buildComplete.emit(self._instance_id)
|
||||
@@ -1,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -85,8 +85,11 @@ background-none;
|
||||
<addaction name="uiOpenProjectAction"/>
|
||||
<addaction name="uiSaveProjectAction"/>
|
||||
<addaction name="uiSaveProjectAsAction"/>
|
||||
<addaction name="uiImportProjectAction"/>
|
||||
<addaction name="uiExportProjectAction"/>
|
||||
<addaction name="uiImportProjectAction"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="uiMoveLocalProjectToCloudAction"/>
|
||||
<addaction name="uiMoveCloudProjectToLocalAction"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="uiImportExportConfigsAction"/>
|
||||
<addaction name="uiScreenshotAction"/>
|
||||
@@ -1109,12 +1112,22 @@ background-none;
|
||||
</action>
|
||||
<action name="uiExportProjectAction">
|
||||
<property name="text">
|
||||
<string>Export project</string>
|
||||
<string>Backup project to cloud</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="uiImportProjectAction">
|
||||
<property name="text">
|
||||
<string>Import project</string>
|
||||
<string>Restore backup from cloud</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="uiMoveLocalProjectToCloudAction">
|
||||
<property name="text">
|
||||
<string>Move local project to cloud</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="uiMoveCloudProjectToLocalAction">
|
||||
<property name="text">
|
||||
<string>Move cloud project to local</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="uiDarkStyleAction">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user