mirror of
https://github.com/nicolargo/glances.git
synced 2024-11-25 08:33:22 +03:00
Merge branch 'develop'
This commit is contained in:
commit
0bb34a8d7f
3
AUTHORS
3
AUTHORS
@ -48,3 +48,6 @@ Frederic Aoustin (https://github.com/fraoustin) and Nicolas Bourges (installer)
|
||||
|
||||
Aljaž Srebrnič for the MacPorts package
|
||||
http://www.macports.org/ports.php?by=name&substr=glances
|
||||
|
||||
John Kirkham for the conda package (at conda-forge)
|
||||
https://github.com/conda-forge/glances-feedstock
|
||||
|
14
NEWS
14
NEWS
@ -2,6 +2,19 @@
|
||||
Glances Version 2
|
||||
==============================================================================
|
||||
|
||||
Version 2.6.1
|
||||
=============
|
||||
|
||||
Enhancements and new features:
|
||||
|
||||
* Add a connector to Riemann (issue #822 by Greogo Nagy)
|
||||
|
||||
Bugs corrected:
|
||||
|
||||
* Browsing for servers which are in the [serverlist] is broken (issue #819)
|
||||
* [WebUI] Glances will not get past loading screen (issue #815) opened 9 days ago
|
||||
* Python error after upgrading from 2.5.1 to 2.6 bug (issue #813)
|
||||
|
||||
Version 2.6
|
||||
===========
|
||||
|
||||
@ -34,6 +47,7 @@ Enhancements and new features:
|
||||
* InfluxDB > 0.9.3 needs float and not int for numerical value (issue#749 and issue#750 by nicolargo)
|
||||
|
||||
Bugs corrected:
|
||||
|
||||
* Can't read sensors on a Thinkpad (issue #711)
|
||||
* InfluxDB/OpenTSDB: tag parsing broken (issue #713)
|
||||
* Grafana Dashboard outdated for InfluxDB 0.9.x (issue #648)
|
||||
|
23
README.rst
23
README.rst
@ -50,6 +50,7 @@ Optional dependencies:
|
||||
- ``docker-py`` (for the Docker monitoring support) [Linux-only]
|
||||
- ``matplotlib`` (for graphical/chart support)
|
||||
- ``pika`` (for the RabbitMQ/ActiveMQ export module)
|
||||
- ``bernhard`` (for the Riemann export module)
|
||||
- ``py-cpuinfo`` (for the Quicklook CPU info module)
|
||||
- ``scandir`` (for the Folders plugin) [Only for Python < 3.5]
|
||||
|
||||
@ -223,6 +224,26 @@ Puppet
|
||||
|
||||
You can install Glances using ``Puppet``: https://github.com/rverchere/puppet-glances
|
||||
|
||||
Known issue on RHEL/CentOS/Fedora installation
|
||||
==============================================
|
||||
|
||||
For Python 2.6 RedHat-based distros there might be an issue with starting Glances:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/usr/bin/glances", line 5, in <module>
|
||||
from pkg_resources import load_entry_point
|
||||
File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 2655, in <module>
|
||||
workingset.require(_requires)
|
||||
File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 648, in require
|
||||
needed = self.resolve(parse_requirements(requirements))
|
||||
File "/usr/lib/python2.6/site-packages/pkg_resources.py", line 546, in resolve
|
||||
raise DistributionNotFound(req)
|
||||
pkg_resources.DistributionNotFound: argparse
|
||||
|
||||
Try upgrading setuptools, has been proven to solve the problem:
|
||||
|
||||
sudo pip install -U setuptools
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
@ -274,7 +295,7 @@ Gateway to other services
|
||||
=========================
|
||||
|
||||
Glances can export stats to: ``CSV`` file, ``InfluxDB``, ``OpenTSDB``,
|
||||
``StatsD`` and ``RabbitMQ`` server.
|
||||
``StatsD``, ``RabbitMQ`` and ``Riemann`` server.
|
||||
|
||||
How to contribute ?
|
||||
===================
|
||||
|
@ -198,7 +198,7 @@ mem_critical=90
|
||||
#default=defaultpassword
|
||||
|
||||
[influxdb]
|
||||
# Configuration file for the --export-influxdb option
|
||||
# Configuration for the --export-influxdb option
|
||||
# https://influxdb.com/
|
||||
host=localhost
|
||||
port=8086
|
||||
@ -209,7 +209,7 @@ prefix=localhost
|
||||
#tags=foo:bar,spam:eggs
|
||||
|
||||
[opentsdb]
|
||||
# Configuration file for the --export-opentsdb option
|
||||
# Configuration for the --export-opentsdb option
|
||||
# http://opentsdb.net/
|
||||
host=localhost
|
||||
port=4242
|
||||
@ -217,23 +217,30 @@ port=4242
|
||||
#tags=foo:bar,spam:eggs
|
||||
|
||||
[statsd]
|
||||
# Configuration file for the --export-statsd option
|
||||
# Configuration file the --export-statsd option
|
||||
# https://github.com/etsy/statsd
|
||||
host=localhost
|
||||
port=8125
|
||||
#prefix=glances
|
||||
|
||||
[elasticsearch]
|
||||
# Configuration file for the --export-elasticsearch option
|
||||
# Configuration file the --export-elasticsearch option
|
||||
# Data are available via the ES Restful API. ex: URL/<index>/cpu/system
|
||||
# https://www.elastic.co
|
||||
host=localhost
|
||||
port=9200
|
||||
index=glances
|
||||
|
||||
[riemann]
|
||||
# Configuration file for the --export-riemann option
|
||||
# http://riemann.io
|
||||
host=localhost
|
||||
port=5555
|
||||
|
||||
[rabbitmq]
|
||||
host=localhost
|
||||
port=5672
|
||||
user=guest
|
||||
password=guest
|
||||
queue=glances_queue
|
||||
|
||||
|
@ -139,6 +139,10 @@ Command-Line Options
|
||||
|
||||
export stats to RabbitMQ broker (pika lib needed)
|
||||
|
||||
.. option:: --export-riemann
|
||||
|
||||
export stats to Riemann server (bernhard lib needed)
|
||||
|
||||
.. option:: --export-elasticsearch
|
||||
|
||||
export stats to an Elasticsearch server (elasticsearch lib needed)
|
||||
|
20
docs/gw/riemann.rst
Normal file
20
docs/gw/riemann.rst
Normal file
@ -0,0 +1,20 @@
|
||||
.. _riemann:
|
||||
|
||||
Riemann
|
||||
========
|
||||
|
||||
You can export statistics to an ``Riemann`` server (using TCP protocol). The
|
||||
connection should be defined in the Glances configuration file as
|
||||
following:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[riemann]
|
||||
host=localhost
|
||||
port=5555
|
||||
|
||||
and run Glances with:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ glances --export-riemann
|
@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "GLANCES" "1" "March 16, 2016" "2.6" "Glances"
|
||||
.TH "GLANCES" "1" "March 26, 2016" "2.6.1" "Glances"
|
||||
.SH NAME
|
||||
glances \- An eye on your system
|
||||
.
|
||||
@ -214,6 +214,11 @@ export stats to RabbitMQ broker (pika lib needed)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-export\-riemann
|
||||
export stats to Riemann server (bernhard lib needed)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-export\-elasticsearch
|
||||
export stats to an Elasticsearch server (elasticsearch lib needed)
|
||||
.UNINDENT
|
||||
|
@ -27,13 +27,13 @@ import sys
|
||||
|
||||
# Global name
|
||||
__appname__ = 'glances'
|
||||
__version__ = '2.6'
|
||||
__version__ = '2.6.1'
|
||||
__author__ = 'Nicolas Hennion <nicolas@nicolargo.com>'
|
||||
__license__ = 'LGPL'
|
||||
|
||||
# Import psutil
|
||||
try:
|
||||
from psutil import __version__ as __psutil_version
|
||||
from psutil import __version__ as psutil_version
|
||||
except ImportError:
|
||||
print('PSutil library not found. Glances cannot start.')
|
||||
sys.exit(1)
|
||||
@ -62,8 +62,8 @@ if sys.version_info[:2] == (2, 6):
|
||||
|
||||
# Check PSutil version
|
||||
psutil_min_version = (2, 0, 0)
|
||||
psutil_version = tuple([int(num) for num in __psutil_version.split('.')])
|
||||
if psutil_version < psutil_min_version:
|
||||
psutil_version_info = tuple([int(num) for num in psutil_version.split('.')])
|
||||
if psutil_version_info < psutil_min_version:
|
||||
print('PSutil 2.0 or higher is needed. Glances cannot start.')
|
||||
sys.exit(1)
|
||||
|
||||
@ -107,7 +107,7 @@ def main():
|
||||
logger.info('{0} {1} and PSutil {2} detected'.format(
|
||||
platform.python_implementation(),
|
||||
platform.python_version(),
|
||||
__psutil_version))
|
||||
psutil_version))
|
||||
|
||||
# Share global var
|
||||
global core, standalone, client, server, webserver
|
||||
|
@ -22,7 +22,8 @@
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from glances.globals import appname, BSD
|
||||
from glances import __appname__
|
||||
from glances.globals import BSD
|
||||
from glances.logger import logger
|
||||
|
||||
try:
|
||||
@ -46,7 +47,7 @@ if zeroconf_tag:
|
||||
sys.exit(1)
|
||||
|
||||
# Global var
|
||||
zeroconf_type = "_%s._tcp." % appname
|
||||
zeroconf_type = "_%s._tcp." % __appname__
|
||||
|
||||
|
||||
class AutoDiscovered(object):
|
||||
|
@ -23,10 +23,10 @@ import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from glances import __version__
|
||||
from glances.compat import Fault, ProtocolError, ServerProxy, Transport
|
||||
from glances.globals import version
|
||||
from glances.logger import logger
|
||||
from glances.stats import GlancesStatsClient
|
||||
from glances.stats_client import GlancesStatsClient
|
||||
from glances.outputs.glances_curses import GlancesCursesClient
|
||||
|
||||
|
||||
@ -122,11 +122,11 @@ class GlancesClient(object):
|
||||
|
||||
if self.client_mode == 'glances':
|
||||
# Check that both client and server are in the same major version
|
||||
if version.split('.')[0] == client_version.split('.')[0]:
|
||||
if __version__.split('.')[0] == client_version.split('.')[0]:
|
||||
# Init stats
|
||||
self.stats = GlancesStatsClient(config=self.config, args=self.args)
|
||||
self.stats.set_plugins(json.loads(self.client.getAllPlugins()))
|
||||
logger.debug("Client version: {0} / Server version: {1}".format(version, client_version))
|
||||
logger.debug("Client version: {0} / Server version: {1}".format(__version__, client_version))
|
||||
else:
|
||||
self.log_and_exit("Client and server not compatible: \
|
||||
Client version: {0} / Server version: {1}".format(version, client_version))
|
||||
@ -139,7 +139,7 @@ class GlancesClient(object):
|
||||
if self.client_mode == 'snmp':
|
||||
logger.info("Trying to grab stats by SNMP...")
|
||||
|
||||
from glances.stats import GlancesStatsClientSNMP
|
||||
from glances.stats_client_snmp import GlancesStatsClientSNMP
|
||||
|
||||
# Init stats
|
||||
self.stats = GlancesStatsClientSNMP(config=self.config, args=self.args)
|
||||
|
@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com>
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
@ -28,7 +28,7 @@ from glances.client import GlancesClient, GlancesClientTransport
|
||||
from glances.logger import logger
|
||||
from glances.password_list import GlancesPasswordList as GlancesPassword
|
||||
from glances.static_list import GlancesStaticServer
|
||||
from glances.outputs.glances_curses import GlancesCursesBrowser
|
||||
from glances.outputs.glances_curses_browser import GlancesCursesBrowser
|
||||
|
||||
|
||||
class GlancesClientBrowser(object):
|
||||
@ -160,7 +160,7 @@ class GlancesClientBrowser(object):
|
||||
"Server list dictionnary change inside the loop (wait next update)")
|
||||
|
||||
# Update the screen (list or Glances client)
|
||||
if not self.screen.active_server:
|
||||
if self.screen.active_server is None:
|
||||
# Display the Glances browser
|
||||
self.screen.update(self.get_servers_list())
|
||||
else:
|
||||
|
@ -23,8 +23,9 @@ import os
|
||||
import sys
|
||||
from io import open
|
||||
|
||||
from glances import __appname__
|
||||
from glances.compat import ConfigParser, NoOptionError
|
||||
from glances.globals import appname, BSD, LINUX, OSX, WINDOWS, sys_prefix
|
||||
from glances.globals import BSD, LINUX, OSX, WINDOWS, sys_prefix
|
||||
from glances.logger import logger
|
||||
|
||||
|
||||
@ -69,22 +70,22 @@ class Config(object):
|
||||
paths.append(
|
||||
os.path.join(os.environ.get('XDG_CONFIG_HOME') or
|
||||
os.path.expanduser('~/.config'),
|
||||
appname, self.config_filename))
|
||||
__appname__, self.config_filename))
|
||||
if BSD:
|
||||
paths.append(
|
||||
os.path.join(sys.prefix, 'etc', appname, self.config_filename))
|
||||
os.path.join(sys.prefix, 'etc', __appname__, self.config_filename))
|
||||
else:
|
||||
paths.append(
|
||||
os.path.join('/etc', appname, self.config_filename))
|
||||
os.path.join('/etc', __appname__, self.config_filename))
|
||||
elif OSX:
|
||||
paths.append(
|
||||
os.path.join(os.path.expanduser('~/Library/Application Support/'),
|
||||
appname, self.config_filename))
|
||||
__appname__, self.config_filename))
|
||||
paths.append(
|
||||
os.path.join(sys_prefix, 'etc', appname, self.config_filename))
|
||||
os.path.join(sys_prefix, 'etc', __appname__, self.config_filename))
|
||||
elif WINDOWS:
|
||||
paths.append(
|
||||
os.path.join(os.environ.get('APPDATA'), appname, self.config_filename))
|
||||
os.path.join(os.environ.get('APPDATA'), __appname__, self.config_filename))
|
||||
|
||||
return paths
|
||||
|
||||
|
92
glances/exports/glances_riemann.py
Normal file
92
glances/exports/glances_riemann.py
Normal file
@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Glances 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Riemann interface class."""
|
||||
|
||||
import socket
|
||||
import sys
|
||||
from numbers import Number
|
||||
|
||||
from glances.compat import NoOptionError, NoSectionError, range
|
||||
from glances.logger import logger
|
||||
from glances.exports.glances_export import GlancesExport
|
||||
|
||||
# Import pika for Riemann
|
||||
import bernhard
|
||||
|
||||
|
||||
class Export(GlancesExport):
|
||||
|
||||
"""This class manages the Riemann export module."""
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
"""Init the Riemann export IF."""
|
||||
super(Export, self).__init__(config=config, args=args)
|
||||
|
||||
# Load the rabbitMQ configuration file
|
||||
self.riemann_host = None
|
||||
self.riemann_port = None
|
||||
self.hostname = socket.gethostname()
|
||||
self.export_enable = self.load_conf()
|
||||
if not self.export_enable:
|
||||
sys.exit(2)
|
||||
|
||||
# Init the rabbitmq client
|
||||
self.client = self.init()
|
||||
|
||||
def load_conf(self, section="riemann"):
|
||||
"""Load the Riemann configuration in the Glances configuration file."""
|
||||
if self.config is None:
|
||||
return False
|
||||
try:
|
||||
self.riemann_host = self.config.get_value(section, 'host')
|
||||
self.riemann_port = int(self.config.get_value(section, 'port'))
|
||||
except NoSectionError:
|
||||
logger.critical("No riemann configuration found")
|
||||
return False
|
||||
except NoOptionError as e:
|
||||
logger.critical("Error in the Riemann configuration (%s)" % e)
|
||||
return False
|
||||
else:
|
||||
logger.debug("Load Riemann from the Glances configuration file")
|
||||
return True
|
||||
|
||||
def init(self):
|
||||
"""Init the connection to the Riemann server."""
|
||||
if not self.export_enable:
|
||||
return None
|
||||
try:
|
||||
client = bernhard.Client(host=self.riemann_host, port=self.riemann_port)
|
||||
return client
|
||||
except Exception as e:
|
||||
logger.critical("Connection to Riemann failed : %s " % e)
|
||||
return None
|
||||
|
||||
def export(self, name, columns, points):
|
||||
"""Write the points in Riemann."""
|
||||
for i in range(len(columns)):
|
||||
if not isinstance(points[i], Number):
|
||||
continue
|
||||
else:
|
||||
data = {'host': self.hostname, 'service': name + " " + columns[i], 'metric': points[i]}
|
||||
logger.debug(data)
|
||||
try:
|
||||
self.client.send(data)
|
||||
except Exception as e:
|
||||
logger.error("Can not export stats to Riemann (%s)" % e)
|
@ -22,11 +22,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Global information
|
||||
appname = 'glances'
|
||||
version = __import__('glances').__version__
|
||||
psutil_version = __import__('glances').__psutil_version
|
||||
|
||||
# Operating system flag
|
||||
# Note: Somes libs depends of OS
|
||||
BSD = sys.platform.find('bsd') != -1
|
||||
|
@ -24,9 +24,10 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from glances import __appname__, __version__, psutil_version
|
||||
from glances.compat import input
|
||||
from glances.config import Config
|
||||
from glances.globals import appname, LINUX, WINDOWS, psutil_version, version
|
||||
from glances.globals import LINUX, WINDOWS
|
||||
from glances.logger import logger
|
||||
|
||||
|
||||
@ -86,14 +87,14 @@ Start the client browser (browser mode):\n\
|
||||
|
||||
def init_args(self):
|
||||
"""Init all the command line arguments."""
|
||||
_version = "Glances v" + version + " with psutil v" + psutil_version
|
||||
version = "Glances v" + __version__ + " with psutil v" + psutil_version
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=appname,
|
||||
prog=__appname__,
|
||||
conflict_handler='resolve',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=self.example_of_use)
|
||||
parser.add_argument(
|
||||
'-V', '--version', action='version', version=_version)
|
||||
'-V', '--version', action='version', version=version)
|
||||
parser.add_argument('-d', '--debug', action='store_true', default=False,
|
||||
dest='debug', help='enable debug mode')
|
||||
parser.add_argument('-C', '--config', dest='conf_file',
|
||||
@ -162,6 +163,8 @@ Start the client browser (browser mode):\n\
|
||||
dest='export_elasticsearch', help='export stats to an ElasticSearch server (elasticsearch lib needed)')
|
||||
parser.add_argument('--export-rabbitmq', action='store_true', default=False,
|
||||
dest='export_rabbitmq', help='export stats to rabbitmq broker (pika lib needed)')
|
||||
parser.add_argument('--export-riemann', action='store_true', default=False,
|
||||
dest='export_riemann', help='export stats to riemann broker (bernhard lib needed)')
|
||||
# Client/Server option
|
||||
parser.add_argument('-c', '--client', dest='client',
|
||||
help='connect to a Glances server by IPv4/IPv6 address or hostname')
|
||||
|
@ -333,6 +333,29 @@ class GlancesBottle(object):
|
||||
abort(404, "Cannot get views for plugin %s (%s)" % (plugin, str(e)))
|
||||
return ret
|
||||
|
||||
def _api_itemvalue(self, plugin, item, value=None):
|
||||
""" Father method for _api_item and _api_value"""
|
||||
response.content_type = 'application/json'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.stats.update()
|
||||
|
||||
if value is None:
|
||||
ret = self.stats.get_plugin(plugin).get_stats_item(item)
|
||||
|
||||
if ret is None:
|
||||
abort(404, "Cannot get item %s in plugin %s" % (item, plugin))
|
||||
else:
|
||||
ret = self.stats.get_plugin(plugin).get_stats_value(item, value)
|
||||
|
||||
if ret is None:
|
||||
abort(404, "Cannot get item(%s)=value(%s) in plugin %s" % (item, value, plugin))
|
||||
|
||||
return ret
|
||||
|
||||
def _api_item(self, plugin, item):
|
||||
"""Glances API RESTFul implementation.
|
||||
|
||||
@ -342,20 +365,7 @@ class GlancesBottle(object):
|
||||
HTTP/404 if others error
|
||||
|
||||
"""
|
||||
response.content_type = 'application/json'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.stats.update()
|
||||
|
||||
plist = self.stats.get_plugin(plugin).get_stats_item(item)
|
||||
|
||||
if plist is None:
|
||||
abort(404, "Cannot get item %s in plugin %s" % (item, plugin))
|
||||
else:
|
||||
return plist
|
||||
return self._api_itemvalue(plugin, item)
|
||||
|
||||
def _api_value(self, plugin, item, value):
|
||||
"""Glances API RESTFul implementation.
|
||||
@ -365,20 +375,7 @@ class GlancesBottle(object):
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.stats.update()
|
||||
|
||||
pdict = self.stats.get_plugin(plugin).get_stats_value(item, value)
|
||||
|
||||
if pdict is None:
|
||||
abort(404, "Cannot get item(%s)=value(%s) in plugin %s" % (item, value, plugin))
|
||||
else:
|
||||
return pdict
|
||||
return self._api_itemvalue(plugin, item, value)
|
||||
|
||||
def _api_args(self):
|
||||
"""Glances API RESTFul implementation.
|
||||
|
@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com>
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
@ -846,7 +846,7 @@ class _GlancesCurses(object):
|
||||
else:
|
||||
# Display the popup
|
||||
popup.refresh()
|
||||
curses.napms(duration * 1000)
|
||||
self.wait(duration * 1000)
|
||||
return True
|
||||
|
||||
def display_plugin(self, plugin_stats,
|
||||
@ -986,10 +986,14 @@ class _GlancesCurses(object):
|
||||
# Redraw display
|
||||
self.flush(stats, cs_status=cs_status)
|
||||
# Wait 100ms...
|
||||
curses.napms(100)
|
||||
self.wait()
|
||||
|
||||
return exitkey
|
||||
|
||||
def wait(self, delay=100):
|
||||
"""Wait delay in ms"""
|
||||
curses.napms(100)
|
||||
|
||||
def get_stats_display_width(self, curse_msg, without_option=False):
|
||||
"""Return the width of the formatted curses message.
|
||||
|
||||
@ -1036,248 +1040,6 @@ class GlancesCursesClient(_GlancesCurses):
|
||||
pass
|
||||
|
||||
|
||||
class GlancesCursesBrowser(_GlancesCurses):
|
||||
|
||||
"""Class for the Glances curse client browser."""
|
||||
|
||||
def __init__(self, args=None):
|
||||
# Init the father class
|
||||
super(GlancesCursesBrowser, self).__init__(args=args)
|
||||
|
||||
_colors_list = {
|
||||
'UNKNOWN': self.no_color,
|
||||
'SNMP': self.default_color2,
|
||||
'ONLINE': self.default_color2,
|
||||
'OFFLINE': self.ifCRITICAL_color2,
|
||||
'PROTECTED': self.ifWARNING_color2,
|
||||
}
|
||||
self.colors_list.update(_colors_list)
|
||||
|
||||
# First time scan tag
|
||||
# Used to display a specific message when the browser is started
|
||||
self.first_scan = True
|
||||
|
||||
# Init refresh time
|
||||
self.__refresh_time = args.time
|
||||
|
||||
# Init the cursor position for the client browser
|
||||
self.cursor_position = 0
|
||||
|
||||
# Active Glances server number
|
||||
self._active_server = None
|
||||
|
||||
@property
|
||||
def active_server(self):
|
||||
"""Return the active server or None if it's the browser list."""
|
||||
return self._active_server
|
||||
|
||||
@active_server.setter
|
||||
def active_server(self, index):
|
||||
"""Set the active server or None if no server selected."""
|
||||
self._active_server = index
|
||||
|
||||
@property
|
||||
def cursor(self):
|
||||
"""Get the cursor position."""
|
||||
return self.cursor_position
|
||||
|
||||
@cursor.setter
|
||||
def cursor(self, position):
|
||||
"""Set the cursor position."""
|
||||
self.cursor_position = position
|
||||
|
||||
def cursor_up(self, servers_list):
|
||||
"""Set the cursor to position N-1 in the list."""
|
||||
if self.cursor_position > 0:
|
||||
self.cursor_position -= 1
|
||||
else:
|
||||
self.cursor_position = len(servers_list) - 1
|
||||
|
||||
def cursor_down(self, servers_list):
|
||||
"""Set the cursor to position N-1 in the list."""
|
||||
if self.cursor_position < len(servers_list) - 1:
|
||||
self.cursor_position += 1
|
||||
else:
|
||||
self.cursor_position = 0
|
||||
|
||||
def __catch_key(self, servers_list):
|
||||
# Catch the browser pressed key
|
||||
self.pressedkey = self.get_key(self.term_window)
|
||||
|
||||
# Actions...
|
||||
if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
|
||||
# 'ESC'|'q' > Quit
|
||||
self.end()
|
||||
logger.info("Stop Glances client browser")
|
||||
sys.exit(0)
|
||||
elif self.pressedkey == 10:
|
||||
# 'ENTER' > Run Glances on the selected server
|
||||
logger.debug("Server number {0} selected".format(self.cursor + 1))
|
||||
self.active_server = self.cursor
|
||||
elif self.pressedkey == 259:
|
||||
# 'UP' > Up in the server list
|
||||
self.cursor_up(servers_list)
|
||||
elif self.pressedkey == 258:
|
||||
# 'DOWN' > Down in the server list
|
||||
self.cursor_down(servers_list)
|
||||
|
||||
# Return the key code
|
||||
return self.pressedkey
|
||||
|
||||
def update(self, servers_list):
|
||||
"""Update the servers' list screen.
|
||||
|
||||
Wait for __refresh_time sec / catch key every 100 ms.
|
||||
|
||||
servers_list: Dict of dict with servers stats
|
||||
"""
|
||||
# Flush display
|
||||
logger.debug('Servers list: {0}'.format(servers_list))
|
||||
self.flush(servers_list)
|
||||
|
||||
# Wait
|
||||
exitkey = False
|
||||
countdown = Timer(self.__refresh_time)
|
||||
while not countdown.finished() and not exitkey:
|
||||
# Getkey
|
||||
pressedkey = self.__catch_key(servers_list)
|
||||
# Is it an exit or select server key ?
|
||||
exitkey = (
|
||||
pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10)
|
||||
if not exitkey and pressedkey > -1:
|
||||
# Redraw display
|
||||
self.flush(servers_list)
|
||||
# Wait 100ms...
|
||||
curses.napms(100)
|
||||
|
||||
return self.active_server
|
||||
|
||||
def flush(self, servers_list):
|
||||
"""Update the servers' list screen.
|
||||
|
||||
servers_list: List of dict with servers stats
|
||||
"""
|
||||
self.erase()
|
||||
self.display(servers_list)
|
||||
|
||||
def display(self, servers_list):
|
||||
"""Display the servers list.
|
||||
|
||||
Return:
|
||||
True if the stats have been displayed
|
||||
False if the stats have not been displayed (no server available)
|
||||
"""
|
||||
# Init the internal line/column for Glances Curses
|
||||
self.init_line_column()
|
||||
|
||||
# Get the current screen size
|
||||
screen_x = self.screen.getmaxyx()[1]
|
||||
screen_y = self.screen.getmaxyx()[0]
|
||||
|
||||
# Init position
|
||||
x = 0
|
||||
y = 0
|
||||
|
||||
# Display top header
|
||||
if len(servers_list) == 0:
|
||||
if self.first_scan and not self.args.disable_autodiscover:
|
||||
msg = 'Glances is scanning your network. Please wait...'
|
||||
self.first_scan = False
|
||||
else:
|
||||
msg = 'No Glances server available'
|
||||
elif len(servers_list) == 1:
|
||||
msg = 'One Glances server available'
|
||||
else:
|
||||
msg = '{0} Glances servers available'.format(len(servers_list))
|
||||
if self.args.disable_autodiscover:
|
||||
msg += ' ' + '(auto discover is disabled)'
|
||||
self.term_window.addnstr(y, x,
|
||||
msg,
|
||||
screen_x - x,
|
||||
self.colors_list['TITLE'])
|
||||
|
||||
if len(servers_list) == 0:
|
||||
return False
|
||||
|
||||
# Display the Glances server list
|
||||
# ================================
|
||||
|
||||
# Table of table
|
||||
# Item description: [stats_id, column name, column size]
|
||||
column_def = [
|
||||
['name', 'Name', 16],
|
||||
['alias', None, None],
|
||||
['load_min5', 'LOAD', 6],
|
||||
['cpu_percent', 'CPU%', 5],
|
||||
['mem_percent', 'MEM%', 5],
|
||||
['status', 'STATUS', 9],
|
||||
['ip', 'IP', 15],
|
||||
# ['port', 'PORT', 5],
|
||||
['hr_name', 'OS', 16],
|
||||
]
|
||||
y = 2
|
||||
|
||||
# Display table header
|
||||
xc = x + 2
|
||||
for cpt, c in enumerate(column_def):
|
||||
if xc < screen_x and y < screen_y and c[1] is not None:
|
||||
self.term_window.addnstr(y, xc,
|
||||
c[1],
|
||||
screen_x - x,
|
||||
self.colors_list['BOLD'])
|
||||
xc += c[2] + self.space_between_column
|
||||
y += 1
|
||||
|
||||
# If a servers has been deleted from the list...
|
||||
# ... and if the cursor is in the latest position
|
||||
if self.cursor > len(servers_list) - 1:
|
||||
# Set the cursor position to the latest item
|
||||
self.cursor = len(servers_list) - 1
|
||||
|
||||
# Display table
|
||||
line = 0
|
||||
for v in servers_list:
|
||||
# Get server stats
|
||||
server_stat = {}
|
||||
for c in column_def:
|
||||
try:
|
||||
server_stat[c[0]] = v[c[0]]
|
||||
except KeyError as e:
|
||||
logger.debug(
|
||||
"Cannot grab stats {0} from server (KeyError: {1})".format(c[0], e))
|
||||
server_stat[c[0]] = '?'
|
||||
# Display alias instead of name
|
||||
try:
|
||||
if c[0] == 'alias' and v[c[0]] is not None:
|
||||
server_stat['name'] = v[c[0]]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Display line for server stats
|
||||
cpt = 0
|
||||
xc = x
|
||||
|
||||
# Is the line selected ?
|
||||
if line == self.cursor:
|
||||
# Display cursor
|
||||
self.term_window.addnstr(
|
||||
y, xc, ">", screen_x - xc, self.colors_list['BOLD'])
|
||||
|
||||
# Display the line
|
||||
xc += 2
|
||||
for c in column_def:
|
||||
if xc < screen_x and y < screen_y and c[1] is not None:
|
||||
# Display server stats
|
||||
self.term_window.addnstr(
|
||||
y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']])
|
||||
xc += c[2] + self.space_between_column
|
||||
cpt += 1
|
||||
# Next line, next server...
|
||||
y += 1
|
||||
line += 1
|
||||
|
||||
return True
|
||||
|
||||
if not WINDOWS:
|
||||
class GlancesTextbox(Textbox, object):
|
||||
|
||||
|
273
glances/outputs/glances_curses_browser.py
Normal file
273
glances/outputs/glances_curses_browser.py
Normal file
@ -0,0 +1,273 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Glances 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Curses browser interface class ."""
|
||||
|
||||
import sys
|
||||
|
||||
from glances.outputs.glances_curses import _GlancesCurses
|
||||
|
||||
from glances.logger import logger
|
||||
from glances.timer import Timer
|
||||
|
||||
|
||||
class GlancesCursesBrowser(_GlancesCurses):
|
||||
|
||||
"""Class for the Glances curse client browser."""
|
||||
|
||||
def __init__(self, args=None):
|
||||
# Init the father class
|
||||
super(GlancesCursesBrowser, self).__init__(args=args)
|
||||
|
||||
_colors_list = {
|
||||
'UNKNOWN': self.no_color,
|
||||
'SNMP': self.default_color2,
|
||||
'ONLINE': self.default_color2,
|
||||
'OFFLINE': self.ifCRITICAL_color2,
|
||||
'PROTECTED': self.ifWARNING_color2,
|
||||
}
|
||||
self.colors_list.update(_colors_list)
|
||||
|
||||
# First time scan tag
|
||||
# Used to display a specific message when the browser is started
|
||||
self.first_scan = True
|
||||
|
||||
# Init refresh time
|
||||
self.__refresh_time = args.time
|
||||
|
||||
# Init the cursor position for the client browser
|
||||
self.cursor_position = 0
|
||||
|
||||
# Active Glances server number
|
||||
self._active_server = None
|
||||
|
||||
@property
|
||||
def active_server(self):
|
||||
"""Return the active server or None if it's the browser list."""
|
||||
return self._active_server
|
||||
|
||||
@active_server.setter
|
||||
def active_server(self, index):
|
||||
"""Set the active server or None if no server selected."""
|
||||
self._active_server = index
|
||||
|
||||
@property
|
||||
def cursor(self):
|
||||
"""Get the cursor position."""
|
||||
return self.cursor_position
|
||||
|
||||
@cursor.setter
|
||||
def cursor(self, position):
|
||||
"""Set the cursor position."""
|
||||
self.cursor_position = position
|
||||
|
||||
def cursor_up(self, servers_list):
|
||||
"""Set the cursor to position N-1 in the list."""
|
||||
if self.cursor_position > 0:
|
||||
self.cursor_position -= 1
|
||||
else:
|
||||
self.cursor_position = len(servers_list) - 1
|
||||
|
||||
def cursor_down(self, servers_list):
|
||||
"""Set the cursor to position N-1 in the list."""
|
||||
if self.cursor_position < len(servers_list) - 1:
|
||||
self.cursor_position += 1
|
||||
else:
|
||||
self.cursor_position = 0
|
||||
|
||||
def __catch_key(self, servers_list):
|
||||
# Catch the browser pressed key
|
||||
self.pressedkey = self.get_key(self.term_window)
|
||||
|
||||
if self.pressedkey != -1:
|
||||
logger.debug("Key pressed. Code=%s" % self.pressedkey)
|
||||
|
||||
# Actions...
|
||||
if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
|
||||
# 'ESC'|'q' > Quit
|
||||
self.end()
|
||||
logger.info("Stop Glances client browser")
|
||||
sys.exit(0)
|
||||
elif self.pressedkey == 10:
|
||||
# 'ENTER' > Run Glances on the selected server
|
||||
logger.debug("Server number {0} selected".format(self.cursor + 1))
|
||||
self.active_server = self.cursor
|
||||
elif self.pressedkey == 65:
|
||||
# 'UP' > Up in the server list
|
||||
self.cursor_up(servers_list)
|
||||
elif self.pressedkey == 66:
|
||||
# 'DOWN' > Down in the server list
|
||||
self.cursor_down(servers_list)
|
||||
|
||||
# Return the key code
|
||||
return self.pressedkey
|
||||
|
||||
def update(self, servers_list):
|
||||
"""Update the servers' list screen.
|
||||
|
||||
Wait for __refresh_time sec / catch key every 100 ms.
|
||||
|
||||
servers_list: Dict of dict with servers stats
|
||||
"""
|
||||
# Flush display
|
||||
logger.debug('Servers list: {0}'.format(servers_list))
|
||||
self.flush(servers_list)
|
||||
|
||||
# Wait
|
||||
exitkey = False
|
||||
countdown = Timer(self.__refresh_time)
|
||||
while not countdown.finished() and not exitkey:
|
||||
# Getkey
|
||||
pressedkey = self.__catch_key(servers_list)
|
||||
# Is it an exit or select server key ?
|
||||
exitkey = (
|
||||
pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10)
|
||||
if not exitkey and pressedkey > -1:
|
||||
# Redraw display
|
||||
self.flush(servers_list)
|
||||
# Wait 100ms...
|
||||
self.wait()
|
||||
|
||||
return self.active_server
|
||||
|
||||
def flush(self, servers_list):
|
||||
"""Update the servers' list screen.
|
||||
|
||||
servers_list: List of dict with servers stats
|
||||
"""
|
||||
self.erase()
|
||||
self.display(servers_list)
|
||||
|
||||
def display(self, servers_list):
|
||||
"""Display the servers list.
|
||||
|
||||
Return:
|
||||
True if the stats have been displayed
|
||||
False if the stats have not been displayed (no server available)
|
||||
"""
|
||||
# Init the internal line/column for Glances Curses
|
||||
self.init_line_column()
|
||||
|
||||
# Get the current screen size
|
||||
screen_x = self.screen.getmaxyx()[1]
|
||||
screen_y = self.screen.getmaxyx()[0]
|
||||
|
||||
# Init position
|
||||
x = 0
|
||||
y = 0
|
||||
|
||||
# Display top header
|
||||
if len(servers_list) == 0:
|
||||
if self.first_scan and not self.args.disable_autodiscover:
|
||||
msg = 'Glances is scanning your network. Please wait...'
|
||||
self.first_scan = False
|
||||
else:
|
||||
msg = 'No Glances server available'
|
||||
elif len(servers_list) == 1:
|
||||
msg = 'One Glances server available'
|
||||
else:
|
||||
msg = '{0} Glances servers available'.format(len(servers_list))
|
||||
if self.args.disable_autodiscover:
|
||||
msg += ' ' + '(auto discover is disabled)'
|
||||
self.term_window.addnstr(y, x,
|
||||
msg,
|
||||
screen_x - x,
|
||||
self.colors_list['TITLE'])
|
||||
|
||||
if len(servers_list) == 0:
|
||||
return False
|
||||
|
||||
# Display the Glances server list
|
||||
# ================================
|
||||
|
||||
# Table of table
|
||||
# Item description: [stats_id, column name, column size]
|
||||
column_def = [
|
||||
['name', 'Name', 16],
|
||||
['alias', None, None],
|
||||
['load_min5', 'LOAD', 6],
|
||||
['cpu_percent', 'CPU%', 5],
|
||||
['mem_percent', 'MEM%', 5],
|
||||
['status', 'STATUS', 9],
|
||||
['ip', 'IP', 15],
|
||||
# ['port', 'PORT', 5],
|
||||
['hr_name', 'OS', 16],
|
||||
]
|
||||
y = 2
|
||||
|
||||
# Display table header
|
||||
xc = x + 2
|
||||
for cpt, c in enumerate(column_def):
|
||||
if xc < screen_x and y < screen_y and c[1] is not None:
|
||||
self.term_window.addnstr(y, xc,
|
||||
c[1],
|
||||
screen_x - x,
|
||||
self.colors_list['BOLD'])
|
||||
xc += c[2] + self.space_between_column
|
||||
y += 1
|
||||
|
||||
# If a servers has been deleted from the list...
|
||||
# ... and if the cursor is in the latest position
|
||||
if self.cursor > len(servers_list) - 1:
|
||||
# Set the cursor position to the latest item
|
||||
self.cursor = len(servers_list) - 1
|
||||
|
||||
# Display table
|
||||
line = 0
|
||||
for v in servers_list:
|
||||
# Get server stats
|
||||
server_stat = {}
|
||||
for c in column_def:
|
||||
try:
|
||||
server_stat[c[0]] = v[c[0]]
|
||||
except KeyError as e:
|
||||
logger.debug(
|
||||
"Cannot grab stats {0} from server (KeyError: {1})".format(c[0], e))
|
||||
server_stat[c[0]] = '?'
|
||||
# Display alias instead of name
|
||||
try:
|
||||
if c[0] == 'alias' and v[c[0]] is not None:
|
||||
server_stat['name'] = v[c[0]]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Display line for server stats
|
||||
cpt = 0
|
||||
xc = x
|
||||
|
||||
# Is the line selected ?
|
||||
if line == self.cursor:
|
||||
# Display cursor
|
||||
self.term_window.addnstr(
|
||||
y, xc, ">", screen_x - xc, self.colors_list['BOLD'])
|
||||
|
||||
# Display the line
|
||||
xc += 2
|
||||
for c in column_def:
|
||||
if xc < screen_x and y < screen_y and c[1] is not None:
|
||||
# Display server stats
|
||||
self.term_window.addnstr(
|
||||
y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']])
|
||||
xc += c[2] + self.space_between_column
|
||||
cpt += 1
|
||||
# Next line, next server...
|
||||
y += 1
|
||||
line += 1
|
||||
|
||||
return True
|
@ -26,8 +26,9 @@ import sys
|
||||
import uuid
|
||||
from io import open
|
||||
|
||||
from glances import __appname__
|
||||
from glances.compat import b, input
|
||||
from glances.globals import appname, BSD, LINUX, OSX, WINDOWS
|
||||
from glances.globals import BSD, LINUX, OSX, WINDOWS
|
||||
from glances.logger import logger
|
||||
|
||||
|
||||
@ -58,7 +59,7 @@ class GlancesPassword(object):
|
||||
app_path = '.'
|
||||
|
||||
# Append the Glances folder
|
||||
app_path = os.path.join(app_path, appname)
|
||||
app_path = os.path.join(app_path, __appname__)
|
||||
|
||||
return app_path
|
||||
|
||||
|
@ -23,7 +23,7 @@ Help plugin.
|
||||
Just a stupid plugin to display the help screen.
|
||||
"""
|
||||
|
||||
from glances.globals import appname, psutil_version, version
|
||||
from glances import __appname__, __version__, psutil_version
|
||||
from glances.plugins.glances_plugin import GlancesPlugin
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ class Plugin(GlancesPlugin):
|
||||
pass
|
||||
|
||||
def generate_view_data(self):
|
||||
self.view_data['version'] = '{0} {1}'.format(appname.title(), version)
|
||||
self.view_data['version'] = '{0} {1}'.format(__appname__.title(), __version__)
|
||||
self.view_data['psutil_version'] = ' with PSutil {0}'.format(psutil_version)
|
||||
|
||||
try:
|
||||
|
@ -17,13 +17,11 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from glances import psutil_version_info
|
||||
from glances.plugins.glances_plugin import GlancesPlugin
|
||||
|
||||
from psutil import __version__ as __psutil_version
|
||||
|
||||
|
||||
class Plugin(GlancesPlugin):
|
||||
|
||||
"""Get the psutil version for client/server purposes.
|
||||
|
||||
stats is a tuple
|
||||
@ -48,7 +46,7 @@ class Plugin(GlancesPlugin):
|
||||
if self.input_method == 'local':
|
||||
# PsUtil version only available in local
|
||||
try:
|
||||
self.stats = tuple([int(num) for num in __psutil_version.split('.')])
|
||||
self.stats = psutil_version_info
|
||||
except NameError:
|
||||
pass
|
||||
else:
|
||||
|
@ -208,7 +208,7 @@ class Plugin(GlancesPlugin):
|
||||
msg, self.get_views(item=i[self.get_key()],
|
||||
key='value',
|
||||
option='decoration')))
|
||||
except ValueError:
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return ret
|
||||
|
@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com>
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
@ -17,7 +17,6 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
|
||||
@ -25,6 +24,7 @@ from glances.compat import iteritems, itervalues, listitems
|
||||
from glances.globals import BSD, LINUX, OSX, WINDOWS
|
||||
from glances.logger import logger
|
||||
from glances.timer import Timer, getTimeSinceLastUpdate
|
||||
from glances.processes_tree import ProcessTreeNode
|
||||
|
||||
import psutil
|
||||
|
||||
@ -39,199 +39,6 @@ def is_kernel_thread(proc):
|
||||
return False
|
||||
|
||||
|
||||
class ProcessTreeNode(object):
|
||||
|
||||
"""Represent a process tree.
|
||||
|
||||
We avoid recursive algorithm to manipulate the tree because function
|
||||
calls are expensive with CPython.
|
||||
"""
|
||||
|
||||
def __init__(self, process=None, stats=None, sort_key=None, sort_reverse=True, root=False):
|
||||
self.process = process
|
||||
self.stats = stats
|
||||
self.children = []
|
||||
self.children_sorted = False
|
||||
self.sort_key = sort_key
|
||||
self.sort_reverse = sort_reverse
|
||||
self.is_root = root
|
||||
|
||||
def __str__(self):
|
||||
"""Return the tree as a string for debugging."""
|
||||
lines = []
|
||||
nodes_to_print = collections.deque([collections.deque([("#", self)])])
|
||||
while nodes_to_print:
|
||||
indent_str, current_node = nodes_to_print[-1].pop()
|
||||
if not nodes_to_print[-1]:
|
||||
nodes_to_print.pop()
|
||||
if current_node.is_root:
|
||||
lines.append(indent_str)
|
||||
else:
|
||||
lines.append("%s[%s]" %
|
||||
(indent_str, current_node.process.name()))
|
||||
indent_str = " " * (len(lines[-1]) - 1)
|
||||
children_nodes_to_print = collections.deque()
|
||||
for child in current_node.children:
|
||||
if child is current_node.children[-1]:
|
||||
tree_char = "└─"
|
||||
else:
|
||||
tree_char = "├─"
|
||||
children_nodes_to_print.appendleft(
|
||||
(indent_str + tree_char, child))
|
||||
if children_nodes_to_print:
|
||||
nodes_to_print.append(children_nodes_to_print)
|
||||
return "\n".join(lines)
|
||||
|
||||
def set_sorting(self, key, reverse):
|
||||
"""Set sorting key or func for use with __iter__.
|
||||
|
||||
This affects the whole tree from this node.
|
||||
"""
|
||||
if self.sort_key != key or self.sort_reverse != reverse:
|
||||
nodes_to_flag_unsorted = collections.deque([self])
|
||||
while nodes_to_flag_unsorted:
|
||||
current_node = nodes_to_flag_unsorted.pop()
|
||||
current_node.children_sorted = False
|
||||
current_node.sort_key = key
|
||||
current_node.reverse_sorting = reverse
|
||||
nodes_to_flag_unsorted.extend(current_node.children)
|
||||
|
||||
def get_weight(self):
|
||||
"""Return 'weight' of a process and all its children for sorting."""
|
||||
if self.sort_key == 'name' or self.sort_key == 'username':
|
||||
return self.stats[self.sort_key]
|
||||
|
||||
# sum ressource usage for self and children
|
||||
total = 0
|
||||
nodes_to_sum = collections.deque([self])
|
||||
while nodes_to_sum:
|
||||
current_node = nodes_to_sum.pop()
|
||||
if isinstance(self.sort_key, collections.Callable):
|
||||
total += self.sort_key(current_node.stats)
|
||||
elif self.sort_key == "io_counters":
|
||||
stats = current_node.stats[self.sort_key]
|
||||
total += stats[0] - stats[2] + stats[1] - stats[3]
|
||||
elif self.sort_key == "cpu_times":
|
||||
total += sum(current_node.stats[self.sort_key])
|
||||
else:
|
||||
total += current_node.stats[self.sort_key]
|
||||
nodes_to_sum.extend(current_node.children)
|
||||
|
||||
return total
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of nodes in the tree."""
|
||||
total = 0
|
||||
nodes_to_sum = collections.deque([self])
|
||||
while nodes_to_sum:
|
||||
current_node = nodes_to_sum.pop()
|
||||
if not current_node.is_root:
|
||||
total += 1
|
||||
nodes_to_sum.extend(current_node.children)
|
||||
return total
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterator returning ProcessTreeNode in sorted order, recursively."""
|
||||
if not self.is_root:
|
||||
yield self
|
||||
if not self.children_sorted:
|
||||
# optimization to avoid sorting twice (once when limiting the maximum processes to grab stats for,
|
||||
# and once before displaying)
|
||||
self.children.sort(
|
||||
key=self.__class__.get_weight, reverse=self.sort_reverse)
|
||||
self.children_sorted = True
|
||||
for child in self.children:
|
||||
for n in iter(child):
|
||||
yield n
|
||||
|
||||
def iter_children(self, exclude_incomplete_stats=True):
|
||||
"""Iterator returning ProcessTreeNode in sorted order.
|
||||
|
||||
Return only children of this node, non recursive.
|
||||
|
||||
If exclude_incomplete_stats is True, exclude processes not
|
||||
having full statistics. It can happen after a resort (change of
|
||||
sort key) because process stats are not grabbed immediately, but
|
||||
only at next full update.
|
||||
"""
|
||||
if not self.children_sorted:
|
||||
# optimization to avoid sorting twice (once when limiting the maximum processes to grab stats for,
|
||||
# and once before displaying)
|
||||
self.children.sort(
|
||||
key=self.__class__.get_weight, reverse=self.sort_reverse)
|
||||
self.children_sorted = True
|
||||
for child in self.children:
|
||||
if not exclude_incomplete_stats or "time_since_update" in child.stats:
|
||||
yield child
|
||||
|
||||
def find_process(self, process):
|
||||
"""Search in tree for the ProcessTreeNode owning process.
|
||||
|
||||
Return it or None if not found.
|
||||
"""
|
||||
nodes_to_search = collections.deque([self])
|
||||
while nodes_to_search:
|
||||
current_node = nodes_to_search.pop()
|
||||
if not current_node.is_root and current_node.process.pid == process.pid:
|
||||
return current_node
|
||||
nodes_to_search.extend(current_node.children)
|
||||
|
||||
@staticmethod
|
||||
def build_tree(process_dict, sort_key, sort_reverse, hide_kernel_threads, excluded_processes):
|
||||
"""Build a process tree using using parent/child relationships.
|
||||
|
||||
Return the tree root node.
|
||||
"""
|
||||
tree_root = ProcessTreeNode(root=True)
|
||||
nodes_to_add_last = collections.deque()
|
||||
|
||||
# first pass: add nodes whose parent are in the tree
|
||||
for process, stats in iteritems(process_dict):
|
||||
new_node = ProcessTreeNode(process, stats, sort_key, sort_reverse)
|
||||
try:
|
||||
parent_process = process.parent()
|
||||
except psutil.NoSuchProcess:
|
||||
# parent is dead, consider no parent
|
||||
parent_process = None
|
||||
if (parent_process is None) or (parent_process in excluded_processes):
|
||||
# no parent, or excluded parent, add this node at the top level
|
||||
tree_root.children.append(new_node)
|
||||
else:
|
||||
parent_node = tree_root.find_process(parent_process)
|
||||
if parent_node is not None:
|
||||
# parent is already in the tree, add a new child
|
||||
parent_node.children.append(new_node)
|
||||
else:
|
||||
# parent is not in tree, add this node later
|
||||
nodes_to_add_last.append(new_node)
|
||||
|
||||
# next pass(es): add nodes to their parents if it could not be done in
|
||||
# previous pass
|
||||
while nodes_to_add_last:
|
||||
# pop from left and append to right to avoid infinite loop
|
||||
node_to_add = nodes_to_add_last.popleft()
|
||||
try:
|
||||
parent_process = node_to_add.process.parent()
|
||||
except psutil.NoSuchProcess:
|
||||
# parent is dead, consider no parent, add this node at the top
|
||||
# level
|
||||
tree_root.children.append(node_to_add)
|
||||
else:
|
||||
if (parent_process is None) or (parent_process in excluded_processes):
|
||||
# no parent, or excluded parent, add this node at the top level
|
||||
tree_root.children.append(node_to_add)
|
||||
else:
|
||||
parent_node = tree_root.find_process(parent_process)
|
||||
if parent_node is not None:
|
||||
# parent is already in the tree, add a new child
|
||||
parent_node.children.append(node_to_add)
|
||||
else:
|
||||
# parent is not in tree, add this node later
|
||||
nodes_to_add_last.append(node_to_add)
|
||||
|
||||
return tree_root
|
||||
|
||||
|
||||
class GlancesProcesses(object):
|
||||
|
||||
"""Get processed stats using the psutil library."""
|
||||
|
217
glances/processes_tree.py
Normal file
217
glances/processes_tree.py
Normal file
@ -0,0 +1,217 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Glances 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import collections
|
||||
|
||||
from glances.compat import iteritems
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
class ProcessTreeNode(object):
|
||||
|
||||
"""Represent a process tree.
|
||||
|
||||
We avoid recursive algorithm to manipulate the tree because function
|
||||
calls are expensive with CPython.
|
||||
"""
|
||||
|
||||
def __init__(self, process=None, stats=None, sort_key=None, sort_reverse=True, root=False):
|
||||
self.process = process
|
||||
self.stats = stats
|
||||
self.children = []
|
||||
self.children_sorted = False
|
||||
self.sort_key = sort_key
|
||||
self.sort_reverse = sort_reverse
|
||||
self.is_root = root
|
||||
|
||||
def __str__(self):
|
||||
"""Return the tree as a string for debugging."""
|
||||
lines = []
|
||||
nodes_to_print = collections.deque([collections.deque([("#", self)])])
|
||||
while nodes_to_print:
|
||||
indent_str, current_node = nodes_to_print[-1].pop()
|
||||
if not nodes_to_print[-1]:
|
||||
nodes_to_print.pop()
|
||||
if current_node.is_root:
|
||||
lines.append(indent_str)
|
||||
else:
|
||||
lines.append("%s[%s]" %
|
||||
(indent_str, current_node.process.name()))
|
||||
indent_str = " " * (len(lines[-1]) - 1)
|
||||
children_nodes_to_print = collections.deque()
|
||||
for child in current_node.children:
|
||||
if child is current_node.children[-1]:
|
||||
tree_char = "└─"
|
||||
else:
|
||||
tree_char = "├─"
|
||||
children_nodes_to_print.appendleft(
|
||||
(indent_str + tree_char, child))
|
||||
if children_nodes_to_print:
|
||||
nodes_to_print.append(children_nodes_to_print)
|
||||
return "\n".join(lines)
|
||||
|
||||
def set_sorting(self, key, reverse):
|
||||
"""Set sorting key or func for use with __iter__.
|
||||
|
||||
This affects the whole tree from this node.
|
||||
"""
|
||||
if self.sort_key != key or self.sort_reverse != reverse:
|
||||
nodes_to_flag_unsorted = collections.deque([self])
|
||||
while nodes_to_flag_unsorted:
|
||||
current_node = nodes_to_flag_unsorted.pop()
|
||||
current_node.children_sorted = False
|
||||
current_node.sort_key = key
|
||||
current_node.reverse_sorting = reverse
|
||||
nodes_to_flag_unsorted.extend(current_node.children)
|
||||
|
||||
def get_weight(self):
|
||||
"""Return 'weight' of a process and all its children for sorting."""
|
||||
if self.sort_key == 'name' or self.sort_key == 'username':
|
||||
return self.stats[self.sort_key]
|
||||
|
||||
# sum ressource usage for self and children
|
||||
total = 0
|
||||
nodes_to_sum = collections.deque([self])
|
||||
while nodes_to_sum:
|
||||
current_node = nodes_to_sum.pop()
|
||||
if isinstance(self.sort_key, collections.Callable):
|
||||
total += self.sort_key(current_node.stats)
|
||||
elif self.sort_key == "io_counters":
|
||||
stats = current_node.stats[self.sort_key]
|
||||
total += stats[0] - stats[2] + stats[1] - stats[3]
|
||||
elif self.sort_key == "cpu_times":
|
||||
total += sum(current_node.stats[self.sort_key])
|
||||
else:
|
||||
total += current_node.stats[self.sort_key]
|
||||
nodes_to_sum.extend(current_node.children)
|
||||
|
||||
return total
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of nodes in the tree."""
|
||||
total = 0
|
||||
nodes_to_sum = collections.deque([self])
|
||||
while nodes_to_sum:
|
||||
current_node = nodes_to_sum.pop()
|
||||
if not current_node.is_root:
|
||||
total += 1
|
||||
nodes_to_sum.extend(current_node.children)
|
||||
return total
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterator returning ProcessTreeNode in sorted order, recursively."""
|
||||
if not self.is_root:
|
||||
yield self
|
||||
if not self.children_sorted:
|
||||
# optimization to avoid sorting twice (once when limiting the maximum processes to grab stats for,
|
||||
# and once before displaying)
|
||||
self.children.sort(
|
||||
key=self.__class__.get_weight, reverse=self.sort_reverse)
|
||||
self.children_sorted = True
|
||||
for child in self.children:
|
||||
for n in iter(child):
|
||||
yield n
|
||||
|
||||
def iter_children(self, exclude_incomplete_stats=True):
|
||||
"""Iterator returning ProcessTreeNode in sorted order.
|
||||
|
||||
Return only children of this node, non recursive.
|
||||
|
||||
If exclude_incomplete_stats is True, exclude processes not
|
||||
having full statistics. It can happen after a resort (change of
|
||||
sort key) because process stats are not grabbed immediately, but
|
||||
only at next full update.
|
||||
"""
|
||||
if not self.children_sorted:
|
||||
# optimization to avoid sorting twice (once when limiting the maximum processes to grab stats for,
|
||||
# and once before displaying)
|
||||
self.children.sort(
|
||||
key=self.__class__.get_weight, reverse=self.sort_reverse)
|
||||
self.children_sorted = True
|
||||
for child in self.children:
|
||||
if not exclude_incomplete_stats or "time_since_update" in child.stats:
|
||||
yield child
|
||||
|
||||
def find_process(self, process):
|
||||
"""Search in tree for the ProcessTreeNode owning process.
|
||||
|
||||
Return it or None if not found.
|
||||
"""
|
||||
nodes_to_search = collections.deque([self])
|
||||
while nodes_to_search:
|
||||
current_node = nodes_to_search.pop()
|
||||
if not current_node.is_root and current_node.process.pid == process.pid:
|
||||
return current_node
|
||||
nodes_to_search.extend(current_node.children)
|
||||
|
||||
@staticmethod
|
||||
def build_tree(process_dict, sort_key, sort_reverse, hide_kernel_threads, excluded_processes):
|
||||
"""Build a process tree using using parent/child relationships.
|
||||
|
||||
Return the tree root node.
|
||||
"""
|
||||
tree_root = ProcessTreeNode(root=True)
|
||||
nodes_to_add_last = collections.deque()
|
||||
|
||||
# first pass: add nodes whose parent are in the tree
|
||||
for process, stats in iteritems(process_dict):
|
||||
new_node = ProcessTreeNode(process, stats, sort_key, sort_reverse)
|
||||
try:
|
||||
parent_process = process.parent()
|
||||
except psutil.NoSuchProcess:
|
||||
# parent is dead, consider no parent
|
||||
parent_process = None
|
||||
if (parent_process is None) or (parent_process in excluded_processes):
|
||||
# no parent, or excluded parent, add this node at the top level
|
||||
tree_root.children.append(new_node)
|
||||
else:
|
||||
parent_node = tree_root.find_process(parent_process)
|
||||
if parent_node is not None:
|
||||
# parent is already in the tree, add a new child
|
||||
parent_node.children.append(new_node)
|
||||
else:
|
||||
# parent is not in tree, add this node later
|
||||
nodes_to_add_last.append(new_node)
|
||||
|
||||
# next pass(es): add nodes to their parents if it could not be done in
|
||||
# previous pass
|
||||
while nodes_to_add_last:
|
||||
# pop from left and append to right to avoid infinite loop
|
||||
node_to_add = nodes_to_add_last.popleft()
|
||||
try:
|
||||
parent_process = node_to_add.process.parent()
|
||||
except psutil.NoSuchProcess:
|
||||
# parent is dead, consider no parent, add this node at the top
|
||||
# level
|
||||
tree_root.children.append(node_to_add)
|
||||
else:
|
||||
if (parent_process is None) or (parent_process in excluded_processes):
|
||||
# no parent, or excluded parent, add this node at the top level
|
||||
tree_root.children.append(node_to_add)
|
||||
else:
|
||||
parent_node = tree_root.find_process(parent_process)
|
||||
if parent_node is not None:
|
||||
# parent is already in the tree, add a new child
|
||||
parent_node.children.append(node_to_add)
|
||||
else:
|
||||
# parent is not in tree, add this node later
|
||||
nodes_to_add_last.append(node_to_add)
|
||||
|
||||
return tree_root
|
@ -24,11 +24,11 @@ import socket
|
||||
import sys
|
||||
from base64 import b64decode
|
||||
|
||||
from glances import __version__
|
||||
from glances.compat import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
|
||||
from glances.autodiscover import GlancesAutoDiscoverClient
|
||||
from glances.globals import version
|
||||
from glances.logger import logger
|
||||
from glances.stats import GlancesStatsServer
|
||||
from glances.stats_server import GlancesStatsServer
|
||||
from glances.timer import Timer
|
||||
|
||||
|
||||
@ -137,7 +137,7 @@ class GlancesInstance(object):
|
||||
|
||||
def init(self):
|
||||
# Return the Glances version
|
||||
return version
|
||||
return __version__
|
||||
|
||||
def getAll(self):
|
||||
# Update and return all the stats
|
||||
|
@ -56,7 +56,7 @@ class GlancesStaticServer(object):
|
||||
if new_server['name'] is not None:
|
||||
# Manage optionnal information
|
||||
if new_server['port'] is None:
|
||||
new_server['port'] = 61209
|
||||
new_server['port'] = '61209'
|
||||
new_server['username'] = 'glances'
|
||||
# By default, try empty (aka no) password
|
||||
new_server['password'] = ''
|
||||
|
173
glances/stats.py
173
glances/stats.py
@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com>
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
@ -21,23 +21,12 @@
|
||||
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from glances.compat import iteritems
|
||||
from glances.globals import exports_path, plugins_path, sys_path
|
||||
from glances.logger import logger
|
||||
|
||||
# SNMP OID regexp pattern to short system name dict
|
||||
oid_to_short_system_name = {'.*Linux.*': 'linux',
|
||||
'.*Darwin.*': 'mac',
|
||||
'.*BSD.*': 'bsd',
|
||||
'.*Windows.*': 'windows',
|
||||
'.*Cisco.*': 'cisco',
|
||||
'.*VMware ESXi.*': 'esxi',
|
||||
'.*NetApp.*': 'netapp'}
|
||||
|
||||
|
||||
class GlancesStats(object):
|
||||
|
||||
@ -240,163 +229,3 @@ class GlancesStats(object):
|
||||
# Close plugins
|
||||
for p in self._plugins:
|
||||
self._plugins[p].exit()
|
||||
|
||||
|
||||
class GlancesStatsServer(GlancesStats):
|
||||
|
||||
"""This class stores, updates and gives stats for the server."""
|
||||
|
||||
def __init__(self, config=None):
|
||||
# Init the stats
|
||||
super(GlancesStatsServer, self).__init__(config)
|
||||
|
||||
# Init the all_stats dict used by the server
|
||||
# all_stats is a dict of dicts filled by the server
|
||||
self.all_stats = collections.defaultdict(dict)
|
||||
|
||||
def update(self, input_stats=None):
|
||||
"""Update the stats."""
|
||||
input_stats = input_stats or {}
|
||||
|
||||
# Force update of all the stats
|
||||
super(GlancesStatsServer, self).update()
|
||||
|
||||
# Build all_stats variable (concatenation of all the stats)
|
||||
self.all_stats = self._set_stats(input_stats)
|
||||
|
||||
def _set_stats(self, input_stats):
|
||||
"""Set the stats to the input_stats one."""
|
||||
# Build the all_stats with the get_raw() method of the plugins
|
||||
ret = collections.defaultdict(dict)
|
||||
for p in self._plugins:
|
||||
ret[p] = self._plugins[p].get_raw()
|
||||
return ret
|
||||
|
||||
def getAll(self):
|
||||
"""Return the stats as a list."""
|
||||
return self.all_stats
|
||||
|
||||
def getAllAsDict(self):
|
||||
"""Return the stats as a dict."""
|
||||
# Python > 2.6
|
||||
# return {p: self.all_stats[p] for p in self._plugins}
|
||||
ret = {}
|
||||
for p in self._plugins:
|
||||
ret[p] = self.all_stats[p]
|
||||
return ret
|
||||
|
||||
|
||||
class GlancesStatsClient(GlancesStats):
|
||||
|
||||
"""This class stores, updates and gives stats for the client."""
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
"""Init the GlancesStatsClient class."""
|
||||
super(GlancesStatsClient, self).__init__()
|
||||
|
||||
# Init the configuration
|
||||
self.config = config
|
||||
|
||||
# Init the arguments
|
||||
self.args = args
|
||||
|
||||
# Load plugins and exports
|
||||
self.load_plugins_and_exports(self.args)
|
||||
|
||||
def set_plugins(self, input_plugins):
|
||||
"""Set the plugin list according to the Glances server."""
|
||||
header = "glances_"
|
||||
for item in input_plugins:
|
||||
# Import the plugin
|
||||
plugin = __import__(header + item)
|
||||
# Add the plugin to the dictionary
|
||||
# The key is the plugin name
|
||||
# for example, the file glances_xxx.py
|
||||
# generate self._plugins_list["xxx"] = ...
|
||||
logger.debug("Server uses {0} plugin".format(item))
|
||||
self._plugins[item] = plugin.Plugin()
|
||||
# Restoring system path
|
||||
sys.path = sys_path
|
||||
|
||||
def update(self, input_stats):
|
||||
"""Update all the stats."""
|
||||
# For Glances client mode
|
||||
for p in input_stats:
|
||||
# Update plugin stats with items sent by the server
|
||||
self._plugins[p].set_stats(input_stats[p])
|
||||
# Update the views for the updated stats
|
||||
self._plugins[p].update_views()
|
||||
|
||||
|
||||
class GlancesStatsClientSNMP(GlancesStats):
|
||||
|
||||
"""This class stores, updates and gives stats for the SNMP client."""
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
super(GlancesStatsClientSNMP, self).__init__()
|
||||
|
||||
# Init the configuration
|
||||
self.config = config
|
||||
|
||||
# Init the arguments
|
||||
self.args = args
|
||||
|
||||
# OS name is used because OID is differents between system
|
||||
self.os_name = None
|
||||
|
||||
# Load plugins and export modules
|
||||
self.load_plugins_and_exports(self.args)
|
||||
|
||||
def check_snmp(self):
|
||||
"""Chek if SNMP is available on the server."""
|
||||
# Import the SNMP client class
|
||||
from glances.snmp import GlancesSNMPClient
|
||||
|
||||
# Create an instance of the SNMP client
|
||||
clientsnmp = GlancesSNMPClient(host=self.args.client,
|
||||
port=self.args.snmp_port,
|
||||
version=self.args.snmp_version,
|
||||
community=self.args.snmp_community,
|
||||
user=self.args.snmp_user,
|
||||
auth=self.args.snmp_auth)
|
||||
|
||||
# If we cannot grab the hostname, then exit...
|
||||
ret = clientsnmp.get_by_oid("1.3.6.1.2.1.1.5.0") != {}
|
||||
if ret:
|
||||
# Get the OS name (need to grab the good OID...)
|
||||
oid_os_name = clientsnmp.get_by_oid("1.3.6.1.2.1.1.1.0")
|
||||
try:
|
||||
self.system_name = self.get_system_name(oid_os_name['1.3.6.1.2.1.1.1.0'])
|
||||
logger.info("SNMP system name detected: {0}".format(self.system_name))
|
||||
except KeyError:
|
||||
self.system_name = None
|
||||
logger.warning("Cannot detect SNMP system name")
|
||||
|
||||
return ret
|
||||
|
||||
def get_system_name(self, oid_system_name):
|
||||
"""Get the short os name from the OS name OID string."""
|
||||
short_system_name = None
|
||||
|
||||
if oid_system_name == '':
|
||||
return short_system_name
|
||||
|
||||
# Find the short name in the oid_to_short_os_name dict
|
||||
for r, v in iteritems(oid_to_short_system_name):
|
||||
if re.search(r, oid_system_name):
|
||||
short_system_name = v
|
||||
break
|
||||
|
||||
return short_system_name
|
||||
|
||||
def update(self):
|
||||
"""Update the stats using SNMP."""
|
||||
# For each plugins, call the update method
|
||||
for p in self._plugins:
|
||||
# Set the input method to SNMP
|
||||
self._plugins[p].input_method = 'snmp'
|
||||
self._plugins[p].short_system_name = self.system_name
|
||||
try:
|
||||
self._plugins[p].update()
|
||||
except Exception as e:
|
||||
logger.error("Update {0} failed: {1}".format(p, e))
|
||||
|
68
glances/stats_client.py
Normal file
68
glances/stats_client.py
Normal file
@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Glances 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""The stats server manager."""
|
||||
|
||||
import sys
|
||||
|
||||
from glances.stats import GlancesStats
|
||||
from glances.globals import sys_path
|
||||
from glances.logger import logger
|
||||
|
||||
|
||||
class GlancesStatsClient(GlancesStats):
|
||||
|
||||
"""This class stores, updates and gives stats for the client."""
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
"""Init the GlancesStatsClient class."""
|
||||
super(GlancesStatsClient, self).__init__()
|
||||
|
||||
# Init the configuration
|
||||
self.config = config
|
||||
|
||||
# Init the arguments
|
||||
self.args = args
|
||||
|
||||
# Load plugins and exports
|
||||
self.load_plugins_and_exports(self.args)
|
||||
|
||||
def set_plugins(self, input_plugins):
|
||||
"""Set the plugin list according to the Glances server."""
|
||||
header = "glances_"
|
||||
for item in input_plugins:
|
||||
# Import the plugin
|
||||
plugin = __import__(header + item)
|
||||
# Add the plugin to the dictionary
|
||||
# The key is the plugin name
|
||||
# for example, the file glances_xxx.py
|
||||
# generate self._plugins_list["xxx"] = ...
|
||||
logger.debug("Server uses {0} plugin".format(item))
|
||||
self._plugins[item] = plugin.Plugin()
|
||||
# Restoring system path
|
||||
sys.path = sys_path
|
||||
|
||||
def update(self, input_stats):
|
||||
"""Update all the stats."""
|
||||
# For Glances client mode
|
||||
for p in input_stats:
|
||||
# Update plugin stats with items sent by the server
|
||||
self._plugins[p].set_stats(input_stats[p])
|
||||
# Update the views for the updated stats
|
||||
self._plugins[p].update_views()
|
109
glances/stats_client_snmp.py
Normal file
109
glances/stats_client_snmp.py
Normal file
@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Glances 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""The stats manager."""
|
||||
|
||||
import re
|
||||
|
||||
from glances.stats import GlancesStats
|
||||
from glances.compat import iteritems
|
||||
from glances.logger import logger
|
||||
|
||||
# SNMP OID regexp pattern to short system name dict
|
||||
oid_to_short_system_name = {'.*Linux.*': 'linux',
|
||||
'.*Darwin.*': 'mac',
|
||||
'.*BSD.*': 'bsd',
|
||||
'.*Windows.*': 'windows',
|
||||
'.*Cisco.*': 'cisco',
|
||||
'.*VMware ESXi.*': 'esxi',
|
||||
'.*NetApp.*': 'netapp'}
|
||||
|
||||
|
||||
class GlancesStatsClientSNMP(GlancesStats):
|
||||
|
||||
"""This class stores, updates and gives stats for the SNMP client."""
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
super(GlancesStatsClientSNMP, self).__init__()
|
||||
|
||||
# Init the configuration
|
||||
self.config = config
|
||||
|
||||
# Init the arguments
|
||||
self.args = args
|
||||
|
||||
# OS name is used because OID is differents between system
|
||||
self.os_name = None
|
||||
|
||||
# Load plugins and export modules
|
||||
self.load_plugins_and_exports(self.args)
|
||||
|
||||
def check_snmp(self):
|
||||
"""Chek if SNMP is available on the server."""
|
||||
# Import the SNMP client class
|
||||
from glances.snmp import GlancesSNMPClient
|
||||
|
||||
# Create an instance of the SNMP client
|
||||
clientsnmp = GlancesSNMPClient(host=self.args.client,
|
||||
port=self.args.snmp_port,
|
||||
version=self.args.snmp_version,
|
||||
community=self.args.snmp_community,
|
||||
user=self.args.snmp_user,
|
||||
auth=self.args.snmp_auth)
|
||||
|
||||
# If we cannot grab the hostname, then exit...
|
||||
ret = clientsnmp.get_by_oid("1.3.6.1.2.1.1.5.0") != {}
|
||||
if ret:
|
||||
# Get the OS name (need to grab the good OID...)
|
||||
oid_os_name = clientsnmp.get_by_oid("1.3.6.1.2.1.1.1.0")
|
||||
try:
|
||||
self.system_name = self.get_system_name(oid_os_name['1.3.6.1.2.1.1.1.0'])
|
||||
logger.info("SNMP system name detected: {0}".format(self.system_name))
|
||||
except KeyError:
|
||||
self.system_name = None
|
||||
logger.warning("Cannot detect SNMP system name")
|
||||
|
||||
return ret
|
||||
|
||||
def get_system_name(self, oid_system_name):
|
||||
"""Get the short os name from the OS name OID string."""
|
||||
short_system_name = None
|
||||
|
||||
if oid_system_name == '':
|
||||
return short_system_name
|
||||
|
||||
# Find the short name in the oid_to_short_os_name dict
|
||||
for r, v in iteritems(oid_to_short_system_name):
|
||||
if re.search(r, oid_system_name):
|
||||
short_system_name = v
|
||||
break
|
||||
|
||||
return short_system_name
|
||||
|
||||
def update(self):
|
||||
"""Update the stats using SNMP."""
|
||||
# For each plugins, call the update method
|
||||
for p in self._plugins:
|
||||
# Set the input method to SNMP
|
||||
self._plugins[p].input_method = 'snmp'
|
||||
self._plugins[p].short_system_name = self.system_name
|
||||
try:
|
||||
self._plugins[p].update()
|
||||
except Exception as e:
|
||||
logger.error("Update {0} failed: {1}".format(p, e))
|
68
glances/stats_server.py
Normal file
68
glances/stats_server.py
Normal file
@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
|
||||
#
|
||||
# Glances is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Glances 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 Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""The stats server manager."""
|
||||
|
||||
import collections
|
||||
|
||||
from glances.stats import GlancesStats
|
||||
|
||||
|
||||
class GlancesStatsServer(GlancesStats):
|
||||
|
||||
"""This class stores, updates and gives stats for the server."""
|
||||
|
||||
def __init__(self, config=None):
|
||||
# Init the stats
|
||||
super(GlancesStatsServer, self).__init__(config)
|
||||
|
||||
# Init the all_stats dict used by the server
|
||||
# all_stats is a dict of dicts filled by the server
|
||||
self.all_stats = collections.defaultdict(dict)
|
||||
|
||||
def update(self, input_stats=None):
|
||||
"""Update the stats."""
|
||||
input_stats = input_stats or {}
|
||||
|
||||
# Force update of all the stats
|
||||
super(GlancesStatsServer, self).update()
|
||||
|
||||
# Build all_stats variable (concatenation of all the stats)
|
||||
self.all_stats = self._set_stats(input_stats)
|
||||
|
||||
def _set_stats(self, input_stats):
|
||||
"""Set the stats to the input_stats one."""
|
||||
# Build the all_stats with the get_raw() method of the plugins
|
||||
ret = collections.defaultdict(dict)
|
||||
for p in self._plugins:
|
||||
ret[p] = self._plugins[p].get_raw()
|
||||
return ret
|
||||
|
||||
def getAll(self):
|
||||
"""Return the stats as a list."""
|
||||
return self.all_stats
|
||||
|
||||
def getAllAsDict(self):
|
||||
"""Return the stats as a dict."""
|
||||
# Python > 2.6
|
||||
# return {p: self.all_stats[p] for p in self._plugins}
|
||||
ret = {}
|
||||
for p in self._plugins:
|
||||
ret[p] = self.all_stats[p]
|
||||
return ret
|
3
setup.py
3
setup.py
@ -50,13 +50,12 @@ def get_requires():
|
||||
|
||||
setup(
|
||||
name='Glances',
|
||||
version='2.6',
|
||||
version='2.6.1',
|
||||
description="A cross-platform curses-based monitoring tool",
|
||||
long_description=open('README.rst').read(),
|
||||
author='Nicolas Hennion',
|
||||
author_email='nicolas@nicolargo.com',
|
||||
url='https://github.com/nicolargo/glances',
|
||||
# download_url='https://s3.amazonaws.com/glances/glances-2.6.tar.gz',
|
||||
license="LGPL",
|
||||
keywords="cli curses monitoring system",
|
||||
install_requires=get_requires(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Required metadata
|
||||
sonar.projectKey=glances
|
||||
sonar.projectName=Glances
|
||||
sonar.projectVersion=2.6
|
||||
sonar.projectVersion=2.6.1
|
||||
|
||||
# Path to the parent source code directory.
|
||||
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
|
||||
|
@ -25,8 +25,8 @@ import subprocess
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from glances import __version__
|
||||
from glances.compat import text_type
|
||||
from glances.globals import version
|
||||
|
||||
import requests
|
||||
|
||||
@ -36,7 +36,7 @@ pid = None
|
||||
|
||||
# Unitest class
|
||||
# ==============
|
||||
print('RESTful API unitary tests for Glances %s' % version)
|
||||
print('RESTful API unitary tests for Glances %s' % __version__)
|
||||
|
||||
|
||||
class TestGlances(unittest.TestCase):
|
||||
|
@ -26,8 +26,8 @@ import subprocess
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from glances import __version__
|
||||
from glances.compat import ServerProxy
|
||||
from glances.globals import version
|
||||
|
||||
SERVER_PORT = 61234
|
||||
URL = "http://localhost:%s" % SERVER_PORT
|
||||
@ -38,7 +38,7 @@ client = ServerProxy(URL)
|
||||
|
||||
# Unitest class
|
||||
# ==============
|
||||
print('XML-RPC API unitary tests for Glances %s' % version)
|
||||
print('XML-RPC API unitary tests for Glances %s' % __version__)
|
||||
|
||||
|
||||
class TestGlances(unittest.TestCase):
|
||||
|
@ -38,12 +38,13 @@ if not core.is_standalone():
|
||||
from glances.stats import GlancesStats
|
||||
stats = GlancesStats()
|
||||
|
||||
from glances.globals import WINDOWS, version
|
||||
from glances import __version__
|
||||
from glances.globals import WINDOWS
|
||||
from glances.outputs.glances_bars import Bar
|
||||
|
||||
# Unitest class
|
||||
# ==============
|
||||
print('Unitary tests for Glances %s' % version)
|
||||
print('Unitary tests for Glances %s' % __version__)
|
||||
|
||||
|
||||
class TestGlances(unittest.TestCase):
|
||||
|
Loading…
Reference in New Issue
Block a user