Show a summary with server usages

Fix #963
This commit is contained in:
Julien Duponchelle
2016-01-26 18:52:53 +01:00
parent cb94fc4d1e
commit d62a32c7d7
8 changed files with 251 additions and 42 deletions

View File

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

View File

@@ -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

View File

@@ -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"])

102
gns3/server_summary_view.py Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
"""
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)

View File

@@ -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):

View File

@@ -472,6 +472,48 @@ background-none;
</layout>
</widget>
</widget>
<widget class="QDockWidget" name="uiServerSummaryDockWidget">
<property name="allowedAreas">
<set>Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea</set>
</property>
<property name="windowTitle">
<string>Servers Summary</string>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="ServerSummaryView" name="uiServerSummaryTreeWidget">
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
<action name="uiAboutAction">
<property name="text">
<string>&amp;About</string>
@@ -1219,7 +1261,7 @@ background-none;
<string>Import appliance</string>
</property>
</action>
<action name="uiExportDebugInformationsAction">
<action name="uiExportDebugInformationAction">
<property name="text">
<string>Export debug informations</string>
</property>
@@ -1246,6 +1288,11 @@ background-none;
<extends>QTreeWidget</extends>
<header>..topology_summary_view.h</header>
</customwidget>
<customwidget>
<class>ServerSummaryView</class>
<extends>QTreeWidget</extends>
<header>..server_summary_view.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>uiGraphicsView</tabstop>

View File

@@ -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

View File

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