From d62a32c7d723c22530cae9cf3c929daaba747f60 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Tue, 26 Jan 2016 18:52:53 +0100 Subject: [PATCH] Show a summary with server usages Fix #963 --- gns3/http_client.py | 20 +++++-- gns3/network_client.py | 9 ---- gns3/project.py | 6 ++- gns3/server_summary_view.py | 102 ++++++++++++++++++++++++++++++++++++ gns3/servers.py | 45 +++++++++++++--- gns3/ui/main_window.ui | 49 ++++++++++++++++- gns3/ui/main_window_ui.py | 58 ++++++++++++-------- tests/test_servers.py | 4 ++ 8 files changed, 251 insertions(+), 42 deletions(-) create mode 100644 gns3/server_summary_view.py diff --git a/gns3/http_client.py b/gns3/http_client.py index 3b5a9251..9976e0b3 100644 --- a/gns3/http_client.py +++ b/gns3/http_client.py @@ -54,7 +54,9 @@ class HTTPClient(QtCore.QObject): # Callback class used for displaying progress _progress_callback = None - connected_signal = QtCore.Signal() + connection_connected_signal = QtCore.Signal() + connection_closed_signal = QtCore.Signal() + system_usage_updated_signal = QtCore.Signal() connection_error_signal = QtCore.Signal(str) def __init__(self, settings, network_manager): @@ -79,6 +81,7 @@ class HTTPClient(QtCore.QObject): self._ram_limit = settings.get("ram_limit", 0) self._allocated_ram = 0 self._accept_insecure_certificate = settings.get("accept_insecure_certificate", None) + self._usage = None self._network_manager = network_manager @@ -267,6 +270,7 @@ class HTTPClient(QtCore.QObject): """ log.info("Connection to %s closed", self.url()) self._connected = False + self.connection_closed_signal.emit() def isLocalServerRunning(self): """ @@ -488,6 +492,7 @@ class HTTPClient(QtCore.QObject): return self._connected = True + self.connection_connected_signal.emit() kwargs["context"] = original_context self.executeHTTPQuery(method, path, callback, body, **kwargs) self._version = params["version"] @@ -768,5 +773,14 @@ class HTTPClient(QtCore.QObject): server["accept_insecure_certificate"] = self._accept_insecure_certificate return server - def isCloud(self): - return False + def systemUsage(self): + """ + Get information about current system usage + + :returns: None or dict + """ + return self._usage + + def setSystemUsage(self, usage): + self._usage = usage + self.system_usage_updated_signal.emit() diff --git a/gns3/network_client.py b/gns3/network_client.py index e285e0df..5d55a8ef 100644 --- a/gns3/network_client.py +++ b/gns3/network_client.py @@ -18,15 +18,6 @@ import ipaddress -def getNetworkClientInstance(settings, network_manager): - """ - Based on url return a network client instance - """ - - from gns3.http_client import HTTPClient - return HTTPClient(settings, network_manager) - - def getNetworkUrl(protocol, host, port, user=None, settings={}): """ Return a network url from settings diff --git a/gns3/project.py b/gns3/project.py index 1c61de5b..f5fb945d 100644 --- a/gns3/project.py +++ b/gns3/project.py @@ -369,7 +369,7 @@ class Project(QtCore.QObject): path = "/projects/{project_id}/notifications".format(project_id=self._id) self._notifications_stream.add(server.createHTTPQuery("GET", path, None, downloadProgressCallback=self._event_received, showProgress=False, ignoreErrors=True)) - def _event_received(self, result, **kwargs): + def _event_received(self, result, server=None, **kwargs): log.debug("Event received: %s", result) if result["action"] in ["vm.started", "vm.stopped"]: @@ -387,3 +387,7 @@ class Project(QtCore.QObject): elif result["action"] == "log.warning": log.warning(result["event"]["message"]) print("Warning: " + result["event"]["message"]) + elif result["action"] == "ping": + # Compatible with 1.4.0 server + if "event" in result: + server.setSystemUsage(result["event"]) diff --git a/gns3/server_summary_view.py b/gns3/server_summary_view.py new file mode 100644 index 00000000..e5ffc448 --- /dev/null +++ b/gns3/server_summary_view.py @@ -0,0 +1,102 @@ +# -*- 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 . + +""" +Server summary view that list all the server, their status. +""" + +import sip + +from .qt import QtGui, QtCore, QtWidgets +from .servers import Servers + +import logging +log = logging.getLogger(__name__) + + +class ServerItem(QtWidgets.QTreeWidgetItem): + + """ + Custom item for the QTreeWidget instance + (topology summary view). + + :param parent: parent widget + :param server: Server instance + """ + + def __init__(self, parent, server): + + super().__init__(parent) + self._server = server + self._parent = parent + + self._server.connection_connected_signal.connect(self._refreshStatusSlot) + self._server.connection_closed_signal.connect(self._refreshStatusSlot) + self._server.system_usage_updated_signal.connect(self._refreshStatusSlot) + self._refreshStatusSlot() + + def _refreshStatusSlot(self): + """ + Changes the icon to show the node status (started, stopped etc.) + """ + + usage = self._server.systemUsage() + + if self._server.isLocal(): + text = "Local" + elif self._server.isGNS3VM(): + text = "GNS3 VM" + else: + text = self._server.url() + + if usage is not None and usage["cpu_usage_percent"] > 0.0: + text = "{} CPU {}%, RAM {}%".format(text, usage["cpu_usage_percent"], usage["memory_usage_percent"]) + + self.setText(0, text) + if self._server.connected(): + if usage is None or (usage["cpu_usage_percent"] < 90 and usage["memory_usage_percent"] < 90): + self.setIcon(0, QtGui.QIcon(':/icons/led_green.svg')) + else: + self.setIcon(0, QtGui.QIcon(':/icons/led_yellow.svg')) + else: + self.setIcon(0, QtGui.QIcon(':/icons/led_red.svg')) + + +class ServerSummaryView(QtWidgets.QTreeWidget): + + """ + Server summary view implementation. + + :param parent: parent widget + """ + + def __init__(self, parent): + + super().__init__(parent) + Servers.instance().server_added_signal.connect(self._serverAddedSlot) + for server in Servers.instance().servers(): + self._serverAddedSlot(server.url()) + + def _serverAddedSlot(self, url): + """ + Called when a server is added to the list of servers + + :params url: URL of the server + """ + server = Servers.instance().getServerFromString(url) + ServerItem(self, server) + diff --git a/gns3/servers.py b/gns3/servers.py index a77de2d9..f675c2a3 100644 --- a/gns3/servers.py +++ b/gns3/servers.py @@ -34,8 +34,8 @@ import stat import struct import psutil -from .qt import QtNetwork, QtWidgets -from .network_client import getNetworkClientInstance, getNetworkUrl +from .qt import QtNetwork, QtWidgets, QtCore +from .network_client import getNetworkUrl from .local_config import LocalConfig from .settings import SERVERS_SETTINGS from .local_server_config import LocalServerConfig @@ -48,14 +48,17 @@ import logging log = logging.getLogger(__name__) -class Servers(): +class Servers(QtCore.QObject): """ Server management class. """ + server_added_signal = QtCore.Signal(str) + def __init__(self): + super().__init__() self._settings = {} self._local_server = None self._vm_server = None @@ -71,6 +74,17 @@ class Servers(): self._pid_path = os.path.join(LocalConfig.configDirectory(), "gns3_server.pid") self.registerLocalServer() + def servers(self): + """ + Return the list of all servers, remote, vm and local + """ + servers = list(self._remote_servers.values()) + if self._local_server: + servers.append(self._local_server) + if self._vm_server: + servers.append(self._vm_server) + return servers + def registerLocalServer(self): """ Register a new local server. @@ -81,11 +95,13 @@ class Servers(): port = local_server_settings["port"] user = local_server_settings["user"] password = local_server_settings["password"] - self._local_server = getNetworkClientInstance({"host": host, "port": port, "user": user, "password": password}, + self._local_server = self.getNetworkClientInstance({"host": host, "port": port, "user": user, "password": password}, self._network_manager) self._local_server.setLocal(True) + self.server_added_signal.emit("local") log.info("New local server connection {} registered".format(self._local_server.url())) + @staticmethod def _findLocalServer(self): """ @@ -603,10 +619,11 @@ class Servers(): "user": gns3_vm_settings["user"], "password": gns3_vm_settings["password"] } - server = getNetworkClientInstance(server_info, self._network_manager) + server = self.getNetworkClientInstance(server_info, self._network_manager) server.setLocal(False) server.setGNS3VM(True) self._vm_server = server + self.server_added_signal.emit("vm") log.info("GNS3 VM server initialized {}".format(server.url())) def vmServer(self): @@ -641,13 +658,23 @@ class Servers(): "password": password} if accept_insecure_certificate: server["accept_insecure_certificate"] = accept_insecure_certificate - server = getNetworkClientInstance(server, self._network_manager) + server = self.getNetworkClientInstance(server, self._network_manager) server.setLocal(False) self._remote_servers[server.url()] = server + self.server_added_signal.emit(server.url()) log.info("New remote server connection {} registered".format(server.url())) return server + def getNetworkClientInstance(self, settings, network_manager): + """ + Based on url return a network client instance + """ + + from gns3.http_client import HTTPClient + client = HTTPClient(settings, network_manager) + return client + def getRemoteServer(self, protocol, host, port, user, settings={}): """ Gets a remote server. @@ -685,6 +712,9 @@ class Servers(): return self.anyRemoteServer() if "://" in server_name: + for server in self.servers(): + if server.url() == server_name: + return server url_settings = urllib.parse.urlparse(server_name) settings = {} port = url_settings.port @@ -732,9 +762,10 @@ class Servers(): if server_id in self._remote_servers: continue - new_server = getNetworkClientInstance(server, self._network_manager) + new_server = self.getNetworkClientInstance(server, self._network_manager) new_server.setLocal(False) self._remote_servers[server_id] = new_server + self.server_added_signal.emit(new_server.url()) log.info("New remote server connection {} registered".format(new_server.url())) def remoteServers(self): diff --git a/gns3/ui/main_window.ui b/gns3/ui/main_window.ui index 09f6d63e..9d3dec0c 100644 --- a/gns3/ui/main_window.ui +++ b/gns3/ui/main_window.ui @@ -472,6 +472,48 @@ background-none; + + + Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea + + + Servers Summary + + + 2 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + + 1 + + + + + + + &About @@ -1219,7 +1261,7 @@ background-none; Import appliance - + Export debug informations @@ -1246,6 +1288,11 @@ background-none; QTreeWidget
..topology_summary_view.h
+ + ServerSummaryView + QTreeWidget +
..server_summary_view.h
+
uiGraphicsView diff --git a/gns3/ui/main_window_ui.py b/gns3/ui/main_window_ui.py index a4dcb6dd..178e5f53 100644 --- a/gns3/ui/main_window_ui.py +++ b/gns3/ui/main_window_ui.py @@ -8,30 +8,28 @@ from PyQt5 import QtCore, QtGui, QtWidgets - class Ui_MainWindow(object): - def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.setWindowModality(QtCore.Qt.NonModal) MainWindow.resize(984, 715) MainWindow.setContextMenuPolicy(QtCore.Qt.PreventContextMenu) MainWindow.setStyleSheet("#toolBar_Devices QToolButton {\n" - "width: 50px;\n" - "height: 55px;\n" - "border:solid 1px black opacity 0.4;\n" - "background-none;\n" - "}\n" - "\n" - "#toolBar_General QToolButton {\n" - "width: 36px;\n" - "height: 36px;\n" - "border:solid 1px black opacity 0.4;\n" - "background-none;\n" - "}\n" - "\n" - "") - MainWindow.setDockOptions(QtWidgets.QMainWindow.AllowTabbedDocks | QtWidgets.QMainWindow.AnimatedDocks) +"width: 50px;\n" +"height: 55px;\n" +"border:solid 1px black opacity 0.4;\n" +"background-none;\n" +"}\n" +"\n" +"#toolBar_General QToolButton {\n" +"width: 36px;\n" +"height: 36px;\n" +"border:solid 1px black opacity 0.4;\n" +"background-none;\n" +"}\n" +"\n" +"") + MainWindow.setDockOptions(QtWidgets.QMainWindow.AllowTabbedDocks|QtWidgets.QMainWindow.AnimatedDocks) self.uiCentralWidget = QtWidgets.QWidget(MainWindow) self.uiCentralWidget.setObjectName("uiCentralWidget") self.gridlayout = QtWidgets.QGridLayout(self.uiCentralWidget) @@ -83,7 +81,7 @@ class Ui_MainWindow(object): self.uiNodesDockWidget.setEnabled(True) self.uiNodesDockWidget.setVisible(True) self.uiNodesDockWidget.setFloating(False) - self.uiNodesDockWidget.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea) + self.uiNodesDockWidget.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea|QtCore.Qt.RightDockWidgetArea) self.uiNodesDockWidget.setObjectName("uiNodesDockWidget") self.uiNodesDockWidgetContents = QtWidgets.QWidget() self.uiNodesDockWidgetContents.setObjectName("uiNodesDockWidgetContents") @@ -145,7 +143,7 @@ class Ui_MainWindow(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.uiTopologySummaryDockWidget.sizePolicy().hasHeightForWidth()) self.uiTopologySummaryDockWidget.setSizePolicy(sizePolicy) - self.uiTopologySummaryDockWidget.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea) + self.uiTopologySummaryDockWidget.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea|QtCore.Qt.RightDockWidgetArea) self.uiTopologySummaryDockWidget.setObjectName("uiTopologySummaryDockWidget") self.uiTopologySummaryDockWidgetContents = QtWidgets.QWidget() sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) @@ -169,6 +167,22 @@ class Ui_MainWindow(object): self.gridlayout1.addWidget(self.uiTopologySummaryTreeWidget, 0, 0, 1, 1) self.uiTopologySummaryDockWidget.setWidget(self.uiTopologySummaryDockWidgetContents) MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.uiTopologySummaryDockWidget) + self.uiServerSummaryDockWidget = QtWidgets.QDockWidget(MainWindow) + self.uiServerSummaryDockWidget.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea|QtCore.Qt.RightDockWidgetArea) + self.uiServerSummaryDockWidget.setObjectName("uiServerSummaryDockWidget") + self.dockWidgetContents = QtWidgets.QWidget() + self.dockWidgetContents.setObjectName("dockWidgetContents") + self.gridLayout = QtWidgets.QGridLayout(self.dockWidgetContents) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.uiServerSummaryTreeWidget = ServerSummaryView(self.dockWidgetContents) + self.uiServerSummaryTreeWidget.setObjectName("uiServerSummaryTreeWidget") + self.uiServerSummaryTreeWidget.headerItem().setText(0, "1") + self.uiServerSummaryTreeWidget.header().setVisible(False) + self.gridLayout.addWidget(self.uiServerSummaryTreeWidget, 0, 0, 1, 1) + self.uiServerSummaryDockWidget.setWidget(self.dockWidgetContents) + MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.uiServerSummaryDockWidget) self.uiAboutAction = QtWidgets.QAction(MainWindow) self.uiAboutAction.setMenuRole(QtWidgets.QAction.AboutRole) self.uiAboutAction.setObjectName("uiAboutAction") @@ -367,10 +381,10 @@ class Ui_MainWindow(object): self.uiAddLinkAction = QtWidgets.QAction(MainWindow) self.uiAddLinkAction.setCheckable(True) icon29 = QtGui.QIcon() + icon29.addPixmap(QtGui.QPixmap(":/icons/cancel-connection.svg"), QtGui.QIcon.Active, QtGui.QIcon.On) icon29.addPixmap(QtGui.QPixmap(":/icons/connection-new.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off) icon29.addPixmap(QtGui.QPixmap(":/icons/cancel-connection.svg"), QtGui.QIcon.Normal, QtGui.QIcon.On) icon29.addPixmap(QtGui.QPixmap(":/icons/connection-new-hover.svg"), QtGui.QIcon.Active, QtGui.QIcon.Off) - icon29.addPixmap(QtGui.QPixmap(":/icons/cancel-connection.svg"), QtGui.QIcon.Active, QtGui.QIcon.On) self.uiAddLinkAction.setIcon(icon29) self.uiAddLinkAction.setObjectName("uiAddLinkAction") self.uiGettingStartedAction = QtWidgets.QAction(MainWindow) @@ -536,6 +550,7 @@ class Ui_MainWindow(object): self.uiAnnotationToolBar.setWindowTitle(_translate("MainWindow", "Drawing")) self.uiTopologySummaryDockWidget.setWindowTitle(_translate("MainWindow", "Topology Summary")) self.uiTopologySummaryTreeWidget.headerItem().setText(0, _translate("MainWindow", "1")) + self.uiServerSummaryDockWidget.setWindowTitle(_translate("MainWindow", "Servers Summary")) self.uiAboutAction.setText(_translate("MainWindow", "&About")) self.uiAboutAction.setStatusTip(_translate("MainWindow", "About")) self.uiQuitAction.setText(_translate("MainWindow", "&Quit")) @@ -676,10 +691,11 @@ class Ui_MainWindow(object): self.uiSetupWizard.setText(_translate("MainWindow", "&Setup Wizard")) self.uiIOUVMConverterAction.setText(_translate("MainWindow", "IOU VM Converter")) self.uiOpenApplianceAction.setText(_translate("MainWindow", "Import appliance")) - self.uiExportDebugInformationAction.setText(_translate("MainWindow", "Export debug information")) + self.uiExportDebugInformationAction.setText(_translate("MainWindow", "Export debug informations")) from ..console_view import ConsoleView from ..graphics_view import GraphicsView from ..nodes_view import NodesView +from ..server_summary_view import ServerSummaryView from ..topology_summary_view import TopologySummaryView from . import resources_rc diff --git a/tests/test_servers.py b/tests/test_servers.py index 23844843..2157f376 100644 --- a/tests/test_servers.py +++ b/tests/test_servers.py @@ -98,6 +98,10 @@ def test_loadSettingsWith13LocalServerSetting(tmpdir, local_config): assert local_server["user"] == "world" assert local_server["password"] == "hello" +def testServers(): + servers = Servers.instance() + http_server = servers.getRemoteServer("http", "localhost", 8000, None) + assert len(servers.servers()) == 2 def test_getRemoteServer(): servers = Servers.instance()