From 120cb895264eab154ee89d96e4f585fea0e714c7 Mon Sep 17 00:00:00 2001 From: Jerry Seutter Date: Mon, 27 Oct 2014 18:11:39 -0600 Subject: [PATCH] IOS devices can be deployed on cloud instances. --- gns3/cloud/utils.py | 127 +++++++++--------- gns3/cloud_inspector_view.py | 20 ++- gns3/graphics_view.py | 23 ++++ gns3/main_window.py | 4 +- gns3/modules/dynamips/__init__.py | 33 ++++- .../dynamips/dialogs/ios_router_wizard.py | 31 +++-- gns3/modules/dynamips/nodes/router.py | 3 + .../pages/ios_router_preferences_page.py | 26 ++++ gns3/servers.py | 22 ++- gns3/topology.py | 51 ++++++- gns3/websocket_client.py | 68 +++++++--- scripts/ssh_to_server.py | 47 +++++++ 12 files changed, 342 insertions(+), 113 deletions(-) create mode 100644 scripts/ssh_to_server.py diff --git a/gns3/cloud/utils.py b/gns3/cloud/utils.py index 1c0e747b..eade1eb3 100644 --- a/gns3/cloud/utils.py +++ b/gns3/cloud/utils.py @@ -8,7 +8,6 @@ import tempfile import time import zipfile -from PyQt4 import QtCore from PyQt4.QtCore import QThread from PyQt4.QtCore import pyqtSignal @@ -117,35 +116,34 @@ class StartGNS3ServerThread(QThread): """ gns3server_started = pyqtSignal(str, str, str) -# # Note: The htop package is for troubleshooting. It can safely be removed. + # Note: The htop package is for troubleshooting. It can safely be removed. + commands = ''' +DEBIAN_FRONTEND=noninteractive apt-get -y update +DEBIAN_FRONTEND=noninteractive apt-get -y install htop +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 +mkdir -p /opt/gns3 +tar xzf /tmp/gns3-server.tgz -C /opt/gns3 +cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt +cd /opt/gns3/gns3-server; python3 ./setup.py install +ln -sf /usr/bin/dynamips /usr/local/bin/dynamips +killall python3 gns3server gns3dms +''' + # commands = ''' -# echo 'hello world' +# DEBIAN_FRONTEND=noninteractive dpkg --configure -a # DEBIAN_FRONTEND=noninteractive apt-get -y update # DEBIAN_FRONTEND=noninteractive apt-get -y install htop # 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 # mkdir -p /opt/gns3 -# tar xzf /tmp/gns3-server.tgz -C /opt/gns3 +# cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git +# cd /opt/gns3/gns3-server; git checkout gns-110 # cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt # cd /opt/gns3/gns3-server; python3 ./setup.py install # killall gns3server gns3dms # ''' - commands = ''' -echo 'hello world' -DEBIAN_FRONTEND=noninteractive dpkg --configure -a -DEBIAN_FRONTEND=noninteractive apt-get -y update -DEBIAN_FRONTEND=noninteractive apt-get -y install htop -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 -mkdir -p /opt/gns3 -cd /opt/gns3; git clone https://github.com/planctechnologies/gns3-server.git -cd /opt/gns3/gns3-server; git checkout dev -cd /opt/gns3/gns3-server; pip3 install -r dev-requirements.txt -cd /opt/gns3/gns3-server; python3 ./setup.py install -killall gns3server gns3dms -''' - def __init__(self, parent, host, private_key_string, server_id, username, api_key, region, dead_time): super(QThread, self).__init__(parent) self._host = host @@ -186,10 +184,6 @@ killall gns3server gns3dms def run(self): # Uncomment this at the same time as the commands above to test without having to push # changes to github. - # os.system('rm -rf /tmp/gns3-server') - # os.system('cp -a /Users/jseutter/projects/gns3-server-newinstancerework /tmp/gns3-server') - # os.system('cd /tmp; tar czf /tmp/gns3-server.tgz gns3-server') - # os.system('scp /tmp/gns3-server.tgz root@{}:/tmp/'.format(self._host)) # We might be attempting a connection before the instance is fully booted, so retry # when the ssh connection fails. @@ -201,6 +195,13 @@ killall gns3server gns3dms continue ssh_connected = True + os.system('rm -rf /tmp/gns3-server') + os.system('cp -a /Users/jseutter/projects/gns3-server /tmp/gns3-server') + os.system('cd /tmp; tar czf /tmp/gns3-server.tgz gns3-server') + sftp = client.open_sftp() + sftp.put('/tmp/gns3-server.tgz', '/tmp/gns3-server.tgz') + sftp.close() + for cmd in [l for l in self.commands.splitlines() if l.strip()]: self.exec_command(client, cmd) @@ -208,11 +209,11 @@ killall gns3server gns3dms 'instance_id': self._server_id, 'cloud_user_name': self._username, 'cloud_api_key': self._api_key, - 'region': self._region, + 'cloud_region': self._region, 'dead_time': self._dead_time, } # TODO: Properly escape the data portion of the command line - start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --data="{}"'.format(data) + start_cmd = '/usr/bin/python3 /opt/gns3/gns3-server/gns3server/start_server.py -d -v --ip={} --data="{}" 2>/tmp/gns3-stderr.log'.format(self._host, data) stdout, stderr = self.exec_command(client, start_cmd, wait_time=15) response = stdout.decode('utf-8') self.gns3server_started.emit(str(self._server_id), str(self._host), str(response)) @@ -225,13 +226,15 @@ class WSConnectThread(QThread): """ established = pyqtSignal(str) - def __init__(self, parent, provider, server_id, host, port, ca_file): + def __init__(self, parent, provider, server_id, host, port, ca_file, auth_user, auth_password): super(QThread, self).__init__(parent) self._provider = provider self._server_id = server_id self._host = host self._port = port self._ca_file = ca_file + self._auth_user = auth_user + self._auth_password = auth_password def run(self): """ @@ -240,7 +243,7 @@ class WSConnectThread(QThread): log.debug('WSConnectThread.run() begin') servers = Servers.instance() - server = servers.getCloudServer(self._host, self._port, self._ca_file) + server = servers.getCloudServer(self._host, self._port, self._ca_file, self._auth_user, self._auth_password) log.debug('after getCloudServer call. {}'.format(server)) self.established.emit(str(self._server_id)) @@ -253,42 +256,23 @@ class UploadProjectThread(QThread): """ Zip and Upload project to the cloud """ - - # signals to update the progress dialog. - error = QtCore.pyqtSignal(str, bool) - completed = QtCore.pyqtSignal() - update = QtCore.pyqtSignal(int) - def __init__(self, project_settings, cloud_settings): super().__init__() self.project_settings = project_settings self.cloud_settings = cloud_settings def run(self): - try: - log.info("Exporting project to cloud") - self.update.emit(0) + log.info("Exporting project to cloud") + zipped_project_file = self.zip_project_dir() - zipped_project_file = self.zip_project_dir() + provider = get_provider(self.cloud_settings) + provider.upload_file(zipped_project_file, 'projects') - self.update.emit(10) # update progress to 10% + topology = Topology.instance() + images = set([node.settings()["image"] for node in topology.nodes() if 'image' in node.settings()]) - provider = get_provider(self.cloud_settings) - provider.upload_file(zipped_project_file, 'projects') - - self.update.emit(20) # update progress to 20% - - topology = Topology.instance() - images = set([node.settings()["image"] for node in topology.nodes() if 'image' in node.settings()]) - - for i, image in enumerate(images): - provider.upload_file(image, 'images') - self.update.emit(20 + (float(i) / len(images) * 80)) - - self.completed.emit() - except Exception as e: - log.exception("Error exporting project to cloud") - self.error.emit("Error exporting project {}".format(str(e)), True) + for image in images: + provider.upload_file(image, 'images') def zip_project_dir(self): """ @@ -305,19 +289,34 @@ class UploadProjectThread(QThread): zip_file.write(root, os.path.relpath(root, relroot)) for file in files: filename = os.path.join(root, file) - if os.path.isfile(filename) and not self._should_exclude(filename): # regular files only + if os.path.isfile(filename): # regular files only arcname = os.path.join(os.path.relpath(root, relroot), file) zip_file.write(filename, arcname) return output_filename - def _should_exclude(self, filename): - """ - Returns True if file should be excluded from zip of project files - :param filename: - :return: True if file should be excluded from zip, False otherwise - """ - return filename.endswith('.ghost') +class UploadFileThread(QThread): + """ + Upload an file to cloud files + """ + def __init__(self, cloud_settings, router_settings): + super().__init__() + self._cloud_settings = cloud_settings + self._router_settings = router_settings + self.completed_callback = None - def stop(self): - self.quit() + + def run(self): + disk_path = self._router_settings['path'] + filename = self._router_settings['image'] + + log.debug('Uploading image {}'.format(disk_path)) + log.debug('Cloud filename: {}'.format(filename)) + provider = get_provider(self._cloud_settings) + provider.upload_file(disk_path, 'images') + + self._cloud_settings['image'] = filename + + log.debug('Uploading image completed') + if self.completed_callback: + self.completed_callback() diff --git a/gns3/cloud_inspector_view.py b/gns3/cloud_inspector_view.py index c3957fa9..414fb48c 100644 --- a/gns3/cloud_inspector_view.py +++ b/gns3/cloud_inspector_view.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import ast import logging +import os from PyQt4.QtGui import QWidget from PyQt4.QtGui import QIcon from PyQt4.QtGui import QMenu @@ -311,12 +312,23 @@ class CloudInspectorView(QWidget, Ui_CloudInspectorView): password = data['WEB_PASSWORD'] ssl_cert = ''.join(data['SSL_CRT']) - # TODO: Store the cert file in an appropriate spot - ca_file = '/tmp/cloud_server_{}.crt'.format(host_ip) - open(ca_file, 'w').write(ssl_cert) + 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) log.debug('Cloud server gns3server started.') - wss_thread = WSConnectThread(self, self._provider, id, host_ip, port, ca_file) + wss_thread = WSConnectThread(self, self._provider, id, host_ip, port, ca_file, username, password) wss_thread.established.connect(self._wss_connected_slot) wss_thread.start() diff --git a/gns3/graphics_view.py b/gns3/graphics_view.py index 1ba67395..1d89fb2e 100644 --- a/gns3/graphics_view.py +++ b/gns3/graphics_view.py @@ -19,6 +19,7 @@ Graphical view on the scene where items are drawn. """ +import logging import os import pickle @@ -51,6 +52,8 @@ from .items.rectangle_item import RectangleItem from .items.ellipse_item import EllipseItem from .items.image_item import ImageItem +log = logging.getLogger(__name__) + class GraphicsView(QtGui.QGraphicsView): """ @@ -1095,6 +1098,7 @@ class GraphicsView(QtGui.QGraphicsView): """ try: + log.debug('In createNode') node_module = None for module in MODULES: instance = module.instance() @@ -1108,11 +1112,30 @@ class GraphicsView(QtGui.QGraphicsView): if node_data["server"] == "local": server = Servers.instance().localServer() + elif node_data["server"] == "cloud": + server = Servers.instance().anyCloudServer() else: host, port = node_data["server"].rsplit(":", 1) server = Servers.instance().getRemoteServer(host, port) if not server.connected() and ConnectToServer(self, server) is False: return + # if self._topology.resourcesType == "cloud": + # use_cloud = True + # else: + # use_cloud = False + # log.debug("use_cloud is set to {}".format(use_cloud)) + # + # server = node_module.allocateServer(node_class, use_cloud) + # if not server.connected(): + # # connect to server in a non-blocking way. + # self._thread = WaitForConnectionThread(server.host, server.port) + # progress_dialog = ProgressDialog(self._thread, + # "Server", + # "Connecting to server {} on port {}...".format(server.host, server.port), + # "Cancel", busy=True, parent=self) + # progress_dialog.show() + # if progress_dialog.exec_() is False: + # return node = node_module.createNode(node_class, server) node.error_signal.connect(self._main_window.uiConsoleTextEdit.writeError) diff --git a/gns3/main_window.py b/gns3/main_window.py index 2f1b5a8f..7322e1b6 100644 --- a/gns3/main_window.py +++ b/gns3/main_window.py @@ -1617,7 +1617,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): index = 0 for instance in instances: settings.setArrayIndex(index) - for name in instance._fields: + for name in instance.fields(): log.debug('{}={}'.format(name, getattr(instance, name))) settings.setValue(name, getattr(instance, name)) index += 1 @@ -1638,7 +1638,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): for index in range(0, size): settings.setArrayIndex(index) info = {} - for name in TopologyInstance._fields: + for name in TopologyInstance.fields(): log.debug('{}={}'.format(name, settings.value(name, ""))) info[name] = settings.value(name, "") topology.addInstance(**info) diff --git a/gns3/modules/dynamips/__init__.py b/gns3/modules/dynamips/__init__.py index 0472dee1..5e2a9ad0 100644 --- a/gns3/modules/dynamips/__init__.py +++ b/gns3/modules/dynamips/__init__.py @@ -387,6 +387,34 @@ class Dynamips(Module): params.update({"project_name": project_name}) server.send_notification("dynamips.settings", params) + def allocateServer(self, node_class, use_cloud=False): + """ + Allocates a server. + + :param node_class: Node object + + :returns: allocated server (WebSocketClient instance) + """ + + # allocate a server for the node + servers = Servers.instance() + + if use_cloud: + from ...topology import Topology + topology = Topology.instance() + top_instance = topology.anyInstance() + server = servers.getCloudServer(top_instance.host, top_instance.port, top_instance.ssl_ca_file) + else: + if self._settings["use_local_server"]: + # use the local server + server = servers.localServer() + else: + # pick up a remote server (round-robin method) + server = next(iter(servers)) + if not server: + raise ModuleError("No remote server is configured") + return server + def createNode(self, node_class, server): """ Creates a new node. @@ -468,7 +496,10 @@ class Dynamips(Module): if wic in ios_router: settings[wic] = ios_router[wic] - node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings) + if node.server().isCloud(): + node.setup(ios_router["image"], ios_router["ram"], initial_settings=settings) + else: + node.setup(ios_router["path"], ios_router["ram"], initial_settings=settings) else: node.setup() diff --git a/gns3/modules/dynamips/dialogs/ios_router_wizard.py b/gns3/modules/dynamips/dialogs/ios_router_wizard.py index b6902e08..808b27e3 100644 --- a/gns3/modules/dynamips/dialogs/ios_router_wizard.py +++ b/gns3/modules/dynamips/dialogs/ios_router_wizard.py @@ -305,12 +305,12 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard): Validates the IOS name. """ - if self.currentPage() == self.uiServerWizardPage: - - #FIXME: prevent users to use "cloud" - if self.uiCloudRadioButton.isChecked(): - QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!") - return False + # if self.currentPage() == self.uiServerWizardPage: + # + # #FIXME: prevent users to use "cloud" + # if self.uiCloudRadioButton.isChecked(): + # QtGui.QMessageBox.critical(self, "Cloud", "Sorry not implemented yet!") + # return False # if self.currentPage() == self.uiNameImageWizardPage: # name = self.uiNameLineEdit.text() @@ -347,14 +347,17 @@ class IOSRouterWizard(QtGui.QWizard, Ui_IOSRouterWizard): path = self.uiIOSImageLineEdit.text() if Dynamips.instance().settings()["use_local_server"] or self.uiLocalRadioButton.isChecked(): server = "local" - elif self.uiLoadBalanceCheckBox.isChecked(): - server = next(iter(Servers.instance())) - if not server: - QtGui.QMessageBox.critical(self, "IOS router", "No remote server available!") - return - server = "{}:{}".format(server.host, server.port) - else: - server = self.uiRemoteServersComboBox.currentText() + elif self.uiRemoteRadioButton.isChecked(): + if self.uiLoadBalanceCheckBox.isChecked(): + server = next(iter(Servers.instance())) + if not server: + QtGui.QMessageBox.critical(self, "IOS router", "No remote server available!") + return + server = "{}:{}".format(server.host, server.port) + else: + server = self.uiRemoteServersComboBox.currentText() + else: # Cloud is selected + server = "cloud" settings = { "name": self.uiNameLineEdit.text(), diff --git a/gns3/modules/dynamips/nodes/router.py b/gns3/modules/dynamips/nodes/router.py index 3d64279a..a47c8e1d 100644 --- a/gns3/modules/dynamips/nodes/router.py +++ b/gns3/modules/dynamips/nodes/router.py @@ -263,6 +263,9 @@ class Router(Node): if "chassis" in initial_settings: params["chassis"] = self._settings["chassis"] = initial_settings.pop("chassis") + #TODO: make this only send when dealing with cloud + params["cloud_path"] = "images/IOS" + # other initial settings will be applied when the router has been created if initial_settings: self._inital_settings = initial_settings diff --git a/gns3/modules/dynamips/pages/ios_router_preferences_page.py b/gns3/modules/dynamips/pages/ios_router_preferences_page.py index bffcfe98..a9be3549 100644 --- a/gns3/modules/dynamips/pages/ios_router_preferences_page.py +++ b/gns3/modules/dynamips/pages/ios_router_preferences_page.py @@ -32,6 +32,7 @@ from gns3.main_window import MainWindow from gns3.utils.progress_dialog import ProgressDialog from gns3.dialogs.symbol_selection_dialog import SymbolSelectionDialog from gns3.dialogs.configuration_dialog import ConfigurationDialog +from gns3.cloud.utils import UploadFileThread from ..utils.decompress_ios import isIOSCompressed from ..utils.decompress_ios_thread import DecompressIOSThread @@ -127,6 +128,26 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget) "system_id": "FTX0945W0MY", "server": ios_settings["server"]} + if ios_settings["server"] == 'cloud': + import logging + log = logging.getLogger(__name__) + + log.debug(ios_settings["image"]) + # Start uploading the image to cloud files + + self._upload_image_progress_dialog = QtGui.QProgressDialog("Uploading image file {}".format(ios_settings['image']), "Cancel", 0, 0, parent=self) + self._upload_image_progress_dialog.setWindowModality(QtCore.Qt.WindowModal) + self._upload_image_progress_dialog.setWindowTitle("IOS image upload") + self._upload_image_progress_dialog.show() + try: + upload_thread = UploadFileThread(MainWindow.instance().cloudSettings(), self._ios_routers[key]) + upload_thread.completed_callback = self._imageUploadComplete + upload_thread.start() + except Exception as e: + self._upload_image_progress_dialog.reject() + log.error(e) + QtGui.QMessageBox.critical(self, "IOS image upload", "Error uploading IOS image: {}".format(e)) + if ios_settings["platform"] == "c7200": self._ios_routers[key]["midplane"] = "vxr" self._ios_routers[key]["npe"] = "npe-400" @@ -151,6 +172,11 @@ class IOSRouterPreferencesPage(QtGui.QWidget, Ui_IOSRouterPreferencesPageWidget) self._items.append(item) self.uiIOSRoutersTreeWidget.setCurrentItem(item) + def _imageUploadComplete(self): + if self._upload_image_progress_dialog.wasCanceled(): + return + self._upload_image_progress_dialog.accept() + def _iosRouterEditSlot(self): """ Edits an IOS router. diff --git a/gns3/servers.py b/gns3/servers.py index 41b4520d..3e7b477e 100644 --- a/gns3/servers.py +++ b/gns3/servers.py @@ -250,7 +250,7 @@ class Servers(QtCore.QObject): def getRemoteServer(self, host, port): for server in self._remote_servers.values(): - if server.host == host and server.port == port: + if server.host == host and int(server.port) == int(port): return server return self._addRemoteServer(host, port) @@ -291,7 +291,7 @@ class Servers(QtCore.QObject): return self._remote_servers - def getCloudServer(self, host, port, ca_file): + def getCloudServer(self, host, port, ca_file, auth_user, auth_password): """ Return a websocket connection to the cloud server, creating one if none exists. @@ -302,13 +302,13 @@ class Servers(QtCore.QObject): """ for server in self._remote_servers.values(): - if server.host == host and server.port == port: + if server.host == host and int(server.port) == int(port): return server heartbeat_freq = self._settings.value("heartbeat_freq", DEFAULT_HEARTBEAT_FREQ) - return self._addCloudServer(host, port, ca_file, heartbeat_freq) - - def _addCloudServer(self, host, port, ca_file, heartbeat_freq): + return self._addCloudServer(host, port, ca_file, auth_user, auth_password, heartbeat_freq) + + def _addCloudServer(self, host, port, ca_file, auth_user, auth_password, heartbeat_freq): """ Create a websocket connection to the specified cloud server @@ -323,12 +323,20 @@ class Servers(QtCore.QObject): url = "wss://{host}:{port}".format(host=host, port=port) log.debug('Starting SecureWebSocketClient url={}'.format(url)) log.debug('Starting SecureWebSocketClient ca_file={}'.format(ca_file)) - server = SecureWebSocketClient(url, ca_file) + server = SecureWebSocketClient(url) + server.setSecureOptions(ca_file, auth_user, auth_password) + server.setCloud(True) server.enableHeartbeatsAt(heartbeat_freq) self._cloud_servers[host] = server log.info("new remote server connection {} registered".format(url)) return server + def anyCloudServer(self): + # Return the first server for now + for key, value in self._cloud_servers.items(): + return value + return None + def __iter__(self): """ Creates a round-robin system to pick up a remote server. diff --git a/gns3/topology.py b/gns3/topology.py index cfc385ae..e25261a8 100644 --- a/gns3/topology.py +++ b/gns3/topology.py @@ -40,9 +40,43 @@ from pkg_resources import parse_version import logging log = logging.getLogger(__name__) -TopologyInstance = namedtuple("TopologyInstance", - ["name", "id", "size_id", "image_id", "private_key", "public_key"], - verbose=False) + +# TopologyInstance = namedtuple("TopologyInstance", +# ["name", "id", "size_id", "image_id", "private_key", "public_key", +# "host", "port", "ssl_ca", "ssl_ca_file"], +# verbose=False) + +class TopologyInstance: + def __init__(self, name, id, size_id, image_id, private_key, public_key, + host=None, port=None, ssl_ca=None, ssl_ca_file=None): + # host, port, ssl_ca and ssl_ca_file are not known when the instance is created. + # They will typically be set at a later point in time. + self.name = name + self.id = id + self.size_id = size_id + self.image_id = image_id + self.public_key = public_key + self.private_key = private_key + self.host = host + self.port = port + self.ssl_ca = ssl_ca + self.ssl_ca_file = ssl_ca_file + + @classmethod + def fields(cls): + return ["name", "id", "size_id", "image_id", "private_key", "public_key", + "host", "port", "ssl_ca", "ssl_ca_file"] + + def set_later_attributes(self, host, port, ssl_ca, ssl_ca_file): + """ + Set attributes that are not known at the time of cloud instance creation. + """ + self.host = host + self.port = port + self.ssl_ca = ssl_ca + self.ssl_ca_file = ssl_ca_file + + class Topology(object): @@ -203,13 +237,16 @@ class Topology(object): if image in self._images: self._images.remove(image) - def addInstance(self, name, id, size_id, image_id, private_key, public_key): + def addInstance(self, name, id, size_id, image_id, private_key, public_key, + host=None, port=None, ssl_ca=None, ssl_ca_file=None): """ Add an instance to this cloud topology """ i = TopologyInstance(name=name, id=id, size_id=size_id, image_id=image_id, - private_key=private_key, public_key=public_key) + private_key=private_key, public_key=public_key, host=host, + port=port, ssl_ca=ssl_ca, ssl_ca_file=ssl_ca_file) + self._instances.append(i) def removeInstance(self, id): @@ -236,6 +273,10 @@ class Topology(object): if instance.id == id: return instance + def anyInstance(self): + # For now, just return the first instance + return self._instances[0] + def nodes(self): """ Returns all the nodes in this topology. diff --git a/gns3/websocket_client.py b/gns3/websocket_client.py index c2f78454..223dfc3a 100644 --- a/gns3/websocket_client.py +++ b/gns3/websocket_client.py @@ -56,6 +56,7 @@ class WebSocketClient(WebSocketBaseClient): self.callbacks = {} self._connected = False self._local = False + self._cloud = False self._version = "" self._fd_notifier = None self._heartbeat_timer = None @@ -108,6 +109,12 @@ class WebSocketClient(WebSocketBaseClient): return self._local + def setCloud(self, value): + self._cloud = value + + def isCloud(self): + return self._cloud + def opened(self): """ Called when the connection with the server is successful. @@ -141,7 +148,7 @@ class WebSocketClient(WebSocketBaseClient): except OSError: raise except Exception as e: - log.error("could to connect {}: {}".format(self.url, e)) + log.error("could not to connect {}: {}".format(self.url, e)) raise OSError("Websocket exception {}: {}".format(type(e), e)) def check_server_version(self): @@ -353,28 +360,57 @@ class WebSocketClient(WebSocketBaseClient): class SecureWebSocketClient(WebSocketClient): - def connect(self, ca_file=''): + def __init__(self, url, protocols=None, extensions=None, + heartbeat_freq=None, ssl_options=None, headers=None): + self.use_auth = True - self.use_ssl = True + self.use_ssl = False - self.ca_file = ca_file - self.login_url = "https://{host}:{port}/login".format(host=self.host, port=self.port) - self.version_url = "https://{host}:{port}/version".format(host=self.host, port=self.port) - self.websocket_url = "wss://{host}:{port}".format(host=self.host, port=self.port) - self.auth_user = 'test123' - self.auth_password = 'test456' + # The url has to be set before the constructor is called + scheme, rest = url.split(':', 1) + if self.use_ssl: + url = "wss:{}".format(rest) + else: + url = "ws:{}".format(rest) + + WebSocketClient.__init__(self, url, + protocols, + extensions, + heartbeat_freq, + ssl_options, + headers) + + + def setSecureOptions(self, ca_file, auth_user, auth_password): + self._ca_file = ca_file + self._auth_user = auth_user + self._auth_password = auth_password + + def connect(self): + log.debug('In SecureWebSocketClient.connect()') + + import ssl + import socket + + if self.use_ssl: + self.login_url = "https://{host}:{port}/login".format(host=self.host, port=self.port) + self.version_url = "https://{host}:{port}/version".format(host=self.host, port=self.port) + self.ssl_options = {'ca_certs': self._ca_file} + context = ssl._create_stdlib_context(cert_reqs=ssl.CERT_REQUIRED, cafile=self._ca_file) + self.https_handler = urllib.request.HTTPSHandler(context=context, check_hostname=False) + else: + self.login_url = "http://{host}:{port}/login".format(host=self.host, port=self.port) + self.version_url = "http://{host}:{port}/version".format(host=self.host, port=self.port) + self.ssl_options = {} + self.https_handler = urllib.request.HTTPHandler() - self.ssl_options = {'ca_certs': self.ca_file} - self.https_handler = urllib.request.HTTPSHandler(check_hostname=False) self.cookie_processor = urllib.request.HTTPCookieProcessor() self.opener = urllib.request.build_opener(self.https_handler, self.cookie_processor) self.check_server_version() - - data = urllib.parse.urlencode({'name': self.auth_user, 'password': self.auth_password}).encode('utf-8') - urllib.request.install_opener(self.opener) - f = urllib.request.urlopen(self.login_url, data, cafile=self.ca_file) - log.debug(self.cookie_processor.cookiejar) + data = urllib.parse.urlencode({'name': self._auth_user, 'password': self._auth_password}).encode('utf-8') + f = self.opener.open(self.login_url, data, socket._GLOBAL_DEFAULT_TIMEOUT) + log.debug('login result: {}'.format(f.read())) self._connect() log.debug(self.sock) diff --git a/scripts/ssh_to_server.py b/scripts/ssh_to_server.py new file mode 100644 index 00000000..36a8f0f6 --- /dev/null +++ b/scripts/ssh_to_server.py @@ -0,0 +1,47 @@ +import os +from PyQt4 import QtCore, QtGui + +QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat) + +app = QtGui.QApplication([]) +app.setOrganizationName("GNS3") +app.setOrganizationDomain("gns3.net") +app.setApplicationName("GNS3") + +settings = QtCore.QSettings() + +print('Reading config from {}'.format(QtCore.QSettings().fileName())) + +def read_cloud_settings(): + settings = QtCore.QSettings() + settings.beginGroup("CloudInstances") + + # Load the instances + size = settings.beginReadArray("cloud_instance") + for index in range(0, size): + settings.setArrayIndex(index) + name = settings.value('name') + host = settings.value('host') + private_key = settings.value('private_key') + public_key = settings.value('public_key') + + # For now, just use the first system. + return name, host, private_key, public_key + + + +name, host, private_key, public_key = read_cloud_settings() + +print(name) +print(host) +print(private_key) +print(public_key) + + +open('/tmp/id_rsa.pub', 'w').write(public_key) +open('/tmp/id_rsa', 'w').write(private_key) +os.system('chmod 0600 /tmp/id_rsa') + +cmd = 'ssh -i /tmp/id_rsa root@{}'.format(host) +os.system(cmd) +