diff --git a/.travis.yml b/.travis.yml index 8cbebe76..715fd5dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,14 @@ language: python python: - - "2.6" - "2.7" - "3.3" - "3.4" + - "pypy" install: - pip install -r requirements.txt -script: python setup.py install + - pip install coveralls +script: + - python setup.py install + - coverage run --source=glances unitest.py +after_success: + - coveralls \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index b8784100..b08248bc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -20,8 +20,8 @@ https://github.com/jrenner Packagers ========= -Geoffroy Youri Berret for the Debian package -http://packages.debian.org/fr/sid/glances +Rémi Verchère for the Debian package +https://github.com/rverchere/debian-glances gasol.wu@gmail.com for the FreeBSD port diff --git a/NEWS b/NEWS index a1b276ba..0d17adf7 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,49 @@ Glances Version 2.x ============================================================================== +Version 2.1 +=========== + + * Add user process filter feature + User can define a process filter pattern (as a regular expression). + The pattern could be defined from the command line (-f ) + or by pressing the ENTER key in the curse interface. + For the moment, process filter feature is only available in standalone mode. + * Add extended processes informations for top process + Top process stats availables: CPU affinity, extended memory information (shared, text, lib, datat, dirty, swap), openned threads/files and TCP/UDP network sessions, IO nice level + For the moment, extended processes stats are only available in standalone mode. + * Add --process-short-name tag and '/' key to switch between short/command line + * Create a max_processes key in the configuration file + The goal is to reduce the number of displayed processes in the curses UI and + so limit the CPU footprint of the Glances standalone mode. + The API always return all the processes, the key is only active in the curses UI. + If the key is not define, all the processes will be displayed. + The default value is 20 (processes displayed). + For the moment, this feature is only available in standalone mode. + * Alias for network interfaces, disks and sensors + Users can configure alias from the Glances configuration file. + * Add Glances log message (in the /tmp/glances.log file) + The default log level is INFO, you can switch to the DEBUG mode using the -d option on the command line. + * Add RESTFul API to the Web server mode + RestFul API doc: https://github.com/nicolargo/glances/wiki/The-Glances-RESTFULL-JSON-API + * Improve SNMP fallback mode for Cisco IOS, VMware ESXi + * Add --theme-white feature to optimize display for white background + * Experimental history feature (--enable-history option on the command line) + This feature allows users to generate graphs within the curse interface. + Graphs are available for CPU, LOAD and MEM. + To generate graph, click on the 'g' key. + To reset the history, press the 'r' key. + Note: This feature uses the matplotlib library. + * CI: Improve Travis coverage + +Bugs corrected: + + * Quitting glances leaves a column layout to the current terminal (issue #392) + * Glances crashes with malformed UTF-8 sequences in process command lines (issue #391) + * SNMP fallback mode is not Python 3 compliant (issue #386) + * Trouble using batinfo, hddtemp, pysensors w/ Python (issue #324) + + Version 2.0.1 ============= @@ -325,13 +368,13 @@ Version 1.3 =========== * Add file system stats (total and used space) - * Adapt unit dynamicaly (K, M, G) + * Adapt unit dynamically (K, M, G) * Add man page (Thanks to Edouard Bourguignon) Version 1.2 =========== - * Resize the terminal and the windows are adapted dynamicaly + * Resize the terminal and the windows are adapted dynamically * Refresh screen instantanetly when a key is pressed Version 1.1.3 diff --git a/README.rst b/README.rst index f57edb1c..2e1f5723 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,18 @@ Optional dependencies: Installation ============ +Glances Auto Install script +--------------------------- + +Just enter the following command line: + +.. code-block:: console + + curl -L http://bit.ly/glances | /bin/bash + +*Note*: Only supported on some GNU/Linux distributions. +If you want to support others distribs, please contribute to `glancesautoinstall`_. + PyPI: The simple way -------------------- @@ -188,6 +200,8 @@ Documentation For complete documentation see `glances-doc`_. +If you have any question (after RTFM !), please post it on the official Q&A `forum`_. + Author ====== @@ -199,6 +213,7 @@ License LGPL. See ``COPYING`` for more details. .. _psutil: https://code.google.com/p/psutil/ +.. _glancesautoinstall: https://github.com/nicolargo/glancesautoinstall .. _@nicolargo: https://twitter.com/nicolargo .. _@glances_system: https://twitter.com/glances_system .. _18Nbs6kg9UCqtX4RPDM3qMkeKwjDxBFYrW: bitcoin:18Nbs6kg9UCqtX4RPDM3qMkeKwjDxBFYrW?amount=1X8&label=Glances @@ -209,3 +224,4 @@ LGPL. See ``COPYING`` for more details. .. _colorconsole: https://pypi.python.org/pypi/colorconsole .. _Puppet: https://puppetlabs.com/puppet/what-is-puppet/ .. _glances-doc: https://github.com/nicolargo/glances/blob/master/docs/glances-doc.rst +.. _forum: https://groups.google.com/forum/?hl=en#!forum/glances-users diff --git a/conf/glances-test.conf b/conf/glances-test.conf index 450e6810..fef896ec 100644 --- a/conf/glances-test.conf +++ b/conf/glances-test.conf @@ -51,7 +51,9 @@ critical=90 [network] # Define the list of hidden network interfaces (comma separeted) hide=lo -# Default limits (in bits per second aka bps) for interface bitrate +# WLAN0 alias name +wlan0_alias=Wireless +# WLAN0 Default limits (in bits per second aka bps) for interface bitrate wlan0_rx_careful=4000000 wlan0_rx_warning=5000000 wlan0_rx_critical=6000000 @@ -62,6 +64,8 @@ wlan0_tx_critical=1000000 [diskio] # Define the list of hidden disks (comma separeted) hide=sda2,sda5 +# Alias for sda1 +#sda1_alias=IntDisk [fs] # Default limits for free filesytem space in % @@ -85,8 +89,17 @@ temperature_hdd_critical=60 battery_careful=80 battery_warning=90 battery_critical=95 +# Sensors alias +temp1_alias=Motherboard 0 +temp2_alias=Motherboard 1 +core 0_alias=CPU Core 0 +core 1_alias=CPU Core 1 [processlist] +# Maximum number of processes to show in the UI +# Note: Only limit number of showed processes (not the one returned by the API) +# Default is 20 processes (Top 20) +max_processes=20 # Limit values for CPU/MEM per process in % # Default values if not defined: 50/70/90 cpu_careful=50 diff --git a/conf/glances.conf b/conf/glances.conf index 81155b31..c0dbb077 100644 --- a/conf/glances.conf +++ b/conf/glances.conf @@ -51,7 +51,9 @@ critical=90 #[network] # Define the list of hidden network interfaces (comma separeted) #hide=lo -# Default limits (in bits per second aka bps) for interface bitrate +# WLAN 0 alias +#wlan0_alias=Wireless IF +# WLAN 0 Default limits (in bits per second aka bps) for interface bitrate #wlan0_rx_careful=4000000 #wlan0_rx_warning=5000000 #wlan0_rx_critical=6000000 @@ -62,6 +64,8 @@ critical=90 #[diskio] # Define the list of hidden disks (comma separeted) #hide=sda2,sda5 +# Alias for sda1 +#sda1_alias=IntDisk [fs] # Default limits for free filesytem space in % @@ -85,8 +89,17 @@ temperature_hdd_critical=60 battery_careful=80 battery_warning=90 battery_critical=95 +# Sensors alias +#temp1_alias=Motherboard 0 +#temp2_alias=Motherboard 1 +#core 0_alias=CPU Core 0 +#core 1_alias=CPU Core 1 [processlist] +# Maximum number of processes to show in the UI +# Note: Only limit number of showed processes (not the one returned by the API) +# Default is 20 processes (Top 20) +max_processes=20 # Limit values for CPU/MEM per process in % # Default values if not defined: 50/70/90 cpu_careful=50 diff --git a/docs/glances-doc.html b/docs/glances-doc.html index 1f044548..32501916 100644 --- a/docs/glances-doc.html +++ b/docs/glances-doc.html @@ -123,9 +123,9 @@ td.option-group {

Glances

-

This manual describes Glances version 2.0.1.

+

This manual describes Glances version 2.1.

Copyright © 2012-2014 Nicolas Hennion <nicolas@nicolargo.com>

-

June 2014

+

September 2014

@@ -206,7 +207,7 @@ TCP port -p PORT.

In client mode, you can set the TCP port of the server -p PORT.

You can also set a password to access to the server --password.

Default binding address is 0.0.0.0 (Glances will listen on all the -network interfaces) and TCP port is 61209.

+available network interfaces) and TCP port is 61209.

In client/server mode, limits are set by the server side.

Glances is IPv6 compatible. Just use the -B :: option to bind to all IPv6 addresses.

@@ -215,13 +216,12 @@ client, the latter will try to grab stats using the client$ glances -c @snmpserver -

Known issues: grab using SNMP is only validated for GNU/Linux with SNMP -v2/2c server.

+

Note: Stats grabbed by SNMP request are limited.

Web Server Mode

If you want to remotely monitor a machine, called server, from any -device with a web browser, called client, just run on the server:

+device with a web browser, just run on the server:

 server$ glances -w
 
@@ -239,6 +239,7 @@ http://@server:61208

Command Reference

Command-Line Options

+
@@ -250,21 +251,16 @@ http://@server:61208 -V, --version - - - - - - - +-d, --debug + + + + @@ -274,7 +270,7 @@ hostname - + @@ -287,6 +283,10 @@ hostname--disable-process + + + @@ -295,16 +295,29 @@ hostname + + + + + + + + + + + - - @@ -325,6 +338,9 @@ file--snmp-auth SNMP_AUTH + + @@ -332,17 +348,40 @@ file - + + + + + + + + + + +
show program's version number and exit
--b, --bytedisplay network rate in byte per second
--B BIND_ADDRESS, --bind BIND_ADDRESS
 bind server to the given IPv4/IPv6 address or hostname
--c CLIENT, --client CLIENT
 connect to a Glances server by IPv4/IPv6 address or -hostname
Enable debug mode
-C CONF_FILE, --config CONF_FILE
 path to the configuration file
+--enable-history
 enable the history mode
--disable-bold disable bold mode in the terminal
 disable disk I/O module
--disable-fsdisable file system module
disable filesystem module
--disable-network
 disable process module
+--disable-process-extended
 disable extended stats on top process
--disable-log disable log module
 export stats to a CSV file
+-c CLIENT, --client CLIENT
 connect to a Glances server by IPv4/IPv6 address or +hostname
+-s, --serverrun Glances in server mode
-p PORT, --port PORT
 define the client/server TCP port [default: 61209]
+-B BIND_ADDRESS, --bind BIND_ADDRESS
 bind server to the given IPv4/IPv6 address or hostname
+--password-badidea PASSWORD_ARG
 define password from the command line
--password define a client/server password from the prompt or file
--s, --serverrun Glances in server mode
--snmp-community SNMP_COMMUNITY
 SNMP authentication key (only for SNMPv3)
+--snmp-forceforce SNMP mode
-t TIME, --time TIME
-w, --webserver
 run Glances in Web server mode
 run Glances in web server mode
+-f PROCESS_FILTER, --process-filter PROCESS_FILTER
 set the process filter patern (regular expression)
+--process-short-name
 force short name for processes name
+-b, --bytedisplay network rate in byte per second
-1, --percpu start Glances in per CPU mode
+--theme-whiteoptimize display for white background
+

Interactive Commands

The following commands (key pressed) are supported while in Glances:

+
ENTER
+

Set the process filter +Filter is a regular expression pattern:

+
    +
  • gnome: all processes starting with the gnome string
  • +
  • .*gnome.*: all processes containing the gnome string
  • +
+
a

Sort process list automatically

    @@ -357,8 +396,12 @@ file
    Sort processes by CPU usage
    d
    Show/hide disk I/O stats
    +
    e
    +
    Enable/disable top extended stats
    f
    Show/hide file system stats
    +
    g
    +
    Generate hraphs for current history
    h
    Show/hide the help screen
    i
    @@ -373,6 +416,8 @@ file
    Sort processes by name
    q
    Quit
    +
    r
    +
    Reset history
    s
    Show/hide sensors stats
    t
    @@ -387,16 +432,17 @@ file
    Show/hide processes stats
    1
    Switch between global CPU and per-CPU stats
    +
    /
    +
    Switch between short name / command line (processes name)

Configuration

-

Caution! Glances version 1.x configuration files are not compatible -with the version 2.x.

No configuration file is mandatory to use Glances.

Furthermore a configuration file is needed to set up limits, disks or -network interfaces to hide and/or monitored processes list.

+network interfaces to hide and/or monitored processes list or to define +alias.

By default, the configuration file is under:

@@ -417,6 +463,8 @@ C:\Documents and Settings\<User>\Application Data

Since Windows Vista and newer versions:

 C:\Users\<User>\AppData\Roaming
+or
+%userprofile%\AppData\Roaming
 

You can override the default configuration, located in one of the above directories on your system, except for Windows.

@@ -429,10 +477,28 @@ cp /usr/share/doc/glances/glances.conf $XDG_CONFIG_HOME/glances/

On OS X, you should copy the configuration file to ~/Library/Application Support/glances/.

+
+

Logs and debug mode

+

Glances logs all its internal messages to a log file. By default, only +INFO & WARNING & ERROR &CRITICAL levels are logged, but DEBUG messages +can ben logged using the -d option on the command line.

+

By default, the configuration file is under:

+
+++ + + + + + + +
Linux, *BSD and OS X:
 /tmp/glances.conf
Windows:%APPDATA%\Local\temp\glances.conf
+
-

Anatomy Of The Application

+

Anatomy Of The Application

-

Legend

+

Legend

GREEN stat counter is "OK"
BLUE stat counter is "CAREFUL"
@@ -443,7 +509,7 @@ cp /usr/share/doc/glances/glances.conf $XDG_CONFIG_HOME/glances/ view.

-

CPU

+

CPU

Short view:

images/cpu.png

If enough horizontal space is available, extended CPU information are @@ -476,7 +542,7 @@ time. The total CPU usage is displayed on the first line.

the [cpu] and/or [percpu] sections.

-

Load

+

Load

images/load.png

On the No Sheep blog, Zachary Tirrell defines the load average [1]:

@@ -496,7 +562,7 @@ The first line also displays the number of CPU core.

the [load] section.

-

Memory

+

Memory

Glances uses two columns: one for the RAM and one for the SWAP.

images/mem.png

If enough space is available, Glances displays extended information for @@ -513,7 +579,7 @@ the RAM:

the [memory] and/or [memswap] sections.

-

Network

+

Network

images/network.png

Glances displays the network interface bit rate. The unit is adapted dynamically (bits per second, kbits per second, Mbits per second, etc).

@@ -521,18 +587,18 @@ dynamically (bits per second, kbits per second, Mbits per second, etc).

(see sample in the configuration file).

Note: it is possibile to define a list of network interfaces to hide and per-interface limit values in the [network] section of the -configuration file.

+configuration file and aliases for interface name.

-

Disk I/O

+

Disk I/O

images/diskio.png

Glances displays the disk I/O throughput. The unit is adapted dynamically.

There is no alert on this information.

Note: it is possible to define a list of disks to hide under the -[diskio] section in the configuration file.

+[diskio] section in the configuration file and aliases for disk name.

-

File System

+

File System

images/fs.png

Glances displays the used and total file system disk space. The unit is adapted dynamically.

@@ -547,7 +613,7 @@ adapted dynamically.

the [filesystem] section.

-

Sensors

+

Sensors

Glances can displays the sensors information using lm-sensors, hddtemp and batinfo [2].

All of the above libraries are available only on Linux.

@@ -555,11 +621,11 @@ the [filesystem] section.

temperature only.

images/sensors.png

There is no alert on this information.

-

Note: limit values can be overwritten in the configuration file under -the [sensors] section.

+

Note: limit values and sensors alias names can be defined in the configuration +file under the [sensors] section.

-

Processes List

+

Processes List

Compact view:

images/processlist.png

Full view:

@@ -612,7 +678,8 @@ automatically sorted by:

IOW/s
Per process I/O write rate (in Byte/s)
COMMAND
-
Process command line (process name is highlighted)
+
Process command line +User cans switch to the process name by pressing on the / key

Process status legend:

@@ -627,11 +694,20 @@ automatically sorted by:

Z
Zombie
+

In standalone mode, additionals informations are provided for the top process:

+images/processlist-top.png +
    +
  • CPU affinity (number of cores used by the process)
  • +
  • Extended memory information (swap, shared, text, lib, data and dirty on Linux)
  • +
  • Openned threads, files and network sessions (TCP and UDP)
  • +
  • IO nice level
  • +
+

The extended stats feature could be disabled using the --disable-process-extended option (command line) or the e key (curses interface).

Note: limit values can be overwritten in the configuration file under the [process] section.

-

Monitored Processes List

+

Monitored Processes List

The monitored processes list allows user, through the configuration file, to group processes and quickly show if the number of running processes is not good.

@@ -683,7 +759,7 @@ get the JSON representation of the monitored processes list.

-

Logs

+

Logs

images/logs.png

A log messages list is displayed in the bottom of the screen if (and only if):

@@ -703,7 +779,7 @@ processes list alerts
-

Other Outputs

+

Other Outputs

It is possible to export statistics to CSV file.

 $ glances --output-csv /tmp/glances.csv
@@ -714,17 +790,21 @@ processes list alerts
 
  • Stats (comma separated)
  • -
    -

    API Documentation

    -

    Glances uses a XML-RPC server and can be used by another client software.

    -

    API documentation is available at -https://github.com/nicolargo/glances/wiki/The-Glances-2.x-API-How-to.

    +
    +

    APIs Documentations

    +

    Glances includes a XML-RPC server and a RESTFULL-JSON API which and can be used by another client software.

    +

    APIs documentations are available at:

    +
    -

    Support

    +

    Support

    +

    To post a question about Glances use case, please post it to the offical Q&A forum.

    To report a bug or a feature request use the bug tracking system at https://github.com/nicolargo/glances/issues.

    -

    Feel free to contribute!

    +

    Feel free to contribute !

    diff --git a/docs/glances-doc.rst b/docs/glances-doc.rst index c41e49d0..7f5f0b63 100644 --- a/docs/glances-doc.rst +++ b/docs/glances-doc.rst @@ -2,11 +2,11 @@ Glances ======= -This manual describes *Glances* version 2.0.1. +This manual describes *Glances* version 2.1. Copyright © 2012-2014 Nicolas Hennion -June 2014 +September 2014 .. contents:: Table of Contents @@ -75,7 +75,7 @@ In client mode, you can set the TCP port of the server ``-p PORT``. You can also set a password to access to the server ``--password``. Default binding address is ``0.0.0.0`` (Glances will listen on all the -network interfaces) and TCP port is ``61209``. +available network interfaces) and TCP port is ``61209``. In client/server mode, limits are set by the server side. @@ -89,14 +89,13 @@ client, the latter will try to grab stats using the ``SNMP`` protocol: client$ glances -c @snmpserver -Known issues: grab using SNMP is only validated for GNU/Linux with SNMP -v2/2c server. +Note: Stats grabbed by SNMP request are limited. Web Server Mode --------------- If you want to remotely monitor a machine, called ``server``, from any -device with a web browser, called ``client``, just run on the server: +device with a web browser, just run on the server: .. code-block:: console @@ -123,48 +122,65 @@ Command Reference Command-Line Options -------------------- --h, --help show this help message and exit --V, --version show program's version number and exit --b, --byte display network rate in byte per second --B BIND_ADDRESS, --bind BIND_ADDRESS - bind server to the given IPv4/IPv6 address or hostname --c CLIENT, --client CLIENT - connect to a Glances server by IPv4/IPv6 address or - hostname --C CONF_FILE, --config CONF_FILE - path to the configuration file ---disable-bold disable bold mode in the terminal ---disable-diskio disable disk I/O module ---disable-fs disable file system module ---disable-network disable network module ---disable-sensors disable sensors module ---disable-process disable process module ---disable-log disable log module ---output-csv OUTPUT_CSV - export stats to a CSV file --p PORT, --port PORT define the client/server TCP port [default: 61209] ---password define a client/server password from the prompt or - file --s, --server run Glances in server mode ---snmp-community SNMP_COMMUNITY - SNMP community ---snmp-port SNMP_PORT - SNMP port ---snmp-version SNMP_VERSION - SNMP version (1, 2c or 3) ---snmp-user SNMP_USER - SNMP username (only for SNMPv3) ---snmp-auth SNMP_AUTH - SNMP authentication key (only for SNMPv3) --t TIME, --time TIME set refresh time in seconds [default: 3 sec] --w, --webserver run Glances in Web server mode --1, --percpu start Glances in per CPU mode + -h, --help show this help message and exit + -V, --version show program's version number and exit + -d, --debug Enable debug mode + -C CONF_FILE, --config CONF_FILE + path to the configuration file + --enable-history enable the history mode + --disable-bold disable bold mode in the terminal + --disable-diskio disable disk I/O module + --disable-fs disable filesystem module + --disable-network disable network module + --disable-sensors disable sensors module + --disable-process disable process module + --disable-process-extended + disable extended stats on top process + --disable-log disable log module + --output-csv OUTPUT_CSV + export stats to a CSV file + -c CLIENT, --client CLIENT + connect to a Glances server by IPv4/IPv6 address or + hostname + -s, --server run Glances in server mode + -p PORT, --port PORT define the client/server TCP port [default: 61209] + -B BIND_ADDRESS, --bind BIND_ADDRESS + bind server to the given IPv4/IPv6 address or hostname + --password-badidea PASSWORD_ARG + define password from the command line + --password define a client/server password from the prompt or + file + --snmp-community SNMP_COMMUNITY + SNMP community + --snmp-port SNMP_PORT + SNMP port + --snmp-version SNMP_VERSION + SNMP version (1, 2c or 3) + --snmp-user SNMP_USER + SNMP username (only for SNMPv3) + --snmp-auth SNMP_AUTH + SNMP authentication key (only for SNMPv3) + --snmp-force force SNMP mode + -t TIME, --time TIME set refresh time in seconds [default: 3 sec] + -w, --webserver run Glances in web server mode + -f PROCESS_FILTER, --process-filter PROCESS_FILTER + set the process filter patern (regular expression) + --process-short-name force short name for processes name + -b, --byte display network rate in byte per second + -1, --percpu start Glances in per CPU mode + --theme-white optimize display for white background Interactive Commands -------------------- The following commands (key pressed) are supported while in Glances: +``ENTER`` + Set the process filter + Filter is a regular expression pattern: + + - gnome: all processes starting with the gnome string + - .*gnome.*: all processes containing the gnome string ``a`` Sort process list automatically @@ -177,8 +193,12 @@ The following commands (key pressed) are supported while in Glances: Sort processes by CPU usage ``d`` Show/hide disk I/O stats +``e`` + Enable/disable top extended stats ``f`` Show/hide file system stats +``g`` + Generate hraphs for current history ``h`` Show/hide the help screen ``i`` @@ -193,6 +213,8 @@ The following commands (key pressed) are supported while in Glances: Sort processes by name ``q`` Quit +``r`` + Reset history ``s`` Show/hide sensors stats ``t`` @@ -207,17 +229,17 @@ The following commands (key pressed) are supported while in Glances: Show/hide processes stats ``1`` Switch between global CPU and per-CPU stats +``/`` + Switch between short name / command line (processes name) Configuration ============= -**Caution! Glances version 1.x configuration files are not compatible -with the version 2.x.** - No configuration file is mandatory to use Glances. Furthermore a configuration file is needed to set up limits, disks or -network interfaces to hide and/or monitored processes list. +network interfaces to hide and/or monitored processes list or to define +alias. By default, the configuration file is under: @@ -236,6 +258,8 @@ Since Windows Vista and newer versions: :: C:\Users\\AppData\Roaming + or + %userprofile%\AppData\Roaming You can override the default configuration, located in one of the above directories on your system, except for Windows. @@ -251,6 +275,18 @@ e.g., on Linux: On OS X, you should copy the configuration file to ``~/Library/Application Support/glances/``. +Logs and debug mode +=================== + +Glances logs all its internal messages to a log file. By default, only +INFO & WARNING & ERROR &CRITICAL levels are logged, but DEBUG messages +can ben logged using the -d option on the command line. + +By default, the configuration file is under: + +:Linux, \*BSD and OS X: ``/tmp/glances.conf`` +:Windows: ``%APPDATA%\Local\temp\glances.conf`` + Anatomy Of The Application ========================== @@ -371,7 +407,7 @@ Alerts are only set if the maximum speed per network interface is available *Note*: it is possibile to define a list of network interfaces to hide and per-interface limit values in the ``[network]`` section of the -configuration file. +configuration file and aliases for interface name. Disk I/O -------- @@ -383,7 +419,7 @@ Glances displays the disk I/O throughput. The unit is adapted dynamically. There is no alert on this information. *Note*: it is possible to define a list of disks to hide under the -``[diskio]`` section in the configuration file. +``[diskio]`` section in the configuration file and aliases for disk name. File System ----------- @@ -418,8 +454,8 @@ temperature only. There is no alert on this information. -*Note*: limit values can be overwritten in the configuration file under -the ``[sensors]`` section. +*Note*: limit values and sensors alias names can be defined in the configuration +file under the ``[sensors]`` section. Processes List -------------- @@ -480,7 +516,8 @@ The number of processes in the list is adapted to the screen size. ``IOW/s`` Per process I/O write rate (in Byte/s) ``COMMAND`` - Process command line (process name is highlighted) + Process command line + User cans switch to the process name by pressing on the ``/`` key Process status legend: @@ -495,6 +532,17 @@ Process status legend: ``Z`` Zombie +In standalone mode, additionals informations are provided for the top process: + +.. image:: images/processlist-top.png + +* CPU affinity (number of cores used by the process) +* Extended memory information (swap, shared, text, lib, data and dirty on Linux) +* Openned threads, files and network sessions (TCP and UDP) +* IO nice level + +The extended stats feature could be disabled using the --disable-process-extended option (command line) or the ``e`` key (curses interface). + *Note*: limit values can be overwritten in the configuration file under the ``[process]`` section. @@ -592,21 +640,25 @@ CSV files have two lines per stats: - Stats description - Stats (comma separated) -API Documentation -================= +APIs Documentations +=================== -Glances uses a `XML-RPC server`_ and can be used by another client software. +Glances includes a `XML-RPC server`_ and a `RESTFULL-JSON`_ API which and can be used by another client software. -API documentation is available at -https://github.com/nicolargo/glances/wiki/The-Glances-2.x-API-How-to. +APIs documentations are available at: + +- XML-RPC: https://github.com/nicolargo/glances/wiki/The-Glances-2.x-API-How-to +- RESTFULL-JSON: https://github.com/nicolargo/glances/wiki/The-Glances-RESTFULL-JSON-API Support ======= +To post a question about Glances use case, please post it to the offical Q&A `forum`_. + To report a bug or a feature request use the bug tracking system at https://github.com/nicolargo/glances/issues. -Feel free to contribute! +Feel free to contribute ! .. [1] http://nosheep.net/story/defining-unix-load-average/ @@ -614,3 +666,5 @@ Feel free to contribute! .. _psutil: https://code.google.com/p/psutil/ .. _XML-RPC server: http://docs.python.org/2/library/simplexmlrpcserver.html +.. _RESTFULL-JSON: http://jsonapi.org/ +.. _forum: https://groups.google.com/forum/?hl=en#!forum/glances-users \ No newline at end of file diff --git a/docs/images/processlist-top.png b/docs/images/processlist-top.png new file mode 100644 index 00000000..dd410c6d Binary files /dev/null and b/docs/images/processlist-top.png differ diff --git a/docs/images/screenshot-wide.png b/docs/images/screenshot-wide.png index a50411ef..5f0a8221 100644 Binary files a/docs/images/screenshot-wide.png and b/docs/images/screenshot-wide.png differ diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png index 0e0e9470..197d34e3 100644 Binary files a/docs/images/screenshot.png and b/docs/images/screenshot.png differ diff --git a/glances/__init__.py b/glances/__init__.py index d5f119be..2ca51a14 100644 --- a/glances/__init__.py +++ b/glances/__init__.py @@ -20,7 +20,7 @@ """Init the Glances software.""" __appname__ = 'glances' -__version__ = '2.0.1' +__version__ = '2.1' __author__ = 'Nicolas Hennion ' __license__ = 'LGPL' @@ -29,6 +29,7 @@ import gettext import locale import signal import sys +import platform # Import psutil try: @@ -37,19 +38,26 @@ except ImportError: print('psutil library not found. Glances cannot start.') sys.exit(1) -# 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: - print('psutil version {0} detected.').format(__psutil_version) - print('psutil 2.0 or higher is needed. Glances cannot start.') - sys.exit(1) - # Import Glances libs # Note: others Glances libs will be imported optionally -from glances.core.glances_globals import gettext_domain, locale_dir +from glances.core.glances_globals import gettext_domain, locale_dir, logger from glances.core.glances_main import GlancesMain +# Get PSutil version +psutil_min_version = (2, 0, 0) +psutil_version = tuple([int(num) for num in __psutil_version.split('.')]) + +# First log with Glances and PSUtil version +logger.info('Start Glances {0}'.format(__version__)) +logger.info('{0} {1} and PSutil {2} detected'.format(platform.python_implementation(), + platform.python_version(), + __psutil_version)) + +# Check PSutil version +if psutil_version < psutil_min_version: + logger.critical('PSutil 2.0 or higher is needed. Glances cannot start.') + sys.exit(1) + def __signal_handler(signal, frame): """Callback for CTRL-C.""" @@ -61,12 +69,19 @@ def end(): if core.is_standalone(): # Stop the standalone (CLI) standalone.end() + logger.info("Stop Glances (with CTRL-C)") elif core.is_client(): # Stop the client client.end() + logger.info("Stop Glances client (with CTRL-C)") elif core.is_server(): # Stop the server server.end() + logger.info("Stop Glances server (with CTRL-C)") + elif core.is_webserver(): + # Stop the Web server + webserver.end() + logger.info("Stop Glances web server(with CTRL-C)") # The end... sys.exit(0) @@ -83,7 +98,7 @@ def main(): gettext.install(gettext_domain, locale_dir) # Share global var - global core, standalone, client, server + global core, standalone, client, server, webserver # Create the Glances main instance core = GlancesMain() @@ -93,6 +108,7 @@ def main(): # Glances can be ran in standalone, client or server mode if core.is_standalone(): + logger.info("Start standalone mode") # Import the Glances standalone module from glances.core.glances_standalone import GlancesStandalone @@ -105,6 +121,7 @@ def main(): standalone.serve_forever() elif core.is_client(): + logger.info("Start client mode") # Import the Glances client module from glances.core.glances_client import GlancesClient @@ -115,7 +132,7 @@ def main(): # Test if client and server are in the same major version if not client.login(): - print(_("Error: The server version is not compatible with the client")) + logger.critical(_("The server version is not compatible with the client")) sys.exit(2) # Start the client loop @@ -125,6 +142,7 @@ def main(): client.close() elif core.is_server(): + logger.info("Start server mode") # Import the Glances server module from glances.core.glances_server import GlancesServer @@ -147,6 +165,7 @@ def main(): server.server_close() elif core.is_webserver(): + logger.info("Start web server mode") # Import the Glances web server module from glances.core.glances_webserver import GlancesWebServer diff --git a/glances/core/glances_client.py b/glances/core/glances_client.py index 27fdeeae..faf70ae1 100644 --- a/glances/core/glances_client.py +++ b/glances/core/glances_client.py @@ -24,18 +24,28 @@ import json import socket import sys try: - from xmlrpc.client import ServerProxy, ProtocolError, Fault + from xmlrpc.client import Transport, ServerProxy, ProtocolError, Fault except ImportError: # Python 2 - from xmlrpclib import ServerProxy, ProtocolError, Fault + from xmlrpclib import Transport, ServerProxy, ProtocolError, Fault +import httplib # Import Glances libs -from glances.core.glances_globals import version +from glances.core.glances_globals import version, logger from glances.core.glances_stats import GlancesStatsClient from glances.outputs.glances_curses import GlancesCurses -class GlancesClient(object): +class GlancesClientTransport(Transport): + """This class overwrite the default XML-RPC transport and manage timeout""" + + def set_timeout(self, timeout): + self.timeout = timeout + def make_connection(self, host): + return httplib.HTTPConnection(host, timeout=self.timeout) + + +class GlancesClient(object): """This class creates and manages the TCP client.""" def __init__(self, config=None, args=None): @@ -54,10 +64,13 @@ class GlancesClient(object): uri = 'http://{0}:{1}'.format(args.client, args.port) # Try to connect to the URI + transport = GlancesClientTransport() + # Configure the server timeout to 7 seconds + transport.set_timeout(7) try: - self.client = ServerProxy(uri) + self.client = ServerProxy(uri, transport = transport) except Exception as e: - print(_("Error: Couldn't create socket {0}: {1}").format(uri, e)) + logger.error(_("Couldn't create socket {0}: {1}").format(uri, e)) sys.exit(2) def set_mode(self, mode='glances'): @@ -81,39 +94,47 @@ class GlancesClient(object): """Logon to the server.""" ret = True - # First of all, trying to connect to a Glances server - try: - client_version = self.client.init() + if not self.args.snmp_force: + # First of all, trying to connect to a Glances server self.set_mode('glances') - except socket.error as err: - # print(_("Error: Connection to {0} server failed").format(self.get_mode())) - # Fallback to SNMP - self.set_mode('snmp') - except ProtocolError as err: - # Others errors - if str(err).find(" 401 ") > 0: - print(_("Error: Connection to server failed: Bad password")) - else: - print(_("Error: Connection to server failed: {0}").format(err)) - sys.exit(2) + client_version = None + try: + client_version = self.client.init() + except socket.error as err: + # Fallback to SNMP + logger.error(_("Connection to Glances server failed")) + self.set_mode('snmp') + fallbackmsg = _("Trying fallback to SNMP...") + print(fallbackmsg) + except ProtocolError as err: + # Others errors + if str(err).find(" 401 ") > 0: + logger.error(_("Connection to server failed (Bad password)")) + else: + logger.error(_("Connection to server failed ({0})").format(err)) + sys.exit(2) - if self.get_mode() == 'glances' and version[:3] == client_version[:3]: - # Init stats - self.stats = GlancesStatsClient() - self.stats.set_plugins(json.loads(self.client.getAllPlugins())) - elif self.get_mode() == 'snmp': - print (_("Info: Connection to Glances server failed. Trying fallback to SNMP...")) - # Then fallback to SNMP if needed + if self.get_mode() == 'glances' and version[:3] == client_version[:3]: + # Init stats + self.stats = GlancesStatsClient() + self.stats.set_plugins(json.loads(self.client.getAllPlugins())) + else: + logger.error("Client version: %s / Server version: %s" % (version, client_version)) + + else: + self.set_mode('snmp') + + if self.get_mode() == 'snmp': + logger.info(_("Trying to grab stats by SNMP...")) + # Fallback to SNMP if needed from glances.core.glances_stats import GlancesStatsClientSNMP # Init stats self.stats = GlancesStatsClientSNMP(args=self.args) if not self.stats.check_snmp(): - print(_("Error: Connection to SNMP server failed")) + logger.error(_("Connection to SNMP server failed")) sys.exit(2) - else: - ret = False if ret: # Load limits from the configuration file @@ -133,7 +154,8 @@ class GlancesClient(object): elif self.get_mode() == 'snmp': return self.update_snmp() else: - print(_("Error: Unknown server mode: {0}").format(self.get_mode())) + self.end() + logger.critical(_("Unknown server mode: {0}").format(self.get_mode())) sys.exit(2) def update_glances(self): @@ -183,8 +205,6 @@ class GlancesClient(object): # Update the screen self.screen.update(self.stats, cs_status=cs_status) - # print self.stats - # print self.stats.getAll() def end(self): """End of the client session.""" diff --git a/glances/core/glances_config.py b/glances/core/glances_config.py index 6db373ea..2f5b1ae3 100644 --- a/glances/core/glances_config.py +++ b/glances/core/glances_config.py @@ -38,7 +38,8 @@ from glances.core.glances_globals import ( is_py3, is_windows, sys_prefix, - work_path + work_path, + logger ) @@ -52,9 +53,12 @@ class Config(object): def __init__(self, location=None): self.location = location + self.config_filename = 'glances.conf' self.parser = RawConfigParser() + + self._loaded_config_file = None self.load() def load(self): @@ -66,12 +70,18 @@ class Config(object): self.parser.read(config_file, encoding='utf-8') else: self.parser.read(config_file) - # print(_("DEBUG: Read configuration file %s") % config_file) + logger.info(_("Read configuration file %s") % config_file) except UnicodeDecodeError as e: - print(_("Error: Cannot decode configuration file '{0}': {1}").format(config_file, e)) + logger.error(_("Cannot decode configuration file '{0}': {1}").format(config_file, e)) sys.exit(1) + # Save the loaded configuration file path (issue #374) + self._loaded_config_file = config_file break + def get_loaded_config_file(self): + """Return the loaded configuration file""" + return self._loaded_config_file + def get_config_paths(self): r"""Get a list of config file paths. diff --git a/glances/core/glances_globals.py b/glances/core/glances_globals.py index 3601adb9..6a9ecda6 100644 --- a/glances/core/glances_globals.py +++ b/glances/core/glances_globals.py @@ -22,6 +22,8 @@ import os import sys +from glances.core.glances_logging import glancesLogger + # Global information appname = 'glances' version = __import__('glances').__version__ @@ -58,10 +60,13 @@ elif os.path.exists(sys_i18n_path): else: locale_dir = None +# Create and init the logging instance +logger = glancesLogger() + # Instances shared between all Glances scripts # ============================================ -# glances_processes for processcount and processlist plugins +# Glances_processes for processcount and processlist plugins from glances.core.glances_processes import GlancesProcesses glances_processes = GlancesProcesses() diff --git a/glances/core/glances_logging.py b/glances/core/glances_logging.py new file mode 100644 index 00000000..86dae964 --- /dev/null +++ b/glances/core/glances_logging.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2014 Nicolargo +# +# 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 . + +"""Custom logging class""" + +import logging +import logging.config +import tempfile +import os + +# Define the logging configuration +LOGGING_CFG = { + 'version': 1, + 'disable_existing_loggers': False, + 'root': { + 'level': 'INFO', + 'handlers': ['file', 'console'] + }, + 'formatters': { + 'standard': { + 'format': '%(asctime)s -- %(levelname)s -- %(message)s' + }, + 'short': { + 'format': '%(levelname)s: %(message)s' + } + }, + 'handlers': { + 'file': { + 'level':'DEBUG', + 'class':'logging.handlers.RotatingFileHandler', + 'formatter': 'standard', + # http://stackoverflow.com/questions/847850/cross-platform-way-of-getting-temp-directory-in-python + 'filename': os.path.join(tempfile.gettempdir(), 'glances.log') + }, + 'console':{ + 'level':'CRITICAL', + 'class':'logging.StreamHandler', + 'formatter': 'short' + } + }, + 'loggers': { + 'debug': { + 'handlers':['file', 'console'], + 'level':'DEBUG', + }, + 'verbose': { + 'handlers': ['file', 'console'], + 'level': 'INFO' + }, + 'standard': { + 'handlers': ['file'], + 'level': 'INFO' + } + } +} + +def glancesLogger(): + _logger = logging.getLogger() + try: + logging.config.dictConfig(LOGGING_CFG) + except AttributeError: + # dictConfig is only available for Python 2.7 or higher + # Minimal configuration for Python 2.6 + logging.basicConfig(filename=os.path.join(tempfile.gettempdir(), 'glances.log'), + level=logging.DEBUG, + format='%(asctime)s -- %(levelname)s -- %(message)s') + return _logger diff --git a/glances/core/glances_logs.py b/glances/core/glances_logs.py index 393ef626..3be7d44d 100644 --- a/glances/core/glances_logs.py +++ b/glances/core/glances_logs.py @@ -87,8 +87,7 @@ class GlancesLogs(object): else: # Default sort is... process_auto_by = 'cpu_percent' - - glances_processes.setsortkey(process_auto_by) + glances_processes.setautosortkey(process_auto_by) return process_auto_by @@ -96,8 +95,8 @@ class GlancesLogs(object): """Reset the process_auto_by variable.""" # Default sort is... process_auto_by = 'cpu_percent' - - glances_processes.setsortkey(process_auto_by) + glances_processes.setautosortkey(process_auto_by) + glances_processes.setmanualsortkey(None) return process_auto_by diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py index 4d866b2d..0f780a03 100644 --- a/glances/core/glances_main.py +++ b/glances/core/glances_main.py @@ -21,10 +21,11 @@ # Import system libs import argparse +import sys # Import Glances libs from glances.core.glances_config import Config -from glances.core.glances_globals import appname, psutil_version, version +from glances.core.glances_globals import appname, psutil_version, version, logger class GlancesMain(object): @@ -55,15 +56,13 @@ class GlancesMain(object): _version = "Glances v" + version + " with psutil v" + psutil_version parser = argparse.ArgumentParser(prog=appname, conflict_handler='resolve') parser.add_argument('-V', '--version', action='version', version=_version) - parser.add_argument('-b', '--byte', action='store_true', default=False, - dest='byte', help=_('display network rate in byte per second')) - parser.add_argument('-B', '--bind', default='0.0.0.0', dest='bind_address', - help=_('bind server to the given IPv4/IPv6 address or hostname')) - parser.add_argument('-c', '--client', dest='client', - help=_('connect to a Glances server by IPv4/IPv6 address or hostname')) + parser.add_argument('-d', '--debug', action='store_true', default=False, + dest='debug', help=_('Enable debug mode')) parser.add_argument('-C', '--config', dest='conf_file', help=_('path to the configuration file')) # Enable or disable option on startup + parser.add_argument('--enable-history', action='store_true', default=False, + dest='enable_history', help=_('enable the history mode')) parser.add_argument('--disable-bold', action='store_false', default=True, dest='disable_bold', help=_('disable bold mode in the terminal')) parser.add_argument('--disable-diskio', action='store_true', default=False, @@ -76,20 +75,26 @@ class GlancesMain(object): dest='disable_sensors', help=_('disable sensors module')) parser.add_argument('--disable-process', action='store_true', default=False, dest='disable_process', help=_('disable process module')) + parser.add_argument('--disable-process-extended', action='store_true', default=False, + dest='disable_process_extended', help=_('disable extended stats on top process')) parser.add_argument('--disable-log', action='store_true', default=False, dest='disable_log', help=_('disable log module')) # CSV output feature parser.add_argument('--output-csv', default=None, dest='output_csv', help=_('export stats to a CSV file')) - # Server option - parser.add_argument('-p', '--port', default=self.server_port, type=int, dest='port', + # Client/Server option + parser.add_argument('-c', '--client', dest='client', + help=_('connect to a Glances server by IPv4/IPv6 address or hostname')) + parser.add_argument('-s', '--server', action='store_true', default=False, + dest='server', help=_('run Glances in server mode')) + parser.add_argument('-p', '--port', default=None, type=int, dest='port', help=_('define the client/server TCP port [default: {0}]').format(self.server_port)) + parser.add_argument('-B', '--bind', default='0.0.0.0', dest='bind_address', + help=_('bind server to the given IPv4/IPv6 address or hostname')) parser.add_argument('--password-badidea', dest='password_arg', help=_('define password from the command line')) parser.add_argument('--password', action='store_true', default=False, dest='password_prompt', help=_('define a client/server password from the prompt or file')) - parser.add_argument('-s', '--server', action='store_true', default=False, - dest='server', help=_('run Glances in server mode')) parser.add_argument('--snmp-community', default='public', dest='snmp_community', help=_('SNMP community')) parser.add_argument('--snmp-port', default=161, type=int, @@ -100,13 +105,23 @@ class GlancesMain(object): help=_('SNMP username (only for SNMPv3)')) parser.add_argument('--snmp-auth', default='password', dest='snmp_auth', help=_('SNMP authentication key (only for SNMPv3)')) - parser.add_argument('-t', '--time', default=self.refresh_time, type=int, + parser.add_argument('--snmp-force', action='store_true', default=False, + dest='snmp_force', help=_('force SNMP mode')) + parser.add_argument('-t', '--time', default=self.refresh_time, type=float, dest='time', help=_('set refresh time in seconds [default: {0} sec]').format(self.refresh_time)) parser.add_argument('-w', '--webserver', action='store_true', default=False, dest='webserver', help=_('run Glances in web server mode')) - # Other options + # Display options + parser.add_argument('-f', '--process-filter', default=None, type=str, + dest='process_filter', help=_('set the process filter patern (regular expression)')) + parser.add_argument('--process-short-name', action='store_true', default=False, + dest='process_short_name', help=_('force short name for processes name')) + parser.add_argument('-b', '--byte', action='store_true', default=False, + dest='byte', help=_('display network rate in byte per second')) parser.add_argument('-1', '--percpu', action='store_true', default=False, dest='percpu', help=_('start Glances in per CPU mode')) + parser.add_argument('--theme-white', action='store_true', default=False, + dest='theme_white', help=_('optimize display for white background')) return parser @@ -114,15 +129,24 @@ class GlancesMain(object): """Parse command line arguments.""" args = self.init_args().parse_args() - # Load the configuration file, it it exists + # Load the configuration file, if it exists self.config = Config(args.conf_file) - # In web server mode, default: - # - refresh time: 5 sec - # - host port: 61208 + # Debug mode + if args.debug: + from logging import DEBUG + logger.setLevel(DEBUG) + + # Client/server Port + if args.port is None: + if args.webserver: + args.port = self.web_server_port + else: + args.port = self.server_port + + # In web server mode, defaul refresh time: 5 sec if args.webserver: args.time = 5 - args.port = self.web_server_port # Server or client login/password args.username = self.username @@ -146,15 +170,6 @@ class GlancesMain(object): # Default is no password args.password = self.password - # !!! Change global variables regarding to user args - # !!! To be refactor to use directly the args list in the code - self.server_tag = args.server - self.webserver_tag = args.webserver - if args.client is not None: - self.client_tag = True - self.server_ip = args.client - # /!!! - # By default help is hidden args.help_tag = False @@ -162,6 +177,14 @@ class GlancesMain(object): args.network_sum = False args.network_cumul = False + # Control parameter and exit if it is not OK + self.args = args + + # Filter is only available in standalone mode + if args.process_filter is not None and not self.is_standalone(): + logger.critical(_("Process filter is only available in standalone mode")) + sys.exit(2) + return args def __hash_password(self, plain_password): @@ -186,19 +209,19 @@ class GlancesMain(object): def is_standalone(self): """Return True if Glances is running in standalone mode.""" - return not self.client_tag and not self.server_tag and not self.webserver_tag + return not self.args.client and not self.args.server and not self.args.webserver def is_client(self): """Return True if Glances is running in client mode.""" - return self.client_tag and not self.server_tag + return self.args.client and not self.args.server def is_server(self): """Return True if Glances is running in server mode.""" - return not self.client_tag and self.server_tag + return not self.args.client and self.args.server def is_webserver(self): """Return True if Glances is running in Web server mode.""" - return not self.client_tag and self.webserver_tag + return not self.args.client and self.args.webserver def get_config(self): """Return configuration file object.""" diff --git a/glances/core/glances_monitor_list.py b/glances/core/glances_monitor_list.py index 08fc4286..59b51937 100644 --- a/glances/core/glances_monitor_list.py +++ b/glances/core/glances_monitor_list.py @@ -24,7 +24,7 @@ import re import subprocess # Import Glances lib -from glances.core.glances_globals import glances_processes +from glances.core.glances_globals import glances_processes, logger class MonitorList(object): @@ -72,7 +72,7 @@ class MonitorList(object): countmin = self.config.get_raw_option(section, key + "countmin") countmax = self.config.get_raw_option(section, key + "countmax") except Exception as e: - print(_("Error: Cannot read monitored list: {0}").format(e)) + logger.error(_("Cannot read monitored list: {0}").format(e)) pass else: if description is not None and regex is not None: diff --git a/glances/core/glances_password.py b/glances/core/glances_password.py index b1a906be..c7cc6be2 100644 --- a/glances/core/glances_password.py +++ b/glances/core/glances_password.py @@ -32,7 +32,8 @@ from glances.core.glances_globals import ( is_bsd, is_linux, is_mac, - is_windows + is_windows, + logger ) # Trick: bind raw_input to input in Python 2 @@ -106,7 +107,7 @@ class GlancesPassword(object): """ if os.path.exists(self.password_filepath) and not clear: # If the password file exist then use it - print(_("Info: Read password from file: {0}").format(self.password_filepath)) + logger.info(_("Read password from file: {0}").format(self.password_filepath)) password = self.load_password() else: # Else enter the password from the command line @@ -122,7 +123,7 @@ class GlancesPassword(object): password_confirm = hashlib.sha256(getpass.getpass(_("Password (confirm): ")).encode('utf-8')).hexdigest() if not self.check_password(password_hashed, password_confirm): - print(_("Error: Sorry, but passwords did not match...")) + logger.critical(_("Sorry, but passwords did not match...")) sys.exit(1) # Return the plain or hashed password @@ -147,7 +148,7 @@ class GlancesPassword(object): try: os.makedirs(self.password_path) except OSError as e: - print(_("Warning: Cannot create Glances directory: {0}").format(e)) + logger.error(_("Cannot create Glances directory: {0}").format(e)) return # Create/overwrite the password file diff --git a/glances/core/glances_processes.py b/glances/core/glances_processes.py index 8913206d..3d75cbdd 100644 --- a/glances/core/glances_processes.py +++ b/glances/core/glances_processes.py @@ -17,10 +17,13 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from glances.core.glances_globals import is_bsd, is_mac, is_windows +# Import Glances lib +from glances.core.glances_globals import is_linux, is_bsd, is_mac, is_windows, logger from glances.core.glances_timer import Timer, getTimeSinceLastUpdate +# Import Python lib import psutil +import re class GlancesProcesses(object): @@ -44,7 +47,7 @@ class GlancesProcesses(object): self.io_old = {} # Init stats - self.processsort = 'cpu_percent' + self.resetsort() self.processlist = [] self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0} @@ -52,6 +55,20 @@ class GlancesProcesses(object): # Default is to enable the processes stats self.disable_tag = False + # Extended stats for top process is enable by default + self.disable_extended_tag = False + + # Maximum number of processes showed in the UI interface + # None if no limit + self.max_processes = None + + # Process filter is a regular expression + self.process_filter = None + self.process_filter_re = None + + # !!! ONLY FOR TEST + # self.set_process_filter('.*python.*') + def enable(self): """Enable process stats.""" self.disable_tag = False @@ -61,99 +78,231 @@ class GlancesProcesses(object): """Disable process stats.""" self.disable_tag = True - def __get_process_stats(self, proc): - """Get process stats.""" - procstat = {} + def enable_extended(self): + """Enable extended process stats.""" + self.disable_extended_tag = False + self.update() - # Process ID - procstat['pid'] = proc.pid + def disable_extended(self): + """Disable extended process stats.""" + self.disable_extended_tag = True - # Process name (cached by PSUtil) - try: - procstat['name'] = proc.name() - except psutil.AccessDenied: - procstat['name'] = "" + def set_max_processes(self, value): + """Set the maximum number of processes showed in the UI interfaces""" + self.max_processes = value + return self.max_processes - # Process username (cached with internal cache) - try: - self.username_cache[procstat['pid']] - except: + def get_max_processes(self): + """Get the maximum number of processes showed in the UI interfaces""" + return self.max_processes + + def set_process_filter(self, value): + """Set the process filter""" + logger.info(_("Set process filter to %s") % value) + self.process_filter = value + if value is not None: try: - self.username_cache[procstat['pid']] = proc.username() - except (KeyError, psutil.AccessDenied): + self.process_filter_re = re.compile(value) + logger.debug(_("Process filter regular expression compilation OK: %s") % self.get_process_filter()) + except: + logger.error(_("Can not compile process filter regular expression: %s") % value) + self.process_filter_re = None + else: + self.process_filter_re = None + return self.process_filter + + def get_process_filter(self): + """Get the process filter""" + return self.process_filter + + def get_process_filter_re(self): + """Get the process regular expression compiled""" + return self.process_filter_re + + def is_filtered(self, value): + """Return True if the value should be filtered""" + if self.get_process_filter() is None: + # No filter => Not filtered + return False + else: + # logger.debug(self.get_process_filter() + " <> " + value + " => " + str(self.get_process_filter_re().match(value) is None)) + return self.get_process_filter_re().match(value) is None + + def __get_process_stats(self, proc, + mandatory_stats=True, + standard_stats=True, + extended_stats=False): + """ + Get process stats of the proc processes (proc is returned psutil.process_iter()) + mandatory_stats: need for the sorting/filter step + => cpu_percent, memory_percent, io_counters, name, cmdline + standard_stats: for all the displayed processes + => username, status, memory_info, cpu_times + extended_stats: only for top processes (see issue #403) + => connections (UDP/TCP), memory_swap... + """ + + # Process ID (always) + procstat = proc.as_dict(attrs=['pid']) + + if mandatory_stats: + procstat['mandatory_stats'] = True + + # Process CPU, MEM percent and name + procstat.update(proc.as_dict(attrs=['cpu_percent', 'memory_percent', 'name'], ad_value='')) + + # Process command line (cached with internal cache) + try: + self.cmdline_cache[procstat['pid']] + except KeyError: + # Patch for issue #391 try: - self.username_cache[procstat['pid']] = proc.uids().real - except (KeyError, AttributeError, psutil.AccessDenied): - self.username_cache[procstat['pid']] = "?" - procstat['username'] = self.username_cache[procstat['pid']] + self.cmdline_cache[procstat['pid']] = ' '.join(proc.cmdline()) + except (AttributeError, psutil.AccessDenied, UnicodeDecodeError): + self.cmdline_cache[procstat['pid']] = "" + procstat['cmdline'] = self.cmdline_cache[procstat['pid']] - # Process command line (cached with internal cache) - try: - self.cmdline_cache[procstat['pid']] - except: - self.cmdline_cache[procstat['pid']] = ' '.join(proc.cmdline()) - procstat['cmdline'] = self.cmdline_cache[procstat['pid']] + # Process IO + # procstat['io_counters'] is a list: + # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] + # If io_tag = 0 > Access denied (display "?") + # If io_tag = 1 > No access denied (display the IO rate) + # Note Disk IO stat not available on Mac OS + if not is_mac: + try: + # Get the process IO counters + proc_io = proc.io_counters() + io_new = [proc_io.read_bytes, proc_io.write_bytes] + except (psutil.AccessDenied, psutil.NoSuchProcess): + # Access denied to process IO (no root account) + # NoSuchProcess (process die between first and second grab) + # Put 0 in all values (for sort) and io_tag = 0 (for display) + procstat['io_counters'] = [0, 0] + [0, 0] + io_tag = 0 + else: + # For IO rate computation + # Append saved IO r/w bytes + try: + procstat['io_counters'] = io_new + self.io_old[procstat['pid']] + except KeyError: + procstat['io_counters'] = io_new + [0, 0] + # then save the IO r/w bytes + self.io_old[procstat['pid']] = io_new + io_tag = 1 - # Process status - procstat['status'] = str(proc.status())[:1].upper() + # Append the IO tag (for display) + procstat['io_counters'] += [io_tag] - # Process nice - try: - procstat['nice'] = proc.nice() - except psutil.AccessDenied: - procstat['nice'] = None + if standard_stats: + procstat['standard_stats'] = True - # Process memory - procstat['memory_info'] = proc.memory_info() - procstat['memory_percent'] = proc.memory_percent() - - # Process CPU - procstat['cpu_times'] = proc.cpu_times() - procstat['cpu_percent'] = proc.cpu_percent(interval=0) - - # Process network connections (TCP and UDP) (Experimental) - # !!! High CPU consumption - # try: - # procstat['tcp'] = len(proc.connections(kind="tcp")) - # procstat['udp'] = len(proc.connections(kind="udp")) - # except: - # procstat['tcp'] = 0 - # procstat['udp'] = 0 - - # Process IO - # procstat['io_counters'] is a list: - # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] - # If io_tag = 0 > Access denied (display "?") - # If io_tag = 1 > No access denied (display the IO rate) - # Note Disk IO stat not available on Mac OS - if not is_mac: + # Process username (cached with internal cache) try: - # Get the process IO counters - proc_io = proc.io_counters() - io_new = [proc_io.read_bytes, proc_io.write_bytes] - except psutil.AccessDenied: - # Access denied to process IO (no root account) - # Put 0 in all values (for sort) and io_tag = 0 (for display) - procstat['io_counters'] = [0, 0] + [0, 0] - io_tag = 0 + self.username_cache[procstat['pid']] + except KeyError: + try: + self.username_cache[procstat['pid']] = proc.username() + except psutil.NoSuchProcess: + self.username_cache[procstat['pid']] = "?" + except (KeyError, psutil.AccessDenied): + try: + self.username_cache[procstat['pid']] = proc.uids().real + except (KeyError, AttributeError, psutil.AccessDenied): + self.username_cache[procstat['pid']] = "?" + procstat['username'] = self.username_cache[procstat['pid']] + + # Process status, nice, memory_info and cpu_times + try: + procstat.update(proc.as_dict(attrs=['status', 'nice', 'memory_info', 'cpu_times'])) + except psutil.NoSuchProcess: + pass else: - # For IO rate computation - # Append saved IO r/w bytes - try: - procstat['io_counters'] = io_new + self.io_old[procstat['pid']] - except KeyError: - procstat['io_counters'] = io_new + [0, 0] - # then save the IO r/w bytes - self.io_old[procstat['pid']] = io_new - io_tag = 1 + procstat['status'] = str(procstat['status'])[:1].upper() - # Append the IO tag (for display) - procstat['io_counters'] += [io_tag] + if extended_stats and not self.disable_extended_tag: + procstat['extended_stats'] = True + + # CPU affinity (Windows and Linux only) + try: + procstat.update(proc.as_dict(attrs=['cpu_affinity'])) + except psutil.NoSuchProcess: + pass + except AttributeError: + procstat['cpu_affinity'] = None + # Memory extended + try: + procstat.update(proc.as_dict(attrs=['memory_info_ex'])) + except psutil.NoSuchProcess: + pass + except AttributeError: + procstat['memory_info_ex'] = None + # Number of context switch + try: + procstat.update(proc.as_dict(attrs=['num_ctx_switches'])) + except psutil.NoSuchProcess: + pass + except AttributeError: + procstat['num_ctx_switches'] = None + # Number of file descriptors (Unix only) + try: + procstat.update(proc.as_dict(attrs=['num_fds'])) + except psutil.NoSuchProcess: + pass + except AttributeError: + procstat['num_fds'] = None + # Threads number + try: + procstat.update(proc.as_dict(attrs=['num_threads'])) + except psutil.NoSuchProcess: + pass + except AttributeError: + procstat['num_threads'] = None + + # Number of handles (Windows only) + if is_windows: + try: + procstat.update(proc.as_dict(attrs=['num_handles'])) + except psutil.NoSuchProcess: + pass + else: + procstat['num_handles'] = None + + # SWAP memory (Only on Linux based OS) + # http://www.cyberciti.biz/faq/linux-which-process-is-using-swap/ + if is_linux: + try: + procstat['memory_swap'] = sum([v.swap for v in proc.memory_maps()]) + except psutil.NoSuchProcess: + pass + except psutil.AccessDenied: + procstat['memory_swap'] = None + + # Process network connections (TCP and UDP) + try: + procstat['tcp'] = len(proc.connections(kind="tcp")) + procstat['udp'] = len(proc.connections(kind="udp")) + except: + procstat['tcp'] = None + procstat['udp'] = None + + # IO Nice + # http://pythonhosted.org/psutil/#psutil.Process.ionice + if is_linux or is_windows: + try: + procstat.update(proc.as_dict(attrs=['ionice'])) + except psutil.NoSuchProcess: + pass + else: + procstat['ionice'] = None + + #logger.debug(procstat) return procstat def update(self): - """Update the processes stats.""" + """ + Update the processes stats + """ # Reset the stats self.processlist = [] self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0} @@ -165,37 +314,71 @@ class GlancesProcesses(object): # Get the time since last update time_since_update = getTimeSinceLastUpdate('process_disk') - # For each existing process... + # Build an internal dict with only mandatories stats (sort keys) + processdict = {} for proc in psutil.process_iter(): + # If self.get_max_processes() is None: Only retreive mandatory stats + # Else: retreive mandatoryadn standard stast + s = self.__get_process_stats(proc, + mandatory_stats=True, + standard_stats=self.get_max_processes() is None) + # Continue to the next process if it has to be filtered + if s is None or (self.is_filtered(s['cmdline']) and self.is_filtered(s['name'])): + continue + # Ok add the process to the list + processdict[proc] = s + # ignore the 'idle' process on Windows and *BSD + # ignore the 'kernel_task' process on OS X + # waiting for upstream patch from psutil + if (is_bsd and processdict[proc]['name'] == 'idle' or + is_windows and processdict[proc]['name'] == 'System Idle Process' or + is_mac and processdict[proc]['name'] == 'kernel_task'): + continue + # Update processcount (global statistics) try: - # Get stats using the PSUtil - procstat = self.__get_process_stats(proc) + self.processcount[str(proc.status())] += 1 + except KeyError: + # Key did not exist, create it + self.processcount[str(proc.status())] = 1 + else: + self.processcount['total'] += 1 + # Update thread number (global statistics) + try: + self.processcount['thread'] += proc.num_threads() + except: + pass + + if self.get_max_processes() is not None: + # Sort the internal dict and cut the top N (Return a list of tuple) + # tuple=key (proc), dict (returned by __get_process_stats) + processiter = sorted(processdict.items(), key=lambda x: x[1][self.getsortkey()], reverse=True) + first = True + for i in processiter[0:self.get_max_processes()]: + # Already existing mandatory stats + procstat = i[1] + # Update with standard stats + # and extended stats but only for TOP (first) process + s = self.__get_process_stats(i[0], + mandatory_stats=False, + standard_stats=True, + extended_stats=first) + if s is None: + continue + procstat.update(s) # Add a specific time_since_update stats for bitrate procstat['time_since_update'] = time_since_update - # ignore the 'idle' process on Windows and *BSD - # ignore the 'kernel_task' process on OS X - # waiting for upstream patch from psutil - if (is_bsd and procstat['name'] == 'idle' or - is_windows and procstat['name'] == 'System Idle Process' or - is_mac and procstat['name'] == 'kernel_task'): - continue - # Update processcount (global statistics) - try: - self.processcount[str(proc.status())] += 1 - except KeyError: - # Key did not exist, create it - self.processcount[str(proc.status())] = 1 - else: - self.processcount['total'] += 1 - # Update thread number (global statistics) - try: - self.processcount['thread'] += proc.num_threads() - except: - pass - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - else: - # Update processlist + # Update process list + self.processlist.append(procstat) + # Next... + first = False + else: + # Get all the processes + for i in processdict.items(): + # Already existing mandatory and standard stats + procstat = i[1] + # Add a specific time_since_update stats for bitrate + procstat['time_since_update'] = time_since_update + # Update process list self.processlist.append(procstat) # Clean internals caches if timeout is reached @@ -214,13 +397,34 @@ class GlancesProcesses(object): return self.processlist def getsortkey(self): - """Get the current sort key for automatic sort.""" - return self.processsort + """Get the current sort key""" + if self.getmanualsortkey() is not None: + return self.getmanualsortkey() + else: + return self.getautosortkey() - def setsortkey(self, sortedby): + def getmanualsortkey(self): + """Get the current sort key for manual sort.""" + return self.processmanualsort + + def getautosortkey(self): + """Get the current sort key for automatic sort.""" + return self.processautosort + + def setmanualsortkey(self, sortedby): + """Set the current sort key for manual sort.""" + self.processmanualsort = sortedby + return self.processmanualsort + + def setautosortkey(self, sortedby): """Set the current sort key for automatic sort.""" - self.processsort = sortedby - return self.processsort + self.processautosort = sortedby + return self.processautosort + + def resetsort(self): + """Set the default sort: Auto""" + self.setmanualsortkey(None) + self.setautosortkey('cpu_percent') def getsortlist(self, sortedby=None): """Get the sorted processlist.""" diff --git a/glances/core/glances_server.py b/glances/core/glances_server.py index 8b7bf349..5f02a198 100644 --- a/glances/core/glances_server.py +++ b/glances/core/glances_server.py @@ -32,7 +32,7 @@ except ImportError: # Python 2 from SimpleXMLRPCServer import SimpleXMLRPCServer # Import Glances libs -from glances.core.glances_globals import version +from glances.core.glances_globals import version, logger from glances.core.glances_stats import GlancesStatsServer from glances.core.glances_timer import Timer @@ -113,7 +113,7 @@ class GlancesXMLRPCServer(SimpleXMLRPCServer): try: self.address_family = socket.getaddrinfo(bind_address, bind_port)[0][0] except socket.error as e: - print(_("Error: Couldn't open socket: {0}").format(e)) + logger.error(_("Couldn't open socket: {0}").format(e)) sys.exit(1) SimpleXMLRPCServer.__init__(self, (bind_address, bind_port), @@ -170,7 +170,6 @@ class GlancesInstance(object): The goal is to dynamically generate the API get'Stats'() methods. """ - # print "DEBUG: Call method: %s" % item header = 'get' # Check if the attribute starts with 'get' if item.startswith(header): @@ -200,7 +199,7 @@ class GlancesServer(object): try: self.server = GlancesXMLRPCServer(args.bind_address, args.port, requestHandler) except Exception as e: - print(_("Error: Cannot start Glances server: {0}").format(e)) + logger.error(_("Cannot start Glances server: {0}").format(e)) sys.exit(2) # The users dict diff --git a/glances/core/glances_snmp.py b/glances/core/glances_snmp.py index f7b8692a..c01da41a 100644 --- a/glances/core/glances_snmp.py +++ b/glances/core/glances_snmp.py @@ -19,11 +19,15 @@ import sys +# Import Glances libs +from glances.core.glances_globals import logger + +# Import mandatory PySNMP lib try: from pysnmp.entity.rfc3413.oneliner import cmdgen except ImportError: - print("PySNMP library not found.") - print("Install using pip: # pip install pysnmp") + logger.critical(_("PySNMP library not found.")) + print(_("Install it using pip: # pip install pysnmp")) sys.exit(2) @@ -55,6 +59,9 @@ class GlancesSNMPClient(object): ret[name.prettyPrint()] = '' else: ret[name.prettyPrint()] = val.prettyPrint() + # In Python 3, prettyPrint() return 'b'linux'' instead of 'linux' + if ret[name.prettyPrint()].startswith('b\''): + ret[name.prettyPrint()] = ret[name.prettyPrint()][2:-1] return ret def get_by_oid(self, *oid): @@ -89,6 +96,9 @@ class GlancesSNMPClient(object): item[name.prettyPrint()] = '' else: item[name.prettyPrint()] = val.prettyPrint() + # In Python 3, prettyPrint() return 'b'linux'' instead of 'linux' + if item[name.prettyPrint()].startswith('b\''): + item[name.prettyPrint()] = item[name.prettyPrint()][2:-1] ret.append(item) return ret diff --git a/glances/core/glances_standalone.py b/glances/core/glances_standalone.py index 05e770ab..a6586f6b 100644 --- a/glances/core/glances_standalone.py +++ b/glances/core/glances_standalone.py @@ -20,8 +20,10 @@ """Manage the Glances standalone session.""" # Import Glances libs +from glances.core.glances_globals import logger from glances.core.glances_stats import GlancesStats from glances.outputs.glances_curses import GlancesCurses +from glances.core.glances_globals import glances_processes class GlancesStandalone(object): @@ -30,7 +32,28 @@ class GlancesStandalone(object): def __init__(self, config=None, args=None): # Init stats - self.stats = GlancesStats(config) + self.stats = GlancesStats(config=config, args=args) + + # If configured, set the maximum processes number to display + try: + max_processes = int(self.stats.get_plugin('processlist').get_conf_value('max_processes')) + logger.debug(_("Limit maximum displayed processes to %s") % max_processes) + except: + max_processes = None + logger.warning(_("Maximum displayed processes is not configured (high CPU consumption)")) + glances_processes.set_max_processes(max_processes) + + # If process extended stats is disabled by user + if args.disable_process_extended: + logger.info(_("Extended stats for top process is disabled")) + glances_processes.disable_extended() + else: + logger.debug(_("Extended stats for top process is enabled (default behavor)")) + glances_processes.enable_extended() + + # Manage optionnal process filter + if args.process_filter is not None: + glances_processes.set_process_filter(args.process_filter) # Initial system informations update self.stats.update() diff --git a/glances/core/glances_stats.py b/glances/core/glances_stats.py index 4f265e06..fbce4c68 100644 --- a/glances/core/glances_stats.py +++ b/glances/core/glances_stats.py @@ -22,20 +22,35 @@ import collections import os import sys +import re -from glances.core.glances_globals import plugins_path, sys_path +from glances.core.glances_globals import plugins_path, sys_path, 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'} class GlancesStats(object): """This class stores, updates and gives stats.""" - def __init__(self, config=None): + def __init__(self, config=None, args=None): # Init the plugin list dict self._plugins = collections.defaultdict(dict) + # Set the argument instance + self.args = args + + # Set the config instance + self.config = config + # Load the plugins - self.load_plugins() + self.load_plugins(args=args) # Load the limits self.load_limits(config) @@ -76,9 +91,14 @@ class GlancesStats(object): # for example, the file glances_xxx.py # generate self._plugins_list["xxx"] = ... plugin_name = os.path.basename(item)[len(header):-3].lower() - self._plugins[plugin_name] = plugin.Plugin(args=args) + if plugin_name == 'help': + self._plugins[plugin_name] = plugin.Plugin(args=args, config=self.config) + else: + self._plugins[plugin_name] = plugin.Plugin(args=args) # Restoring system path sys.path = sys_path + # Log plugins list + logger.debug(_("Available plugins list: %s"), self.getAllPlugins()) def getAllPlugins(self): """Return the plugins list.""" @@ -88,7 +108,7 @@ class GlancesStats(object): """Load the stats limits.""" # For each plugins, call the init_limits method for p in self._plugins: - # print "DEBUG: Load limits for %s" % p + # logger.debug(_("Load limits for %s") % p) self._plugins[p].load_limits(config) def __update__(self, input_stats): @@ -96,8 +116,8 @@ class GlancesStats(object): if input_stats == {}: # For standalone and server modes # For each plugins, call the update method - for p in self._plugins: - # print "DEBUG: Update %s stats" % p + for p in self._plugins: + # logger.debug(_("Update %s stats") % p) self._plugins[p].update() else: # For Glances client mode @@ -110,9 +130,18 @@ class GlancesStats(object): self.__update__(input_stats) def getAll(self): - """Return all the stats.""" + """Return all the stats (list)""" return [self._plugins[p].get_raw() for p in self._plugins] + def getAllAsDict(self): + """Return all the stats (dict)""" + # Python > 2.6 + # {p: self._plugins[p].get_raw() for p in self._plugins} + ret = {} + for p in self._plugins: + ret[p] = self._plugins[p].get_raw() + return ret + def get_plugin_list(self): """Return the plugin list.""" self._plugins @@ -146,9 +175,18 @@ class GlancesStatsServer(GlancesStats): self.all_stats[p] = self._plugins[p].get_raw() def getAll(self): - """Return the stats as a dict.""" + """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 + def getAllPlugins(self): """Return the plugins list.""" return [p for p in self._plugins] @@ -176,8 +214,8 @@ class GlancesStatsClient(GlancesStats): # Add the plugin to the dictionary # The key is the plugin name # for example, the file glances_xxx.py - # generate self._plugins_list["xxx"] = ... - # print "DEBUG: Init %s plugin" % item + # generate self._plugins_list["xxx"] = ... + logger.debug(_("Init %s plugin") % item) self._plugins[item] = plugin.Plugin() # Restoring system path sys.path = sys_path @@ -197,6 +235,9 @@ class GlancesStatsClientSNMP(GlancesStats): # Init the arguments self.args = args + # OS name is used because OID is differents between system + self.os_name = None + # Load plugins self.load_plugins(args=self.args) @@ -213,17 +254,48 @@ class GlancesStatsClientSNMP(GlancesStats): user=self.args.snmp_user, auth=self.args.snmp_auth) - return clientsnmp.get_by_oid("1.3.6.1.2.1.1.5.0") != {} + # If we can not 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(_('Can not 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 + try: + iteritems = oid_to_short_system_name.iteritems() + except AttributeError: + # Correct issue #386 + iteritems = oid_to_short_system_name.items() + for r,v in iteritems: + 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].set_input('snmp') - # print "DEBUG: Update %s stats using SNMP request" % p + self._plugins[p].set_input('snmp', self.system_name) try: self._plugins[p].update() except Exception as e: - print(_("Error: Update {0} failed: {1}").format(p, e)) - # pass + logger.error(_("Error: Update {0} failed: {1}").format(p, e)) diff --git a/glances/outputs/glances_bottle.py b/glances/outputs/glances_bottle.py index fb3c7570..da95af15 100644 --- a/glances/outputs/glances_bottle.py +++ b/glances/outputs/glances_bottle.py @@ -22,11 +22,17 @@ import os import sys +# Import Glances libs +from glances.core.glances_globals import logger + +# Import mandatory Bottle lib try: - from bottle import Bottle, template, static_file, TEMPLATE_PATH + from bottle import Bottle, template, static_file, TEMPLATE_PATH, abort, response except ImportError: - print('Bottle module not found. Glances cannot start in web server mode.') - sys.exit(1) + logger.critical('Bottle module not found. Glances cannot start in web server mode.') + print(_("Install it using pip: # pip install bottle")) + sys.exit(2) +import json class GlancesBottle(object): @@ -58,6 +64,7 @@ class GlancesBottle(object): 'BOLD': 'bold', 'SORT': 'sort', 'OK': 'ok', + 'FILTER': 'filter', 'TITLE': 'title', 'CAREFUL': 'careful', 'WARNING': 'warning', @@ -77,13 +84,27 @@ class GlancesBottle(object): self._app.route('/', method=["GET", "POST"], callback=self._index) self._app.route('/', method="GET", callback=self._css) self._app.route('/', method="GET", callback=self._js) + # REST API + self._app.route('/api/2/pluginslist', method="GET", callback=self._api_plugins) + self._app.route('/api/2/all', method="GET", callback=self._api_all) + self._app.route('/api/2/:plugin', method="GET", callback=self._api) + self._app.route('/api/2/:plugin/limits', method="GET", callback=self._api_limits) + self._app.route('/api/2/:plugin/:item', method="GET", callback=self._api_item) + self._app.route('/api/2/:plugin/:item/:value', method="GET", callback=self._api_value) def start(self, stats): """Start the bottle.""" # Init stats self.stats = stats - self._app.run(host=self.args.bind_address, port=self.args.port) + # Init plugin list + self.plugins_list = self.stats.getAllPlugins() + + # Bind the Bottle TCP address/port + bindmsg = _("Glances web server started on http://{0}:{1}/").format(self.args.bind_address, self.args.port) + logger.info(bindmsg) + print(bindmsg) + self._app.run(host=self.args.bind_address, port=self.args.port, quiet=not self.args.debug) def end(self): """End the bottle.""" @@ -91,6 +112,7 @@ class GlancesBottle(object): def _index(self, refresh_time=None): """Bottle callback for index.html (/) file.""" + response.content_type = 'text/html' # Manage parameter if refresh_time is None: refresh_time = self.args.time @@ -103,14 +125,146 @@ class GlancesBottle(object): def _css(self, filename): """Bottle callback for *.css files.""" + response.content_type = 'text/html' # Return the static file return static_file(filename, root=os.path.join(self.STATIC_PATH, 'css')) def _js(self, filename): """Bottle callback for *.js files.""" + response.content_type = 'text/html' # Return the static file return static_file(filename, root=os.path.join(self.STATIC_PATH, 'js')) + def _api_plugins(self): + """ + Glances API RESTFul implementation + Return the plugin list + or 404 error + """ + response.content_type = 'application/json' + + # Update the stat + self.stats.update() + + try: + plist = json.dumps(self.plugins_list) + except Exception as e: + abort(404, "Can not get plugin list (%s)" % str(e)) + return plist + + def _api_all(self): + """ + Glances API RESTFul implementation + Return the JSON representation of all the plugins + HTTP/200 if OK + HTTP/400 if plugin is not found + HTTP/404 if others error + """ + response.content_type = 'application/json' + + # Update the stat + self.stats.update() + + try: + # Get the JSON value of the stat ID + statval = json.dumps(self.stats.getAllAsDict()) + except Exception as e: + abort(404, "Can not get stats (%s)" % str(e)) + return statval + + def _api(self, plugin): + """ + Glances API RESTFul implementation + Return the JSON representation of a given plugin + HTTP/200 if OK + 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() + + try: + # Get the JSON value of the stat ID + statval = self.stats.get_plugin(plugin).get_stats() + except Exception as e: + abort(404, "Can not get plugin %s (%s)" % (plugin, str(e))) + return statval + + def _api_limits(self, plugin): + """ + Glances API RESTFul implementation + Return the JSON limits of a given plugin + HTTP/200 if OK + 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() + + try: + # Get the JSON value of the stat ID + limits = self.stats.get_plugin(plugin).get_limits() + except Exception as e: + abort(404, "Can not get limits for plugin %s (%s)" % (plugin, str(e))) + return limits + + def _api_item(self, plugin, item): + """ + Glances API RESTFul implementation + Return the JSON represenation of the couple plugin/item + HTTP/200 if OK + 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() + + plist = self.stats.get_plugin(plugin).get_stats_item(item) + + if plist is None: + abort(404, "Can not get item %s in plugin %s" % (item, plugin)) + else: + return plist + + def _api_value(self, plugin, item, value): + """ + Glances API RESTFul implementation + Return the process stats (dict) for the given item=value + HTTP/200 if OK + 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, "Can not get item(%s)=value(%s) in plugin %s" % (item, value, plugin)) + else: + return pdict + def display(self, stats, refresh_time=None): """Display stats on the web page. diff --git a/glances/outputs/glances_colorconsole.py b/glances/outputs/glances_colorconsole.py index f61f66a2..7a9c8018 100644 --- a/glances/outputs/glances_colorconsole.py +++ b/glances/outputs/glances_colorconsole.py @@ -21,12 +21,14 @@ import sys import threading import time +from glances.core.glances_globals import logger + import msvcrt try: import colorconsole import colorconsole.terminal except ImportError: - print('Colorconsole module not found. Glances cannot start in standalone mode.') + logger.critical(_('Colorconsole module not found. Glances cannot start in standalone mode.')) sys.exit(1) try: diff --git a/glances/outputs/glances_csv.py b/glances/outputs/glances_csv.py index ff378fb4..02ea9aa4 100644 --- a/glances/outputs/glances_csv.py +++ b/glances/outputs/glances_csv.py @@ -20,10 +20,11 @@ """CSV interface class.""" # Import sys libs -import csv import sys +import csv -# Import Glances libs +# Import Glances lib +from glances.core.glances_globals import logger from glances.core.glances_globals import is_py3 # List of stats enabled in the CSV output @@ -46,10 +47,10 @@ class GlancesCSV(object): self.csv_file = open(self.csv_filename, 'wb') self.writer = csv.writer(self.csv_file) except IOError as e: - print(_("Error: Cannot create the CSV file: {0}").format(e)) + logger.critical(_("Error: Cannot create the CSV file: {0}").format(e)) sys.exit(2) - print(_("Stats dumped to CSV file: {0}").format(self.csv_filename)) + logger.info(_("Stats dumped to CSV file: {0}").format(self.csv_filename)) def exit(self): """Close the CSV file.""" diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 253e2eb2..9c592964 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -23,7 +23,7 @@ import sys # Import Glances lib -from glances.core.glances_globals import glances_logs, glances_processes, is_windows +from glances.core.glances_globals import glances_logs, glances_processes, is_linux, is_bsd, is_mac, is_windows, logger from glances.core.glances_timer import Timer # Import curses lib for "normal" operating system and consolelog for Windows @@ -31,8 +31,9 @@ if not is_windows: try: import curses import curses.panel + from curses.textpad import Textbox, rectangle except ImportError: - print('Curses module not found. Glances cannot start in standalone mode.') + logger.critical('Curses module not found. Glances cannot start in standalone mode.') sys.exit(1) else: from glances.outputs.glances_colorconsole import WCurseLight @@ -58,7 +59,7 @@ class GlancesCurses(object): # Init the curses screen self.screen = curses.initscr() if not self.screen: - print(_("Error: Cannot init the curses library.\n")) + logger.critical(_("Error: Cannot init the curses library.\n")) sys.exit(1) # Set curses options @@ -70,18 +71,17 @@ class GlancesCurses(object): curses.noecho() if hasattr(curses, 'cbreak'): curses.cbreak() - if hasattr(curses, 'curs_set'): - try: - curses.curs_set(0) - except Exception: - pass + self.set_cursor(0) # Init colors self.hascolors = False if curses.has_colors() and curses.COLOR_PAIRS > 8: self.hascolors = True # FG color, BG color - curses.init_pair(1, curses.COLOR_WHITE, -1) + if args.theme_white: + curses.init_pair(1, curses.COLOR_BLACK, -1) + else: + curses.init_pair(1, curses.COLOR_WHITE, -1) curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED) curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN) curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) @@ -89,7 +89,21 @@ class GlancesCurses(object): curses.init_pair(6, curses.COLOR_RED, -1) curses.init_pair(7, curses.COLOR_GREEN, -1) curses.init_pair(8, curses.COLOR_BLUE, -1) - curses.init_pair(9, curses.COLOR_MAGENTA, -1) + try: + curses.init_pair(9, curses.COLOR_MAGENTA, -1) + except: + if args.theme_white: + curses.init_pair(9, curses.COLOR_BLACK, -1) + else: + curses.init_pair(9, curses.COLOR_WHITE, -1) + try: + curses.init_pair(10, curses.COLOR_CYAN, -1) + except: + if args.theme_white: + curses.init_pair(10, curses.COLOR_BLACK, -1) + else: + curses.init_pair(10, curses.COLOR_WHITE, -1) + else: self.hascolors = False @@ -113,6 +127,7 @@ class GlancesCurses(object): self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD + self.filter_color = curses.color_pair(10) | A_BOLD else: # B&W text styles self.no_color = curses.A_NORMAL @@ -125,6 +140,7 @@ class GlancesCurses(object): self.ifCAREFUL_color2 = curses.A_UNDERLINE self.ifWARNING_color2 = A_BOLD self.ifCRITICAL_color2 = curses.A_REVERSE + self.filter_color = A_BOLD # Define the colors list (hash table) for stats self.__colors_list = { @@ -133,6 +149,7 @@ class GlancesCurses(object): 'BOLD': A_BOLD, 'SORT': A_BOLD, 'OK': self.default_color2, + 'FILTER': self.filter_color, 'TITLE': self.title_color, 'PROCESS': self.default_color2, 'STATUS': self.default_color2, @@ -155,11 +172,37 @@ class GlancesCurses(object): # Init process sort method self.args.process_sorted_by = 'auto' + # Init edit filter tag + self.edit_filter = False + # Catch key pressed with non blocking mode self.term_window.keypad(1) self.term_window.nodelay(1) self.pressedkey = -1 + # History tag + self.reset_history_tag = False + self.history_tag = False + if args.enable_history: + logger.info('Stats history enabled') + from glances.outputs.glances_history import GlancesHistory + self.glances_history = GlancesHistory() + if not self.glances_history.graph_enabled(): + args.enable_history = False + logger.error('Stats history disabled because graph lib is not available') + + def set_cursor(self, value): + """Configure the cursor + 0: invisible + 1: visible + 2: very visible + """ + if hasattr(curses, 'curs_set'): + try: + curses.curs_set(value) + except Exception: + pass + def __get_key(self, window): # Catch ESC key AND numlock key (issue #163) keycode = [0, 0] @@ -181,13 +224,21 @@ class GlancesCurses(object): if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'): # 'ESC'|'q' > Quit self.end() + logger.info("Stop Glances") sys.exit(0) + elif self.pressedkey == 10: + # 'ENTER' > Edit the process filter + self.edit_filter = not self.edit_filter elif self.pressedkey == ord('1'): # '1' > Switch between CPU and PerCPU information self.args.percpu = not self.args.percpu + elif self.pressedkey == ord('/'): + # '/' > Switch between short/long name for processes + self.args.process_short_name = not self.args.process_short_name elif self.pressedkey == ord('a'): # 'a' > Sort processes automatically self.args.process_sorted_by = 'auto' + glances_processes.resetsort() elif self.pressedkey == ord('b'): # 'b' > Switch between bit/s and Byte/s for network IO # self.net_byteps_tag = not self.net_byteps_tag @@ -195,30 +246,47 @@ class GlancesCurses(object): elif self.pressedkey == ord('c'): # 'c' > Sort processes by CPU usage self.args.process_sorted_by = 'cpu_percent' + glances_processes.setmanualsortkey(self.args.process_sorted_by) elif self.pressedkey == ord('d'): # 'd' > Show/hide disk I/O stats self.args.disable_diskio = not self.args.disable_diskio + elif self.pressedkey == ord('e'): + # 'e' > Enable/Disable extended stats for top process + self.args.disable_process_extended = not self.args.disable_process_extended + if self.args.disable_process_extended: + glances_processes.disable_extended() + else: + glances_processes.enable_extended() elif self.pressedkey == ord('f'): # 'f' > Show/hide fs stats self.args.disable_fs = not self.args.disable_fs + elif self.pressedkey == ord('g'): + # 'g' > History + self.history_tag = not self.history_tag elif self.pressedkey == ord('h'): # 'h' > Show/hide help self.args.help_tag = not self.args.help_tag elif self.pressedkey == ord('i'): # 'i' > Sort processes by IO rate (not available on OS X) self.args.process_sorted_by = 'io_counters' + glances_processes.setmanualsortkey(self.args.process_sorted_by) elif self.pressedkey == ord('l'): # 'l' > Show/hide log messages self.args.disable_log = not self.args.disable_log elif self.pressedkey == ord('m'): # 'm' > Sort processes by MEM usage self.args.process_sorted_by = 'memory_percent' + glances_processes.setmanualsortkey(self.args.process_sorted_by) elif self.pressedkey == ord('n'): # 'n' > Show/hide network stats self.args.disable_network = not self.args.disable_network elif self.pressedkey == ord('p'): # 'p' > Sort processes by name self.args.process_sorted_by = 'name' + glances_processes.setmanualsortkey(self.args.process_sorted_by) + elif self.pressedkey == ord('r'): + # 'r' > Reset history + self.reset_history_tag = not self.reset_history_tag elif self.pressedkey == ord('s'): # 's' > Show/hide sensors stats (Linux-only) self.args.disable_sensors = not self.args.disable_sensors @@ -247,7 +315,7 @@ class GlancesCurses(object): return self.pressedkey def end(self): - """Shutdown the curses window.""" + """Shutdown the curses window.""" if hasattr(curses, 'echo'): curses.echo() if hasattr(curses, 'nocbreak'): @@ -258,6 +326,31 @@ class GlancesCurses(object): except Exception: pass curses.endwin() + + def init_line_column(self): + """Init the line and column position for the curses inteface""" + self.line = 0 + self.column = 0 + self.next_line = 0 + self.next_column = 0 + + def init_line(self): + """Init the line position for the curses inteface""" + self.line = 0 + self.next_line = 0 + + def init_column(self): + """Init the column position for the curses inteface""" + self.column = 0 + self.next_column = 0 + + def new_line(self): + """New line in the curses interface""" + self.line = self.next_line + + def new_column(self): + """New column in the curses interface""" + self.column = self.next_column def display(self, stats, cs_status="None"): """Display stats on the screen. @@ -273,96 +366,241 @@ class GlancesCurses(object): True if the stats have been displayed False if the help have been displayed """ - # Init the internal line/column dict for Glances Curses - self.line_to_y = {} - self.column_to_x = {} + # Init the internal line/column for Glances Curses + self.init_line_column() # Get the screen size screen_x = self.screen.getmaxyx()[1] screen_y = self.screen.getmaxyx()[0] + # No processes list in SNMP mode + if cs_status == 'SNMP': + # so... more space for others plugins + plugin_max_width = 43 + else: + plugin_max_width = None + + # Update the stats messages + ########################### + + # Update the client server status + self.args.cs_status = cs_status + stats_system = stats.get_plugin('system').get_stats_display(args=self.args) + stats_uptime = stats.get_plugin('uptime').get_stats_display() + if self.args.percpu: + stats_percpu = stats.get_plugin('percpu').get_stats_display() + else: + stats_cpu = stats.get_plugin('cpu').get_stats_display() + stats_load = stats.get_plugin('load').get_stats_display() + stats_mem = stats.get_plugin('mem').get_stats_display() + stats_memswap = stats.get_plugin('memswap').get_stats_display() + stats_network = stats.get_plugin('network').get_stats_display(args=self.args, max_width=plugin_max_width) + stats_diskio = stats.get_plugin('diskio').get_stats_display(args=self.args) + stats_fs = stats.get_plugin('fs').get_stats_display(args=self.args, max_width=plugin_max_width) + stats_sensors = stats.get_plugin('sensors').get_stats_display(args=self.args) + stats_now = stats.get_plugin('now').get_stats_display() + stats_processcount = stats.get_plugin('processcount').get_stats_display(args=self.args) + stats_processlist = stats.get_plugin('processlist').get_stats_display(args=self.args) + stats_monitor = stats.get_plugin('monitor').get_stats_display(args=self.args) + stats_alert = stats.get_plugin('alert').get_stats_display(args=self.args) + + # Display the stats on the curses interface + ########################################### + + # Help screen (on top of the other stats) if self.args.help_tag: # Display the stats... self.display_plugin(stats.get_plugin('help').get_stats_display(args=self.args)) # ... and exit return False - # Update the client server status - self.args.cs_status = cs_status - # Display first line (system+uptime) - stats_system = stats.get_plugin('system').get_stats_display(args=self.args) - stats_uptime = stats.get_plugin('uptime').get_stats_display() + self.new_line() l = self.get_stats_display_width(stats_system) + self.get_stats_display_width(stats_uptime) + self.space_between_column self.display_plugin(stats_system, display_optional=(screen_x >= l)) + self.new_column() self.display_plugin(stats_uptime) # Display second line (CPU|PERCPU+LOAD+MEM+SWAP+) # CPU|PERCPU + self.init_column() + self.new_line() if self.args.percpu: - stats_percpu = stats.get_plugin('percpu').get_stats_display() l = self.get_stats_display_width(stats_percpu) else: - stats_cpu = stats.get_plugin('cpu').get_stats_display() l = self.get_stats_display_width(stats_cpu) - stats_load = stats.get_plugin('load').get_stats_display() - stats_mem = stats.get_plugin('mem').get_stats_display() - stats_memswap = stats.get_plugin('memswap').get_stats_display() l += self.get_stats_display_width(stats_load) + self.get_stats_display_width(stats_mem) + self.get_stats_display_width(stats_memswap) # Space between column - if screen_x > (3 * self.space_between_column + l): - self.space_between_column = int((screen_x - l) / 3) + space_number = int(stats_load['msgdict'] != []) + int(stats_mem['msgdict'] != []) + int(stats_memswap['msgdict'] != []) + if space_number == 0: + space_number = 1 + if screen_x > (space_number * self.space_between_column + l): + self.space_between_column = int((screen_x - l) / space_number) # Display if self.args.percpu: self.display_plugin(stats_percpu) else: self.display_plugin(stats_cpu, display_optional=(screen_x >= 80)) + self.new_column() self.display_plugin(stats_load) - self.display_plugin(stats_mem, display_optional=(screen_x >= (3 * self.space_between_column + l))) + self.new_column() + self.display_plugin(stats_mem, display_optional=(screen_x >= (space_number * self.space_between_column + l))) + self.new_column() self.display_plugin(stats_memswap) # Space between column self.space_between_column = 3 - # Display left sidebar (NETWORK+DISKIO+FS+SENSORS) - self.display_plugin(stats.get_plugin('network').get_stats_display(args=self.args)) - self.display_plugin(stats.get_plugin('diskio').get_stats_display(args=self.args)) - self.display_plugin(stats.get_plugin('fs').get_stats_display(args=self.args)) - self.display_plugin(stats.get_plugin('sensors').get_stats_display(args=self.args)) - # Display last line (currenttime) - self.display_plugin(stats.get_plugin('now').get_stats_display()) + # Backup line position + self.saved_line = self.next_line - # Display right sidebar (PROCESS_COUNT+MONITORED+PROCESS_LIST+ALERT) + # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time) + self.init_column() + self.new_line() + self.display_plugin(stats_network) + self.new_line() + self.display_plugin(stats_diskio) + self.new_line() + self.display_plugin(stats_fs) + self.new_line() + self.display_plugin(stats_sensors) + self.new_line() + self.display_plugin(stats_now) + + # If space available... if screen_x > 52: - stats_processcount = stats.get_plugin('processcount').get_stats_display(args=self.args) - stats_processlist = stats.get_plugin('processlist').get_stats_display(args=self.args) - stats_alert = stats.get_plugin('alert').get_stats_display(args=self.args) - stats_monitor = stats.get_plugin('monitor').get_stats_display(args=self.args) - # Display + # Restore line position + self.next_line = self.saved_line + + # Display right sidebar (PROCESS_COUNT+MONITORED+PROCESS_LIST+ALERT) + self.new_column() + self.new_line() self.display_plugin(stats_processcount) - self.display_plugin(stats_monitor) + if glances_processes.get_process_filter() == None and cs_status == 'None': + # Do not display stats monitor list if a filter exist + self.new_line() + self.display_plugin(stats_monitor) + self.new_line() self.display_plugin(stats_processlist, display_optional=(screen_x > 102), + display_additional=(is_mac == False), max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2)) + self.new_line() self.display_plugin(stats_alert) + # History option + # Generate history graph + if self.history_tag and self.args.enable_history: + self.display_popup(_("Graphs history generated in %s") % self.glances_history.get_output_folder()) + self.glances_history.generate_graph(stats) + elif self.reset_history_tag and self.args.enable_history: + self.display_popup(_("Reset history")) + self.glances_history.reset(stats) + elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history: + self.display_popup(_("History disabled\nEnable it using --enable-history")) + self.history_tag = False + self.reset_history_tag = False + + # Display edit filter popup + # Only in standalone mode (cs_status == 'None') + if self.edit_filter and cs_status == 'None': + new_filter = self.display_popup(_("Process filter pattern: "), + is_input=True, + input_value=glances_processes.get_process_filter()) + glances_processes.set_process_filter(new_filter) + elif self.edit_filter and cs_status != 'None': + self.display_popup(_("Process filter only available in standalone mode")) + self.edit_filter = False + return True - def display_plugin(self, plugin_stats, display_optional=True, max_y=65535): + def display_popup(self, message, + size_x=None, size_y=None, + duration=3, + is_input=False, + input_size=30, + input_value=None): + """ + If is_input is False: + Display a centered popup with the given message during duration seconds + If size_x and size_y: set the popup size + else set it automatically + Return True if the popup could be displayed + If is_input is True: + Display a centered popup with the given message and a input field + If size_x and size_y: set the popup size + else set it automatically + Return the input string or None if the field is empty + """ + + # Center the popup + if size_x is None: + size_x = len(message) + 4 + # Add space for the input field + if is_input: + size_x += input_size + if size_y is None: + size_y = message.count('\n') + 1 + 4 + screen_x = self.screen.getmaxyx()[1] + screen_y = self.screen.getmaxyx()[0] + if size_x > screen_x or size_y > screen_y: + # No size to display the popup => abord + return False + pos_x = int((screen_x - size_x) / 2) + pos_y = int((screen_y - size_y) / 2) + + # Create the popup + popup = curses.newwin(size_y, size_x, pos_y, pos_x) + + # Fill the popup + popup.border() + + # Add the message + y = 0 + for m in message.split('\n'): + popup.addnstr(2 + y, 2, m, len(m)) + y += 1 + + if is_input and not is_windows: + # Create a subwindow for the text field + subpop = popup.derwin(1, input_size, 2, 2 + len(m)) + subpop.attron(self.__colors_list['FILTER']) + # Init the field with the current value + if input_value is not None: + subpop.addnstr(0, 0, input_value, len(input_value)) + # Display the popup + popup.refresh() + subpop.refresh() + # Create the textbox inside the subwindows + self.set_cursor(2) + textbox = glances_textbox(subpop, insert_mode=False) + textbox.edit() + self.set_cursor(0) + if textbox.gather() != '': + logger.debug(_("User enters the following process filter patern: %s") % textbox.gather()) + return textbox.gather()[:-1] + else: + logger.debug(_("User clears the process filter patern")) + return None + else: + # Display the popup + popup.refresh() + curses.napms(duration * 1000) + return True + + def display_plugin(self, plugin_stats, + display_optional=True, + display_additional=True, + max_y=65535): """Display the plugin_stats on the screen. - If display_optional=True display the optional stats. + If display_optional=True display the optional stats + If display_additional=True display additionnal stats max_y do not display line > max_y """ # Exit if: # - the plugin_stats message is empty # - the display tag = False if plugin_stats['msgdict'] == [] or not plugin_stats['display']: - # Display the next plugin at the current plugin position - try: - self.column_to_x[plugin_stats['column'] + 1] = self.column_to_x[plugin_stats['column']] - self.line_to_y[plugin_stats['line'] + 1] = self.line_to_y[plugin_stats['line']] - except Exception: - pass # Exit return 0 @@ -371,21 +609,17 @@ class GlancesCurses(object): screen_y = self.screen.getmaxyx()[0] # Set the upper/left position of the message - if plugin_stats['column'] < 0: + if plugin_stats['align'] == 'right': # Right align (last column) display_x = screen_x - self.get_stats_display_width(plugin_stats) else: - if plugin_stats['column'] not in self.column_to_x: - self.column_to_x[plugin_stats['column']] = plugin_stats['column'] - display_x = self.column_to_x[plugin_stats['column']] - if plugin_stats['line'] < 0: + display_x = self.column + if plugin_stats['align'] == 'bottom': # Bottom (last line) display_y = screen_y - self.get_stats_display_height(plugin_stats) else: - if plugin_stats['line'] not in self.line_to_y: - self.line_to_y[plugin_stats['line']] = plugin_stats['line'] - display_y = self.line_to_y[plugin_stats['line']] - + display_y = self.line + # Display x = display_x y = display_y @@ -407,13 +641,16 @@ class GlancesCurses(object): # If display_optional = False do not display optional stats if not display_optional and m['optional']: continue + # If display_additional = False do not display additional stats + if not display_additional and m['additional']: + continue # Is it possible to display the stat with the current screen size # !!! Crach if not try/except... Why ??? try: self.term_window.addnstr(y, x, m['msg'], screen_x - x, # Do not disply outside the screen - self.__colors_list[m['decoration']]) + self.__colors_list[m['decoration']]) except: pass else: @@ -421,10 +658,8 @@ class GlancesCurses(object): x = x + len(m['msg']) # Compute the next Glances column/line position - if plugin_stats['column'] > -1: - self.column_to_x[plugin_stats['column'] + 1] = x + self.space_between_column - if plugin_stats['line'] > -1: - self.line_to_y[plugin_stats['line'] + 1] = y + self.space_between_line + self.next_column = max(self.next_column, x + self.space_between_column) + self.next_line = max(self.next_line, y + self.space_between_line) def erase(self): """Erase the content of the screen.""" @@ -461,8 +696,8 @@ class GlancesCurses(object): while not countdown.finished(): # Getkey if self.__catch_key() > -1: - # flush display - self.flush(stats, cs_status=cs_status) + # Redraw display + self.flush(stats, cs_status=cs_status) # Wait 100ms... curses.napms(100) @@ -496,3 +731,17 @@ class GlancesCurses(object): return 0 else: return c + 1 + +if not is_windows: + class glances_textbox(Textbox): + """ + """ + def __init__(*args, **kwargs): + Textbox.__init__(*args, **kwargs) + + def do_command(self, ch): + if ch == 10: # Enter + return 0 + if ch == 127: # Enter + return 8 + return Textbox.do_command(self, ch) \ No newline at end of file diff --git a/glances/outputs/glances_history.py b/glances/outputs/glances_history.py new file mode 100644 index 00000000..91cc16a4 --- /dev/null +++ b/glances/outputs/glances_history.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2014 Nicolargo +# +# 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 . + +"""History class.""" + +# Import system lib +import os +import tempfile + +# Import Glances lib +from glances.core.glances_globals import logger + +# Import specific lib +try: + from matplotlib import __version__ as matplotlib_version + import matplotlib.pyplot as plt + import matplotlib.dates as dates +except: + matplotlib_check = False + logger.warning('Can not load Matplotlib library. Please install it using "pip install matplotlib"') +else: + matplotlib_check = True + logger.info('Load Matplotlib version %s' % matplotlib_version) + + +class GlancesHistory(object): + + """This class define the object to manage stats history""" + + def __init__(self, output_folder=tempfile.gettempdir()): + # !!! MINUS: matplotlib footprint (mem/cpu) => Fork process ? + # !!! MINUS: Mem used to store history + # !!! TODO: sampling before graph => Usefull ? + # !!! TODO: do not display first two point (glances is running) + # !!! TODO: replace /tmp by a cross platform way to get /tmp folder + self.output_folder = output_folder + + def get_output_folder(self): + """Return the output folder where the graph are generated""" + return self.output_folder + + def graph_enabled(self): + """Return True if Glances can generaate history graphs""" + return matplotlib_check + + def reset(self, stats): + """ + Reset all the history + """ + if not self.graph_enabled(): + return False + for p in stats.getAllPlugins(): + h = stats.get_plugin(p).get_stats_history() + if h is not None: + stats.get_plugin(p).reset_stats_history() + return True + + def generate_graph(self, stats): + """ + Generate graphs from plugins history + """ + if not self.graph_enabled(): + return False + + for p in stats.getAllPlugins(): + h = stats.get_plugin(p).get_stats_history() + if h is not None: + # Build the graph + # fig = plt.figure(dpi=72) + ax = plt.subplot(1, 1, 1) + + # Label + plt.title("%s stats" % p) + + handles = [] + for i in stats.get_plugin(p).get_items_history_list(): + handles.append(plt.Rectangle((0, 0), 1, 1, fc=i['color'], ec=i['color'], linewidth=1)) + labels = [i['name'] for i in stats.get_plugin(p).get_items_history_list()] + plt.legend(handles, labels, loc=1, prop={'size':9}) + formatter = dates.DateFormatter('%H:%M:%S') + ax.xaxis.set_major_formatter(formatter) + # ax.set_ylabel('%') + + # Draw the stats + for i in stats.get_plugin(p).get_items_history_list(): + ax.plot_date(h['date'], h[i['name']], + i['color'], + label='%s' % i['name'], + xdate=True, ydate=False) + + # Save and display + plt.savefig(os.path.join(self.output_folder, 'glances_%s.png' % p), dpi=72) + # plt.show() + + return True diff --git a/glances/outputs/static/css/style.css b/glances/outputs/static/css/style.css index 49e45f59..59980f2b 100644 --- a/glances/outputs/static/css/style.css +++ b/glances/outputs/static/css/style.css @@ -61,6 +61,9 @@ div#newline{ #ok { color: green; } +#filter { + color: cyan; +} #ok_log { background-color: green; color: white; diff --git a/glances/plugins/glances_alert.py b/glances/plugins/glances_alert.py index a334398c..25eff22a 100644 --- a/glances/plugins/glances_alert.py +++ b/glances/plugins/glances_alert.py @@ -20,6 +20,7 @@ """Alert plugin.""" # Import system lib +import types from datetime import datetime # Import Glances libs @@ -40,12 +41,9 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True + # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 1 - # Enter -1 to diplay bottom - self.line_curse = -1 + self.set_align('bottom') # Init the stats self.reset() @@ -107,7 +105,7 @@ class Plugin(GlancesPlugin): msg = str(alert[3]) ret.append(self.curse_add_line(msg, decoration=alert[2])) # Min / Mean / Max - if alert[6] == alert[4]: + if self.approx_equal(alert[6], alert[4], tolerance=0.1): msg = ' ({0:.1f})'.format(alert[5]) else: msg = _(" (Min:{0:.1f} Mean:{1:.1f} Max:{2:.1f})").format(alert[6], alert[5], alert[4]) @@ -122,3 +120,12 @@ class Plugin(GlancesPlugin): # ret.append(self.curse_add_line(msg)) return ret + + def approx_equal(self, a, b, tolerance=0.0): + """ + Compare a with b using the tolerance (if numerical) + """ + if str(int(a)).isdigit() and str(int(b)).isdigit(): + return abs(a-b) <= max(abs(a), abs(b)) * tolerance + else: + return a == b diff --git a/glances/plugins/glances_batpercent.py b/glances/plugins/glances_batpercent.py index d083e78d..edb269db 100644 --- a/glances/plugins/glances_batpercent.py +++ b/glances/plugins/glances_batpercent.py @@ -19,14 +19,15 @@ """Battery plugin.""" +# Import Glances libs +from glances.core.glances_globals import logger +from glances.plugins.glances_plugin import GlancesPlugin + # Batinfo library (optional; Linux-only) try: import batinfo except ImportError: - pass - -# Import Glances libs -from glances.plugins.glances_plugin import GlancesPlugin + logger.debug(_("Cannot grab battery sensor. Missing BatInfo library.")) class Plugin(GlancesPlugin): @@ -84,18 +85,12 @@ class GlancesGrabBat(object): self.bat_list = [] self.update() except Exception: - # print(_("Warning: Cannot grab battery sensor. Missing BatInfo library.")) self.initok = False def update(self): """Update the stats.""" if self.initok: - reply = self.bat.update() - if reply is not None: - self.bat_list = [] - new_item = {'label': _("Battery (%)"), - 'value': self.getcapacitypercent()} - self.bat_list.append(new_item) + self.bat_list = [{'label': _("Battery (%)"), 'value': self.getcapacitypercent()}] else: self.bat_list = [] @@ -108,15 +103,14 @@ class GlancesGrabBat(object): if not self.initok or self.bat.stat == []: return [] - # Init the bsum (sum of percent) and bcpt (number of batteries) + # Init the bsum (sum of percent) # and Loop over batteries (yes a computer could have more than 1 battery) bsum = 0 - for bcpt in range(len(self.bat.stat)): + for b in self.bat.stat: try: - bsum = bsum + int(self.bat.stat[bcpt].capacity) + bsum = bsum + int(b.capacity) except ValueError: return [] - bcpt = bcpt + 1 # Return the global percent - return int(bsum / bcpt) + return int(bsum / len(self.bat.stat)) diff --git a/glances/plugins/glances_cpu.py b/glances/plugins/glances_cpu.py index 479e6358..db4a7a43 100644 --- a/glances/plugins/glances_cpu.py +++ b/glances/plugins/glances_cpu.py @@ -27,13 +27,20 @@ import psutil # percentage of user CPU time: .1.3.6.1.4.1.2021.11.9.0 # percentages of system CPU time: .1.3.6.1.4.1.2021.11.10.0 # percentages of idle CPU time: .1.3.6.1.4.1.2021.11.11.0 -snmp_oid = {'user': '1.3.6.1.4.1.2021.11.9.0', - 'system': '1.3.6.1.4.1.2021.11.10.0', - 'idle': '1.3.6.1.4.1.2021.11.11.0'} +snmp_oid = {'default': {'user': '1.3.6.1.4.1.2021.11.9.0', + 'system': '1.3.6.1.4.1.2021.11.10.0', + 'idle': '1.3.6.1.4.1.2021.11.11.0'}, + 'windows': {'percent': '1.3.6.1.2.1.25.3.3.1.2'}, + 'esxi': {'percent': '1.3.6.1.2.1.25.3.3.1.2'}} + +# Define the history items list +# 'color' define the graph color in #RGB format +# All items in this list will be historised if the --enable-history tag is set +items_history_list = [{'name': 'user', 'color': '#00FF00'}, + {'name': 'system', 'color': '#FF0000'}] class Plugin(GlancesPlugin): - """ Glances' CPU plugin. @@ -42,16 +49,10 @@ class Plugin(GlancesPlugin): def __init__(self, args=None): """Init the CPU plugin.""" - GlancesPlugin.__init__(self, args=args) + GlancesPlugin.__init__(self, args=args, items_history_list=items_history_list) # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 1 # Init stats self.first_call = True @@ -91,14 +92,45 @@ class Plugin(GlancesPlugin): self.stats[cpu] = getattr(cputimespercent, cpu) elif self.get_input() == 'snmp': # Update stats using SNMP - self.stats = self.set_stats_snmp(snmp_oid=snmp_oid) - if self.stats['user'] == '': - self.reset() - return self.stats + if self.get_short_system_name() in ('windows', 'esxi'): + # Windows or VMWare ESXi + # You can find the CPU utilization of windows system by querying the oid + # Give also the number of core (number of element in the table) + try: + cpu_stats = self.set_stats_snmp(snmp_oid=snmp_oid[self.get_short_system_name()], + bulk=True) + except KeyError: + self.reset() - for key in self.stats.iterkeys(): - self.stats[key] = float(self.stats[key]) + # Iter through CPU and compute the idle CPU stats + self.stats['nb_log_core'] = 0 + self.stats['idle'] = 0 + for c in cpu_stats: + if c.startswith('percent'): + self.stats['idle'] += float(cpu_stats['percent.3']) + self.stats['nb_log_core'] += 1 + if self.stats['nb_log_core'] > 0: + self.stats['idle'] = self.stats['idle'] / self.stats['nb_log_core'] + self.stats['idle'] = 100 - self.stats['idle'] + + else: + # Default behavor + try: + self.stats = self.set_stats_snmp(snmp_oid=snmp_oid[self.get_short_system_name()]) + except KeyError: + self.stats = self.set_stats_snmp(snmp_oid=snmp_oid['default']) + + if self.stats['idle'] == '': + self.reset() + return self.stats + + # Convert SNMP stats to float + for key in list(self.stats.keys()): + self.stats[key] = float(self.stats[key]) + + # Update the history list + self.update_stats_history() return self.stats @@ -112,12 +144,17 @@ class Plugin(GlancesPlugin): return ret # Build the string message + # If user stat is not here, display only idle / total CPU usage (for exemple on Windows OS) + idle_tag = 'user' not in self.stats # Header msg = '{0:8}'.format(_("CPU")) ret.append(self.curse_add_line(msg, "TITLE")) # Total CPU usage msg = '{0:>6.1%}'.format((100 - self.stats['idle']) / 100) - ret.append(self.curse_add_line(msg)) + if idle_tag: + ret.append(self.curse_add_line(msg, self.get_alert_log((100 - self.stats['idle']) / 100, header="system"))) + else: + ret.append(self.curse_add_line(msg)) # Nice CPU if 'nice' in self.stats: msg = ' {0:8}'.format(_("nice:")) @@ -131,7 +168,12 @@ class Plugin(GlancesPlugin): msg = '{0:8}'.format(_("user:")) ret.append(self.curse_add_line(msg)) msg = '{0:>6.1%}'.format(self.stats['user'] / 100) - ret.append(self.curse_add_line(msg, self.get_alert_log(self.stats['user'], header="user"))) + ret.append(self.curse_add_line(msg, self.get_alert_log(self.stats['user'], header="user"))) + elif 'idle' in self.stats: + msg = '{0:8}'.format(_("idle:")) + ret.append(self.curse_add_line(msg)) + msg = '{0:>6.1%}'.format(self.stats['idle'] / 100) + ret.append(self.curse_add_line(msg)) # IRQ CPU if 'irq' in self.stats: msg = ' {0:8}'.format(_("irq:")) @@ -141,11 +183,16 @@ class Plugin(GlancesPlugin): # New line ret.append(self.curse_new_line()) # System CPU - if 'system' in self.stats: + if 'system' in self.stats and not idle_tag: msg = '{0:8}'.format(_("system:")) ret.append(self.curse_add_line(msg)) msg = '{0:>6.1%}'.format(self.stats['system'] / 100) ret.append(self.curse_add_line(msg, self.get_alert_log(self.stats['system'], header="system"))) + else: + msg = '{0:8}'.format(_("core:")) + ret.append(self.curse_add_line(msg)) + msg = '{0:>6}'.format(self.stats['nb_log_core']) + ret.append(self.curse_add_line(msg)) # IOWait CPU if 'iowait' in self.stats: msg = ' {0:8}'.format(_("iowait:")) @@ -155,7 +202,7 @@ class Plugin(GlancesPlugin): # New line ret.append(self.curse_new_line()) # Idle CPU - if 'idle' in self.stats: + if 'idle' in self.stats and not idle_tag: msg = '{0:8}'.format(_("idle:")) ret.append(self.curse_add_line(msg)) msg = '{0:>6.1%}'.format(self.stats['idle'] / 100) diff --git a/glances/plugins/glances_diskio.py b/glances/plugins/glances_diskio.py index b42226b4..5997570f 100644 --- a/glances/plugins/glances_diskio.py +++ b/glances/plugins/glances_diskio.py @@ -39,12 +39,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 3 # Init the stats self.reset() @@ -134,13 +128,15 @@ class Plugin(GlancesPlugin): # Do not display hidden interfaces if self.is_hide(i['disk_name']): continue + # Is there an alias for the disk name ? + disk_name = self.has_alias(i['disk_name']) + if disk_name is None: + disk_name = i['disk_name'] # New line ret.append(self.curse_new_line()) - if len(i['disk_name']) > 9: + if len(disk_name) > 9: # Cut disk name if it is too long - disk_name = '_' + i['disk_name'][-8:] - else: - disk_name = i['disk_name'] + disk_name = '_' + disk_name[-8:] msg = '{0:9}'.format(disk_name) ret.append(self.curse_add_line(msg)) txps = self.auto_unit(int(i['read_bytes'] // i['time_since_update'])) diff --git a/glances/plugins/glances_fs.py b/glances/plugins/glances_fs.py index c4f7d322..3a12e089 100644 --- a/glances/plugins/glances_fs.py +++ b/glances/plugins/glances_fs.py @@ -19,8 +19,14 @@ """File system plugin.""" +# System libs +import base64 + +# Glances libs +from glances.core.glances_globals import version, logger from glances.plugins.glances_plugin import GlancesPlugin +# PSutil lib for local grab import psutil # SNMP OID @@ -37,11 +43,16 @@ import psutil # Used space on the disk: .1.3.6.1.4.1.2021.9.1.8.1 # Percentage of space used on disk: .1.3.6.1.4.1.2021.9.1.9.1 # Percentage of inodes used on disk: .1.3.6.1.4.1.2021.9.1.10.1 -snmp_oid = {'mnt_point': '1.3.6.1.4.1.2021.9.1.2', - 'device_name': '1.3.6.1.4.1.2021.9.1.3', - 'size': '1.3.6.1.4.1.2021.9.1.6', - 'used': '1.3.6.1.4.1.2021.9.1.8', - 'percent': '1.3.6.1.4.1.2021.9.1.9'} +snmp_oid = {'default': {'mnt_point': '1.3.6.1.4.1.2021.9.1.2', + 'device_name': '1.3.6.1.4.1.2021.9.1.3', + 'size': '1.3.6.1.4.1.2021.9.1.6', + 'used': '1.3.6.1.4.1.2021.9.1.8', + 'percent': '1.3.6.1.4.1.2021.9.1.9'}, + 'windows': {'mnt_point': '1.3.6.1.2.1.25.2.3.1.3', + 'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4', + 'size': '1.3.6.1.2.1.25.2.3.1.5', + 'used': '1.3.6.1.2.1.25.2.3.1.6'}} +snmp_oid['esxi'] = snmp_oid['windows'] class Plugin(GlancesPlugin): @@ -57,12 +68,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 4 # Init the stats self.reset() @@ -109,22 +114,42 @@ class Plugin(GlancesPlugin): elif self.get_input() == 'snmp': # Update stats using SNMP - # SNMP bulk command to get all file system in one shot - fs_stat = self.set_stats_snmp(snmp_oid=snmp_oid, bulk=True) + # SNMP bulk command to get all file system in one shot + try: + fs_stat = self.set_stats_snmp(snmp_oid=snmp_oid[self.get_short_system_name()], + bulk=True) + except KeyError: + fs_stat = self.set_stats_snmp(snmp_oid=snmp_oid['default'], + bulk=True) # Loop over fs - for fs in fs_stat: - fs_current = {} - fs_current['device_name'] = fs_stat[fs]['device_name'] - fs_current['mnt_point'] = fs - fs_current['size'] = int(fs_stat[fs]['size']) * 1024 - fs_current['used'] = int(fs_stat[fs]['used']) * 1024 - fs_current['percent'] = float(fs_stat[fs]['percent']) - self.stats.append(fs_current) + if self.get_short_system_name() in ('windows', 'esxi'): + # Windows or ESXi tips + for fs in fs_stat: + # Memory stats are grabed in the same OID table (ignore it) + if fs == 'Virtual Memory' or fs == 'Physical Memory' or fs == 'Real Memory': + continue + fs_current = {} + fs_current['device_name'] = '' + fs_current['mnt_point'] = fs.partition(' ')[0] + fs_current['size'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit']) + fs_current['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit']) + fs_current['percent'] = float(fs_current['used'] * 100 / fs_current['size']) + self.stats.append(fs_current) + else: + # Default behavor + for fs in fs_stat: + fs_current = {} + fs_current['device_name'] = fs_stat[fs]['device_name'] + fs_current['mnt_point'] = fs + fs_current['size'] = int(fs_stat[fs]['size']) * 1024 + fs_current['used'] = int(fs_stat[fs]['used']) * 1024 + fs_current['percent'] = float(fs_stat[fs]['percent']) + self.stats.append(fs_current) return self.stats - def msg_curse(self, args=None): + def msg_curse(self, args=None, max_width=None): """Return the dict to display in the curse interface.""" # Init the return message ret = [] @@ -133,9 +158,16 @@ class Plugin(GlancesPlugin): if self.stats == [] or args.disable_fs: return ret + # Max size for the fsname name + if max_width is not None and max_width >= 23: + # Interface size name = max_width - space for interfaces bitrate + fsname_max_width = max_width - 14 + else: + fsname_max_width = 9 + # Build the string message # Header - msg = '{0:9}'.format(_("FILE SYS")) + msg = '{0:{width}}'.format(_("FILE SYS"), width=fsname_max_width) ret.append(self.curse_add_line(msg, "TITLE")) msg = '{0:>7}'.format(_("Used")) ret.append(self.curse_add_line(msg)) @@ -146,15 +178,17 @@ class Plugin(GlancesPlugin): for i in sorted(self.stats, key=lambda fs: fs['mnt_point']): # New line ret.append(self.curse_new_line()) - if len(i['mnt_point']) + len(i['device_name'].split('/')[-1]) <= 6: + if i['device_name'] == '' or i['device_name'] == 'none': + mnt_point = i['mnt_point'][-fsname_max_width+1:] + elif len(i['mnt_point']) + len(i['device_name'].split('/')[-1]) <= fsname_max_width - 3: # If possible concatenate mode info... Glances touch inside :) mnt_point = i['mnt_point'] + ' (' + i['device_name'].split('/')[-1] + ')' - elif len(i['mnt_point']) > 9: + elif len(i['mnt_point']) > fsname_max_width: # Cut mount point name if it is too long - mnt_point = '_' + i['mnt_point'][-8:] + mnt_point = '_' + i['mnt_point'][-fsname_max_width+1:] else: mnt_point = i['mnt_point'] - msg = '{0:9}'.format(mnt_point) + msg = '{0:{width}}'.format(mnt_point, width=fsname_max_width) ret.append(self.curse_add_line(msg)) msg = '{0:>7}'.format(self.auto_unit(i['used'])) ret.append(self.curse_add_line(msg, self.get_alert(i['used'], max=i['size']))) diff --git a/glances/plugins/glances_help.py b/glances/plugins/glances_help.py index d824f194..0daa886a 100644 --- a/glances/plugins/glances_help.py +++ b/glances/plugins/glances_help.py @@ -32,18 +32,15 @@ class Plugin(GlancesPlugin): """Glances' help plugin.""" - def __init__(self, args=None): + def __init__(self, args=None, config=None): """Init the plugin.""" GlancesPlugin.__init__(self, args=args) + # Set the config instance + self.config = config + # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 0 def update(self): """No stats. It is just a plugin to display the help.""" @@ -58,62 +55,88 @@ class Plugin(GlancesPlugin): # Header msg = '{0} {1}'.format(appname.title(), version) ret.append(self.curse_add_line(msg, "TITLE")) - msg = _(" with psutil {0}").format(psutil_version) + msg = _(" with PSutil {0}").format(psutil_version) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) + # Configuration file path + try: + msg = '{0}: {1}'.format(_("Configuration file"), self.config.get_loaded_config_file()) + except AttributeError as e: + pass + else: + ret.append(self.curse_new_line()) + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_new_line()) + # Keys msg_col = ' {0:1} {1:35}' msg_col2 = ' {0:1} {1:35}' ret.append(self.curse_new_line()) - msg = msg_col.format(_("a"), _("Sort processes automatically")) + msg = msg_col.format("a", _("Sort processes automatically")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("b"), _("Bytes or bits for network I/O")) + msg = msg_col2.format("b", _("Bytes or bits for network I/O")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("c"), _("Sort processes by CPU%")) + msg = msg_col.format("c", _("Sort processes by CPU%")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("l"), _("Show/hide logs (alerts)")) + msg = msg_col2.format("l", _("Show/hide logs (alerts)")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("m"), _("Sort processes by MEM%")) + msg = msg_col.format("m", _("Sort processes by MEM%")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("w"), _("Delete warning alerts")) + msg = msg_col2.format("w", _("Delete warning alerts")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("p"), _("Sort processes by name")) + msg = msg_col.format("p", _("Sort processes by name")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("x"), _("Delete warning and critical alerts")) + msg = msg_col2.format("x", _("Delete warning and critical alerts")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("i"), _("Sort processes by I/O rate")) + msg = msg_col.format("i", _("Sort processes by I/O rate")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("1"), _("Global CPU or per-CPU stats")) + msg = msg_col2.format("1", _("Global CPU or per-CPU stats")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("d"), _("Show/hide disk I/O stats")) + msg = msg_col.format("d", _("Show/hide disk I/O stats")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("h"), _("Show/hide this help screen")) + msg = msg_col2.format("h", _("Show/hide this help screen")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("f"), _("Show/hide file system stats")) + msg = msg_col.format("f", _("Show/hide file system stats")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("t"), _("View network I/O as combination")) + msg = msg_col2.format("t", _("View network I/O as combination")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("n"), _("Show/hide network stats")) + msg = msg_col.format("n", _("Show/hide network stats")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("u"), _("View cumulative network I/O")) + msg = msg_col2.format("u", _("View cumulative network I/O")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("s"), _("Show/hide sensors stats")) + msg = msg_col.format("s", _("Show/hide sensors stats")) ret.append(self.curse_add_line(msg)) - msg = msg_col2.format(_("z"), _("Enable/disable processes stats")) + msg = msg_col2.format("g", _("Generate graphs for current history")) ret.append(self.curse_add_line(msg)) ret.append(self.curse_new_line()) - msg = msg_col.format(_("q"), _("Quit (Esc and Ctrl-C also work)")) + msg = msg_col.format("z", _("Enable/disable processes stats")) ret.append(self.curse_add_line(msg)) + msg = msg_col2.format("r", _("Reset history")) + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_new_line()) + msg = msg_col.format("e", _("Enable/disable top extended stats")) + ret.append(self.curse_add_line(msg)) + msg = msg_col2.format("q", _("Quit (Esc and Ctrl-C also work)")) + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_new_line()) + msg = msg_col.format("/", _("Enable/disable short processes name")) + ret.append(self.curse_add_line(msg)) + + ret.append(self.curse_new_line()) + ret.append(self.curse_new_line()) + msg = '{0}: {1}'.format("ENTER", _("Edit the process filter patern")) + ret.append(self.curse_add_line(msg)) + # Return the message with decoration return ret diff --git a/glances/plugins/glances_load.py b/glances/plugins/glances_load.py index 4b6a7a14..22a3cd04 100644 --- a/glances/plugins/glances_load.py +++ b/glances/plugins/glances_load.py @@ -23,6 +23,7 @@ import os # Import Glances libs +from glances.core.glances_globals import logger from glances.plugins.glances_core import Plugin as CorePlugin from glances.plugins.glances_plugin import GlancesPlugin @@ -34,6 +35,13 @@ snmp_oid = {'min1': '1.3.6.1.4.1.2021.10.1.3.1', 'min5': '1.3.6.1.4.1.2021.10.1.3.2', 'min15': '1.3.6.1.4.1.2021.10.1.3.3'} +# Define the history items list +# All items in this list will be historised if the --enable-history tag is set +# 'color' define the graph color in #RGB format +items_history_list = [{'name': 'min1', 'color': '#0000FF'}, + {'name': 'min5', 'color': '#0000AA'}, + {'name': 'min15', 'color': '#000044'}] + class Plugin(GlancesPlugin): @@ -44,20 +52,20 @@ class Plugin(GlancesPlugin): def __init__(self, args=None): """Init the plugin.""" - GlancesPlugin.__init__(self, args=args) + GlancesPlugin.__init__(self, args=args, items_history_list=items_history_list) # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 1 - # Enter -1 to diplay bottom - self.line_curse = 1 # Init stats self.reset() + # Call CorePlugin in order to display the core number + try: + self.nb_log_core = CorePlugin(args=self.args).update()["log"] + except Exception: + self.nb_log_core = 0 + def reset(self): """Reset/init the stats.""" self.stats = {} @@ -67,12 +75,6 @@ class Plugin(GlancesPlugin): # Reset stats self.reset() - # Call CorePlugin in order to display the core number - try: - nb_log_core = CorePlugin().update()["log"] - except Exception: - nb_log_core = 0 - if self.get_input() == 'local': # Update stats using the standard system lib @@ -85,19 +87,28 @@ class Plugin(GlancesPlugin): self.stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], - 'cpucore': nb_log_core} + 'cpucore': self.nb_log_core} elif self.get_input() == 'snmp': # Update stats using SNMP self.stats = self.set_stats_snmp(snmp_oid=snmp_oid) - self.stats['cpucore'] = nb_log_core - if self.stats['min1'] == '': self.reset() return self.stats - for key in self.stats.iterkeys(): - self.stats[key] = float(self.stats[key]) + # Python 3 return a dict like: + # {'min1': "b'0.08'", 'min5': "b'0.12'", 'min15': "b'0.15'"} + try: + iteritems = self.stats.iteritems() + except AttributeError: + iteritems = self.stats.items() + for k, v in iteritems: + self.stats[k] = float(v) + + self.stats['cpucore'] = self.nb_log_core + + # Update the history list + self.update_stats_history() return self.stats @@ -116,7 +127,7 @@ class Plugin(GlancesPlugin): ret.append(self.curse_add_line(msg, "TITLE")) # Core number if self.stats['cpucore'] > 0: - msg = _("{0}-core").format(self.stats['cpucore'], '>1') + msg = _("{0:d}-core").format(int(self.stats['cpucore']), '>1') ret.append(self.curse_add_line(msg)) # New line ret.append(self.curse_new_line()) diff --git a/glances/plugins/glances_mem.py b/glances/plugins/glances_mem.py index c89ac343..6e6ed101 100644 --- a/glances/plugins/glances_mem.py +++ b/glances/plugins/glances_mem.py @@ -30,12 +30,25 @@ import psutil # Total RAM Shared: .1.3.6.1.4.1.2021.4.13.0 # Total RAM Buffered: .1.3.6.1.4.1.2021.4.14.0 # Total Cached Memory: .1.3.6.1.4.1.2021.4.15.0 -snmp_oid = {'total': '1.3.6.1.4.1.2021.4.5.0', - # 'used': '1.3.6.1.4.1.2021.4.6.0', - 'free': '1.3.6.1.4.1.2021.4.11.0', - 'shared': '1.3.6.1.4.1.2021.4.13.0', - 'buffers': '1.3.6.1.4.1.2021.4.14.0', - 'cached': '1.3.6.1.4.1.2021.4.15.0'} +# Note: For Windows, stats are in the FS table +snmp_oid = {'default': {'total': '1.3.6.1.4.1.2021.4.5.0', + 'free': '1.3.6.1.4.1.2021.4.11.0', + 'shared': '1.3.6.1.4.1.2021.4.13.0', + 'buffers': '1.3.6.1.4.1.2021.4.14.0', + 'cached': '1.3.6.1.4.1.2021.4.15.0'}, + 'windows': {'mnt_point': '1.3.6.1.2.1.25.2.3.1.3', + 'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4', + 'size': '1.3.6.1.2.1.25.2.3.1.5', + 'used': '1.3.6.1.2.1.25.2.3.1.6'}, + 'esxi': {'mnt_point': '1.3.6.1.2.1.25.2.3.1.3', + 'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4', + 'size': '1.3.6.1.2.1.25.2.3.1.5', + 'used': '1.3.6.1.2.1.25.2.3.1.6'}} + +# Define the history items list +# All items in this list will be historised if the --enable-history tag is set +# 'color' define the graph color in #RGB format +items_history_list = [{'name': 'percent', 'color': '#00FF00'}] class Plugin(GlancesPlugin): @@ -47,16 +60,10 @@ class Plugin(GlancesPlugin): def __init__(self, args=None): """Init the plugin.""" - GlancesPlugin.__init__(self, args=args) + GlancesPlugin.__init__(self, args=args, items_history_list=items_history_list) # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 2 - # Enter -1 to diplay bottom - self.line_curse = 1 # Init the stats self.reset() @@ -88,7 +95,7 @@ class Plugin(GlancesPlugin): # cached: (Linux, BSD): cache for various things. # wired: (BSD, OSX): memory that is marked to always stay in RAM. It is never moved to disk. # shared: (BSD): memory that may be simultaneously accessed by multiple processes. - self.stats = {} + self.reset() for mem in ['total', 'available', 'percent', 'used', 'free', 'active', 'inactive', 'buffers', 'cached', 'wired', 'shared']: @@ -106,24 +113,46 @@ class Plugin(GlancesPlugin): self.stats['used'] = self.stats['total'] - self.stats['free'] elif self.get_input() == 'snmp': # Update stats using SNMP - self.stats = self.set_stats_snmp(snmp_oid=snmp_oid) + if self.get_short_system_name() in ('windows', 'esxi'): + # Mem stats for Windows|Vmware Esxi are stored in the FS table + try: + fs_stat = self.set_stats_snmp(snmp_oid=snmp_oid[self.get_short_system_name()], + bulk=True) + except KeyError: + self.reset() + else: + for fs in fs_stat: + # The Physical Memory (Windows) or Real Memory (VmWare) + # gives statistics on RAM usage and availability. + if fs in ('Physical Memory', 'Real Memory'): + self.stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit']) + self.stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit']) + self.stats['percent'] = float(self.stats['used'] * 100 / self.stats['total']) + self.stats['free'] = self.stats['total'] - self.stats['used'] + break + else: + # Default behavor for others OS + self.stats = self.set_stats_snmp(snmp_oid=snmp_oid['default']) - if self.stats['total'] == '': - self.reset() - return self.stats + if self.stats['total'] == '': + self.reset() + return self.stats - for key in self.stats.iterkeys(): - if self.stats[key] != '': - self.stats[key] = float(self.stats[key]) * 1024 + for key in list(self.stats.keys()): + if self.stats[key] != '': + self.stats[key] = float(self.stats[key]) * 1024 - # Use the 'free'/htop calculation - self.stats['free'] = self.stats['free'] - self.stats['total'] + (self.stats['buffers'] + self.stats['cached']) + # Use the 'free'/htop calculation + self.stats['free'] = self.stats['free'] - self.stats['total'] + (self.stats['buffers'] + self.stats['cached']) - # used=total-free - self.stats['used'] = self.stats['total'] - self.stats['free'] + # used=total-free + self.stats['used'] = self.stats['total'] - self.stats['free'] - # percent: the percentage usage calculated as (total - available) / total * 100. - self.stats['percent'] = float((self.stats['total'] - self.stats['free']) / self.stats['total'] * 100) + # percent: the percentage usage calculated as (total - available) / total * 100. + self.stats['percent'] = float((self.stats['total'] - self.stats['free']) / self.stats['total'] * 100) + + # Update the history list + self.update_stats_history() return self.stats diff --git a/glances/plugins/glances_memswap.py b/glances/plugins/glances_memswap.py index 1d0091b3..180b3344 100644 --- a/glances/plugins/glances_memswap.py +++ b/glances/plugins/glances_memswap.py @@ -26,8 +26,12 @@ import psutil # SNMP OID # Total Swap Size: .1.3.6.1.4.1.2021.4.3.0 # Available Swap Space: .1.3.6.1.4.1.2021.4.4.0 -snmp_oid = {'total': '1.3.6.1.4.1.2021.4.3.0', - 'free': '1.3.6.1.4.1.2021.4.4.0'} +snmp_oid = {'default': {'total': '1.3.6.1.4.1.2021.4.3.0', + 'free': '1.3.6.1.4.1.2021.4.4.0'}, + 'windows': {'mnt_point': '1.3.6.1.2.1.25.2.3.1.3', + 'alloc_unit': '1.3.6.1.2.1.25.2.3.1.4', + 'size': '1.3.6.1.2.1.25.2.3.1.5', + 'used': '1.3.6.1.2.1.25.2.3.1.6'}} class Plugin(GlancesPlugin): @@ -43,12 +47,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 3 - # Enter -1 to diplay bottom - self.line_curse = 1 # Init the stats self.reset() @@ -80,21 +78,39 @@ class Plugin(GlancesPlugin): self.stats[swap] = getattr(sm_stats, swap) elif self.get_input() == 'snmp': # Update stats using SNMP - self.stats = self.set_stats_snmp(snmp_oid=snmp_oid) + if self.get_short_system_name() == 'windows': + # Mem stats for Windows OS are stored in the FS table + try: + fs_stat = self.set_stats_snmp(snmp_oid=snmp_oid[self.get_short_system_name()], + bulk=True) + except KeyError: + self.reset() + else: + for fs in fs_stat: + # The virtual memory concept is used by the operating system to extend (virtually) the physical + # memory and thus to run more programs by swapping unused memory zone (page) to a disk file. + if fs == 'Virtual Memory': + self.stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit']) + self.stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit']) + self.stats['percent'] = float(self.stats['used'] * 100 / self.stats['total']) + self.stats['free'] = self.stats['total'] - self.stats['used'] + break + else: + self.stats = self.set_stats_snmp(snmp_oid=snmp_oid['default']) - if self.stats['total'] == '': - self.reset() - return self.stats + if self.stats['total'] == '': + self.reset() + return self.stats - for key in self.stats.iterkeys(): - if self.stats[key] != '': - self.stats[key] = float(self.stats[key]) * 1024 + for key in list(self.stats.keys()): + if self.stats[key] != '': + self.stats[key] = float(self.stats[key]) * 1024 - # used=total-free - self.stats['used'] = self.stats['total'] - self.stats['free'] + # used=total-free + self.stats['used'] = self.stats['total'] - self.stats['free'] - # percent: the percentage usage calculated as (total - available) / total * 100. - self.stats['percent'] = float((self.stats['total'] - self.stats['free']) / self.stats['total'] * 100) + # percent: the percentage usage calculated as (total - available) / total * 100. + self.stats['percent'] = float((self.stats['total'] - self.stats['free']) / self.stats['total'] * 100) return self.stats diff --git a/glances/plugins/glances_monitor.py b/glances/plugins/glances_monitor.py index e8fc2616..6f42f644 100644 --- a/glances/plugins/glances_monitor.py +++ b/glances/plugins/glances_monitor.py @@ -20,6 +20,7 @@ """Monitor plugin.""" # Import Glances lib +from glances.core.glances_globals import logger from glances.core.glances_monitor_list import MonitorList as glancesMonitorList from glances.plugins.glances_plugin import GlancesPlugin @@ -34,12 +35,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 1 - # Enter -1 to diplay bottom - self.line_curse = 3 # Init stats self.glances_monitors = None @@ -47,7 +42,7 @@ class Plugin(GlancesPlugin): def load_limits(self, config): """Load the monitored list from the conf file.""" - # print "DEBUG: Monitor plugin load config file %s" % config + logger.debug(_("Monitor plugin configuration detected in the configuration file")) self.glances_monitors = glancesMonitorList(config) def update(self): @@ -93,7 +88,7 @@ class Plugin(GlancesPlugin): ret = [] # Only process if stats exist and display plugin enable... - if self.stats == [] or args.disable_process: + if self.stats == [] or args.disable_process: return ret # Build the string message diff --git a/glances/plugins/glances_network.py b/glances/plugins/glances_network.py index 356832f1..32d52fc4 100644 --- a/glances/plugins/glances_network.py +++ b/glances/plugins/glances_network.py @@ -19,6 +19,8 @@ """Network plugin.""" +import base64 + from glances.core.glances_timer import getTimeSinceLastUpdate from glances.plugins.glances_plugin import GlancesPlugin @@ -27,9 +29,9 @@ import psutil # SNMP OID # http://www.net-snmp.org/docs/mibs/interfaces.html # Dict key = interface_name -snmp_oid = {'interface_name': '1.3.6.1.2.1.2.2.1.2', - 'cumulative_rx': '1.3.6.1.2.1.2.2.1.10', - 'cumulative_tx': '1.3.6.1.2.1.2.2.1.16'} +snmp_oid = {'default': {'interface_name': '1.3.6.1.2.1.2.2.1.2', + 'cumulative_rx': '1.3.6.1.2.1.2.2.1.10', + 'cumulative_tx': '1.3.6.1.2.1.2.2.1.16'}} class Plugin(GlancesPlugin): @@ -45,12 +47,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 2 # Init the stats self.reset() @@ -94,7 +90,7 @@ class Plugin(GlancesPlugin): for net in network_new: try: # Try necessary to manage dynamic network interface - netstat = {} + netstat = {} netstat['interface_name'] = net netstat['time_since_update'] = time_since_update netstat['cumulative_rx'] = network_new[net].bytes_recv @@ -118,7 +114,12 @@ class Plugin(GlancesPlugin): # Update stats using SNMP # SNMP bulk command to get all network interface in one shot - netiocounters = self.set_stats_snmp(snmp_oid=snmp_oid, bulk=True) + try: + netiocounters = self.set_stats_snmp(snmp_oid=snmp_oid[self.get_short_system_name()], + bulk=True) + except KeyError: + netiocounters = self.set_stats_snmp(snmp_oid=snmp_oid['default'], + bulk=True) # Previous network interface stats are stored in the network_old variable if not hasattr(self, 'network_old'): @@ -138,7 +139,15 @@ class Plugin(GlancesPlugin): try: # Try necessary to manage dynamic network interface netstat = {} - netstat['interface_name'] = net + # Windows: a tips is needed to convert HEX to TXT + # http://blogs.technet.com/b/networking/archive/2009/12/18/how-to-query-the-list-of-network-interfaces-using-snmp-via-the-ifdescr-counter.aspx + if self.get_short_system_name() == 'windows': + try: + netstat['interface_name'] = str(base64.b16decode(net[2:-2].upper())) + except TypeError: + netstat['interface_name'] = net + else: + netstat['interface_name'] = net netstat['time_since_update'] = time_since_update netstat['cumulative_rx'] = float(network_new[net]['cumulative_rx']) netstat['rx'] = (float(network_new[net]['cumulative_rx']) - @@ -159,9 +168,8 @@ class Plugin(GlancesPlugin): return self.stats - def msg_curse(self, args=None): + def msg_curse(self, args=None, max_width=None): """Return the dict to display in the curse interface.""" - # !!! TODO: Add alert on network interface bitrate # Init the return message ret = [] @@ -170,9 +178,16 @@ class Plugin(GlancesPlugin): if self.stats == [] or args.disable_network: return ret + # Max size for the interface name + if max_width is not None and max_width >= 23: + # Interface size name = max_width - space for interfaces bitrate + ifname_max_width = max_width - 14 + else: + ifname_max_width = 9 + # Build the string message # Header - msg = '{0:9}'.format(_("NETWORK")) + msg = '{0:{width}}'.format(_("NETWORK"), width=ifname_max_width) ret.append(self.curse_add_line(msg, "TITLE")) if args.network_cumul: # Cumulative stats @@ -203,10 +218,13 @@ class Plugin(GlancesPlugin): if self.is_hide(i['interface_name']): continue # Format stats - ifname = i['interface_name'].split(':')[0] - if len(ifname) > 9: + # Is there an alias for the interface name ? + ifname = self.has_alias(i['interface_name']) + if ifname is None: + ifname = i['interface_name'].split(':')[0] + if len(ifname) > ifname_max_width: # Cut interface name if it is too long - ifname = '_' + ifname[-8:] + ifname = '_' + ifname[-ifname_max_width+1:] if args.byte: # Bytes per second (for dummy) if args.network_cumul: @@ -233,7 +251,7 @@ class Plugin(GlancesPlugin): int(i['tx'] // i['time_since_update'] * 8)) + "b" # New line ret.append(self.curse_new_line()) - msg = '{0:9}'.format(ifname) + msg = '{0:{width}}'.format(ifname, width=ifname_max_width) ret.append(self.curse_add_line(msg)) if args.network_sum: msg = '{0:>14}'.format(sx) diff --git a/glances/plugins/glances_now.py b/glances/plugins/glances_now.py index a7713ae5..7c57d6d9 100644 --- a/glances/plugins/glances_now.py +++ b/glances/plugins/glances_now.py @@ -37,12 +37,9 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True + # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = -1 + self.set_align('bottom') def update(self): """Update current date/time.""" diff --git a/glances/plugins/glances_percpu.py b/glances/plugins/glances_percpu.py index d6de4fa9..c230eeb8 100644 --- a/glances/plugins/glances_percpu.py +++ b/glances/plugins/glances_percpu.py @@ -39,12 +39,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 1 # Init stats self.reset() diff --git a/glances/plugins/glances_plugin.py b/glances/plugins/glances_plugin.py index a1daf1c9..29071ed3 100644 --- a/glances/plugins/glances_plugin.py +++ b/glances/plugins/glances_plugin.py @@ -24,30 +24,41 @@ I am your father... """ # Import system libs +from datetime import datetime import json +from operator import itemgetter # Import Glances lib -from glances.core.glances_globals import glances_logs +from glances.core.glances_globals import glances_logs, logger class GlancesPlugin(object): """Main class for Glances' plugin.""" - def __init__(self, args=None): + def __init__(self, args=None, items_history_list=None): """Init the plugin of plugins class.""" # Plugin name (= module name without glances_) self.plugin_name = self.__class__.__module__[len('glances_'):] + # logger.debug(_("Init plugin %s") % self.plugin_name) # Init the args self.args = args + # Init the default alignement (for curses) + self.set_align('left') + # Init the input method self.input_method = 'local' + self.short_system_name = None # Init the stats list self.stats = None + # Init the history list + self.items_history_list = items_history_list + self.stats_history = self.init_stats_history() + # Init the limits dictionnary self.limits = dict() @@ -59,20 +70,70 @@ class GlancesPlugin(object): """Return the human-readable stats.""" return str(self.stats) - def set_input(self, input_method): + def init_stats_history(self): + """Init the stats history (dict of list)""" + ret = None + if self.args is not None and self.args.enable_history and self.get_items_history_list() is not None: + iList = [i['name'] for i in self.get_items_history_list()] + logger.debug(_("Stats history activated for plugin %s (items: %s)") % (self.plugin_name, iList)) + ret = {} + # First column for the date + ret['date'] = [] + for i in self.get_items_history_list(): + # One column per item + ret[i['name']] = [] + return ret + + def reset_stats_history(self): + """Reset the stats history (dict of list)""" + if self.args is not None and self.args.enable_history and self.get_items_history_list() is not None: + iList = [i['name'] for i in self.get_items_history_list()] + logger.debug(_("Reset history for plugin %s (items: %s)") % (self.plugin_name, iList)) + self.stats_history = {} + # First column for the date + self.stats_history['date'] = [] + for i in self.get_items_history_list(): + # One column per item + self.stats_history[i['name']] = [] + return self.stats_history + + def update_stats_history(self): + """Update stats history""" + if self.args is not None and self.args.enable_history and self.get_items_history_list() is not None: + self.stats_history['date'].append(datetime.now()) + for i in self.get_items_history_list(): + self.stats_history[i['name']].append(self.stats[i['name']]) + return self.stats_history + + def get_stats_history(self): + """Return the stats history""" + return self.stats_history + + def get_items_history_list(self): + """Return the items history list""" + return self.items_history_list + + def set_input(self, input_method, short_system_name=None): """Set the input method. * local: system local grab (psutil or direct access) * snmp: Client server mode via SNMP * glances: Client server mode via Glances API + + For SNMP, short_system_name is detected short OS name """ self.input_method = input_method + self.short_system_name = short_system_name return self.input_method def get_input(self): """Get the input method.""" return self.input_method + def get_short_system_name(self): + """Get the short detected OS name""" + return self.short_system_name + def set_stats(self, input_stats): """Set the stats to input_stats.""" self.stats = input_stats @@ -97,28 +158,35 @@ class GlancesPlugin(object): # Bulk request snmpresult = clientsnmp.getbulk_by_oid(0, 10, *snmp_oid.values()) - # Build the internal dict with the SNMP result - # key is the first item in the snmp_oid - index = 1 - for item in snmpresult: - item_stats = {} - item_key = None - for key in snmp_oid.iterkeys(): - oid = snmp_oid[key] + '.' + str(index) - if oid in item: - if item_key is None: - item_key = item[oid] - else: - item_stats[key] = item[oid] - if item_stats != {}: - ret[item_key] = item_stats - index += 1 + if len(snmp_oid) == 1: + # Bulk command for only one OID + # Note: key is the item indexed but the OID result + for item in snmpresult: + if item.keys()[0].startswith(snmp_oid.values()[0]): + ret[snmp_oid.keys()[0] + item.keys()[0].split(snmp_oid.values()[0])[1]] = item.values()[0] + else: + # Build the internal dict with the SNMP result + # Note: key is the first item in the snmp_oid + index = 1 + for item in snmpresult: + item_stats = {} + item_key = None + for key in list(snmp_oid.keys()): + oid = snmp_oid[key] + '.' + str(index) + if oid in item: + if item_key is None: + item_key = item[oid] + else: + item_stats[key] = item[oid] + if item_stats != {}: + ret[item_key] = item_stats + index += 1 else: # Simple get request snmpresult = clientsnmp.get_by_oid(*snmp_oid.values()) # Build the internal dict with the SNMP result - for key in snmp_oid.iterkeys(): + for key in list(snmp_oid.keys()): ret[key] = snmpresult[snmp_oid[key]] return ret @@ -128,17 +196,52 @@ class GlancesPlugin(object): return self.stats def get_stats(self): - """Return the stats object in JSON format for the XML-RPC API.""" + """Return the stats object in JSON format""" return json.dumps(self.stats) + def get_stats_item(self, item): + """ + Return the stats object for a specific item (in JSON format) + Stats should be a list of dict (processlist, network...) + """ + if type(self.stats) is not list: + if type(self.stats) is dict: + try: + return json.dumps({ item: self.stats[item] }) + except KeyError as e: + logger.error(_("Can not get item %s (%s)") % (item, e)) + else: + return None + else: + try: + # Source: http://stackoverflow.com/questions/4573875/python-get-index-of-dictionary-item-in-list + return json.dumps({ item: map(itemgetter(item), self.stats) }) + except (KeyError, ValueError) as e: + logger.error(_("Can not get item %s (%s)") % (item, e)) + return None + + def get_stats_value(self, item, value): + """ + Return the stats object for a specific item=value (in JSON format) + Stats should be a list of dict (processlist, network...) + """ + if type(self.stats) is not list: + return None + else: + if value.isdigit(): + value = int(value) + try: + return json.dumps({ value: [i for i in self.stats if i[item] == value] }) + except (KeyError, ValueError) as e: + logger.error(_("Can not get item(%s)=value(%s) (%s)") % (item, value,e)) + return None + def load_limits(self, config): """Load the limits from the configuration file.""" if (hasattr(config, 'has_section') and config.has_section(self.plugin_name)): - # print "Load limits for %s" % self.plugin_name for s, v in config.items(self.plugin_name): # Read limits - # print "\t%s = %s" % (self.plugin_name + '_' + s, v) try: self.limits[self.plugin_name + '_' + s] = config.get_option(self.plugin_name, s) except ValueError: @@ -229,55 +332,67 @@ class GlancesPlugin(object): else: return self.limits[self.plugin_name + '_' + header + '_' + 'careful'] - def get_hide(self, header=""): - """Return the hide configuration list key for the current plugin.""" + def get_conf_value(self, value, header="", plugin_name=None): + """Return the configuration (header_)value for the current plugin (or the one given by the plugin_name var)""" + if plugin_name is None: + plugin_name = self.plugin_name if header == "": try: - return self.limits[self.plugin_name + '_' + 'hide'] + return self.limits[plugin_name + '_' + value] except KeyError: return [] else: try: - return self.limits[self.plugin_name + '_' + header + '_' + 'hide'] + return self.limits[plugin_name + '_' + header + '_' + value] except KeyError: return [] def is_hide(self, value, header=""): """Return True if the value is in the hide configuration list.""" - return value in self.get_hide(header=header) + return value in self.get_conf_value('hide', header=header) - def msg_curse(self, args): + def has_alias(self, header): + """Return the alias name for the relative header or None if nonexist""" + try: + return self.limits[self.plugin_name + '_' + header + '_' + 'alias'][0] + except (KeyError, IndexError): + return None + + def msg_curse(self, args=None, max_width=None): """Return default string to display in the curse interface.""" return [self.curse_add_line(str(self.stats))] - def get_stats_display(self, args=None): + def get_stats_display(self, args=None, max_width=None): """Return a dict with all the information needed to display the stat. key | description ---------------------------- display | Display the stat (True or False) msgdict | Message to display (list of dict [{ 'msg': msg, 'decoration': decoration } ... ]) - column | column number - line | Line number + align | Message position """ display_curse = False - column_curse = -1 - line_curse = -1 if hasattr(self, 'display_curse'): display_curse = self.display_curse - if hasattr(self, 'column_curse'): - column_curse = self.column_curse - if hasattr(self, 'line_curse'): - line_curse = self.line_curse + if hasattr(self, 'align'): + align_curse = self.align - return {'display': display_curse, - 'msgdict': self.msg_curse(args), - 'column': column_curse, - 'line': line_curse} + if max_width is not None: + ret = {'display': display_curse, + 'msgdict': self.msg_curse(args, max_width=max_width), + 'align': align_curse} + else: + ret = {'display': display_curse, + 'msgdict': self.msg_curse(args), + 'align': align_curse} - def curse_add_line(self, msg, decoration="DEFAULT", optional=False, splittable=False): - """Return a dict with: { 'msg': msg, 'decoration': decoration, 'optional': False }. + return ret + + def curse_add_line(self, msg, decoration="DEFAULT", + optional=False, additional=False, + splittable=False): + """Return a dict with Where: msg: string @@ -298,14 +413,26 @@ class GlancesPlugin(object): CRITICAL: Value is CRITICAL and non logged CRITICAL_LOG: Value is CRITICAL and logged optional: True if the stat is optional (display only if space is available) + additional: True if the stat is additional (display only if space is available after optional) spittable: Line can be splitted to fit on the screen (default is not) """ - return {'msg': msg, 'decoration': decoration, 'optional': optional, 'splittable': splittable} + return {'msg': msg, 'decoration': decoration, 'optional': optional, 'additional': additional, 'splittable': splittable} def curse_new_line(self): """Go to a new line.""" return self.curse_add_line('\n') + def set_align(self, align='left'): + """Set the Curse align""" + if align in ('left', 'right', 'bottom'): + self.align = align + else: + self.align = 'left' + + def get_align(self): + """Get the Curse align""" + return self.align + def auto_unit(self, number, low_precision=False): """Make a nice human-readable string out of number. diff --git a/glances/plugins/glances_processcount.py b/glances/plugins/glances_processcount.py index 0de5a6a9..b171258a 100644 --- a/glances/plugins/glances_processcount.py +++ b/glances/plugins/glances_processcount.py @@ -37,12 +37,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 1 - # Enter -1 to diplay bottom - self.line_curse = 2 # Note: 'glances_processes' is already init in the glances_processes.py script @@ -75,9 +69,6 @@ class Plugin(GlancesPlugin): ret = [] # Only process if stats exist and display plugin enable... - # if self.stats == {} or args.disable_process: - # return ret - if args.disable_process: msg = _("PROCESSES DISABLED (press 'z' to display)") ret.append(self.curse_add_line(msg)) @@ -86,6 +77,16 @@ class Plugin(GlancesPlugin): if self.stats == {}: return ret + # Display the filter (if it exists) + if glances_processes.get_process_filter() is not None: + msg = _("Processes filter:") + ret.append(self.curse_add_line(msg, "TITLE")) + msg = _(" {0} ").format(glances_processes.get_process_filter()) + ret.append(self.curse_add_line(msg, "FILTER")) + msg = _("(press ENTER to edit)") + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_new_line()) + # Build the string message # Header msg = _("TASKS ") @@ -113,17 +114,13 @@ class Plugin(GlancesPlugin): ret.append(self.curse_add_line(msg)) # Display sort information - try: - args.process_sorted_by - except AttributeError: - args.process_sorted_by = glances_processes.getsortkey() - if args.process_sorted_by == 'auto': + if glances_processes.getmanualsortkey() is None: msg = _("sorted automatically") ret.append(self.curse_add_line(msg)) - msg = _(" by {0}").format(glances_processes.getsortkey()) + msg = _(" by {0}").format(glances_processes.getautosortkey()) ret.append(self.curse_add_line(msg)) else: - msg = _("sorted by {0}").format(args.process_sorted_by) + msg = _("sorted by {0}").format(glances_processes.getmanualsortkey()) ret.append(self.curse_add_line(msg)) # Return the message with decoration diff --git a/glances/plugins/glances_processlist.py b/glances/plugins/glances_processlist.py index 348ea4a6..269a7e4f 100644 --- a/glances/plugins/glances_processlist.py +++ b/glances/plugins/glances_processlist.py @@ -24,7 +24,7 @@ import os from datetime import timedelta # Import Glances libs -from glances.core.glances_globals import glances_processes, is_windows +from glances.core.glances_globals import glances_processes, is_linux, is_bsd, is_mac, is_windows, logger from glances.plugins.glances_plugin import GlancesPlugin @@ -41,12 +41,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 1 - # Enter -1 to diplay bottom - self.line_curse = 4 # Note: 'glances_processes' is already init in the glances_processes.py script @@ -63,10 +57,9 @@ class Plugin(GlancesPlugin): # Update stats using the standard system lib # Note: Update is done in the processcount plugin # Just return the processes list - self.stats = glances_processes.getlist() + self.stats = glances_processes.getlist() elif self.get_input() == 'snmp': - # Update stats using SNMP - # !!! TODO + # No SNMP grab for processes pass return self.stats @@ -81,14 +74,10 @@ class Plugin(GlancesPlugin): return ret # Compute the sort key - try: - args.process_sorted_by - except AttributeError: - args.process_sorted_by = glances_processes.getsortkey() - if args.process_sorted_by == 'auto': - process_sort_key = glances_processes.getsortkey() + if glances_processes.getmanualsortkey() is None: + process_sort_key = glances_processes.getautosortkey() else: - process_sort_key = args.process_sorted_by + process_sort_key = glances_processes.getmanualsortkey() sort_style = 'SORT' # Header @@ -111,9 +100,9 @@ class Plugin(GlancesPlugin): msg = '{0:>9}'.format(_("TIME+")) ret.append(self.curse_add_line(msg, optional=True)) msg = '{0:>6}'.format(_("IOR/s")) - ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True)) + ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True)) msg = '{0:>6}'.format(_("IOW/s")) - ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True)) + ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True)) msg = ' {0:8}'.format(_("Command")) ret.append(self.curse_add_line(msg)) @@ -121,45 +110,72 @@ class Plugin(GlancesPlugin): tag_proc_time = True # Loop over processes (sorted by the sort key previously compute) + first = True for p in self.sortlist(process_sort_key): ret.append(self.curse_new_line()) # CPU - msg = '{0:>6.1f}'.format(p['cpu_percent']) - ret.append(self.curse_add_line(msg, - self.get_alert(p['cpu_percent'], header="cpu"))) + if 'cpu_percent' in p and p['cpu_percent'] is not None and p['cpu_percent'] != '': + msg = '{0:>6.1f}'.format(p['cpu_percent']) + ret.append(self.curse_add_line(msg, + self.get_alert(p['cpu_percent'], header="cpu"))) + else: + msg = '{0:>6}'.format('?') + ret.append(self.curse_add_line(msg)) # MEM - msg = '{0:>6.1f}'.format(p['memory_percent']) - ret.append(self.curse_add_line(msg, - self.get_alert(p['memory_percent'], header="mem"))) - # VMS - msg = '{0:>6}'.format(self.auto_unit(p['memory_info'][1], low_precision=False)) - ret.append(self.curse_add_line(msg, optional=True)) - # RSS - msg = '{0:>6}'.format(self.auto_unit(p['memory_info'][0], low_precision=False)) - ret.append(self.curse_add_line(msg, optional=True)) + if 'memory_percent' in p and p['memory_percent'] is not None and p['memory_percent'] != '': + msg = '{0:>6.1f}'.format(p['memory_percent']) + ret.append(self.curse_add_line(msg, + self.get_alert(p['memory_percent'], header="mem"))) + else: + msg = '{0:>6}'.format('?') + ret.append(self.curse_add_line(msg)) + # VMS/RSS + if 'memory_info' in p and p['memory_info'] is not None and p['memory_info'] != '': + # VMS + msg = '{0:>6}'.format(self.auto_unit(p['memory_info'][1], low_precision=False)) + ret.append(self.curse_add_line(msg, optional=True)) + # RSS + msg = '{0:>6}'.format(self.auto_unit(p['memory_info'][0], low_precision=False)) + ret.append(self.curse_add_line(msg, optional=True)) + else: + msg = '{0:>6}'.format('?') + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_add_line(msg)) # PID msg = '{0:>6}'.format(p['pid']) ret.append(self.curse_add_line(msg)) # USER - # docker internal users are displayed as ints only, therefore str() - msg = ' {0:9}'.format(str(p['username'])[:9]) - ret.append(self.curse_add_line(msg)) - # NICE - nice = p['nice'] - if nice is None: - nice = '?' - msg = '{0:>5}'.format(nice) - if isinstance(nice, int) and ((is_windows and nice != 32) or - (not is_windows and nice != 0)): - ret.append(self.curse_add_line(msg, decoration='NICE')) + if 'username' in p: + # docker internal users are displayed as ints only, therefore str() + msg = ' {0:9}'.format(str(p['username'])[:9]) + ret.append(self.curse_add_line(msg)) else: + msg = ' {0:9}'.format('?') + ret.append(self.curse_add_line(msg)) + # NICE + if 'nice' in p: + nice = p['nice'] + if nice is None: + nice = '?' + msg = '{0:>5}'.format(nice) + if isinstance(nice, int) and ((is_windows and nice != 32) or + (not is_windows and nice != 0)): + ret.append(self.curse_add_line(msg, decoration='NICE')) + else: + ret.append(self.curse_add_line(msg)) + else: + msg = '{0:>5}'.format('?') ret.append(self.curse_add_line(msg)) # STATUS - status = p['status'] - msg = '{0:>2}'.format(status) - if status == 'R': - ret.append(self.curse_add_line(msg, decoration='STATUS')) + if 'status' in p: + status = p['status'] + msg = '{0:>2}'.format(status) + if status == 'R': + ret.append(self.curse_add_line(msg, decoration='STATUS')) + else: + ret.append(self.curse_add_line(msg)) else: + msg = '{0:>2}'.format('?') ret.append(self.curse_add_line(msg)) # TIME+ if tag_proc_time: @@ -185,29 +201,30 @@ class Plugin(GlancesPlugin): msg = '{0:>6}'.format("0") else: msg = '{0:>6}'.format(self.auto_unit(io_rs, low_precision=False)) - ret.append(self.curse_add_line(msg, optional=True)) + ret.append(self.curse_add_line(msg, optional=True, additional=True)) # IO write io_ws = (p['io_counters'][1] - p['io_counters'][3]) / p['time_since_update'] if io_ws == 0: msg = '{0:>6}'.format("0") else: msg = '{0:>6}'.format(self.auto_unit(io_ws, low_precision=False)) - ret.append(self.curse_add_line(msg, optional=True)) + ret.append(self.curse_add_line(msg, optional=True, additional=True)) else: msg = '{0:>6}'.format("?") - ret.append(self.curse_add_line(msg, optional=True)) - ret.append(self.curse_add_line(msg, optional=True)) + ret.append(self.curse_add_line(msg, optional=True, additional=True)) + ret.append(self.curse_add_line(msg, optional=True, additional=True)) + # Command line # If no command line for the process is available, fallback to # the bare process name instead cmdline = p['cmdline'] - if cmdline == "": + if cmdline == "" or args.process_short_name: msg = ' {0}'.format(p['name']) ret.append(self.curse_add_line(msg, splittable=True)) else: try: cmd = cmdline.split()[0] - args = ' '.join(cmdline.split()[1:]) + argument = ' '.join(cmdline.split()[1:]) path, basename = os.path.split(cmd) if os.path.isdir(path): msg = ' {0}'.format(path) + os.sep @@ -216,11 +233,83 @@ class Plugin(GlancesPlugin): else: msg = ' {0}'.format(basename) ret.append(self.curse_add_line(msg, decoration='PROCESS', splittable=True)) - msg = " {0}".format(args) + msg = " {0}".format(argument) ret.append(self.curse_add_line(msg, splittable=True)) except UnicodeEncodeError: ret.append(self.curse_add_line("", splittable=True)) + # Add extended stats but only for the top processes + # !!! CPU consumption ??? + # TODO: extended stats into the web interface + if first and 'extended_stats' in p: + # Left padding + xpad = ' ' * 13 + # First line is CPU affinity + if 'cpu_affinity' in p and p['cpu_affinity'] is not None: + ret.append(self.curse_new_line()) + msg = xpad + _('CPU affinity: ') + str(len(p['cpu_affinity'])) + _(' cores') + ret.append(self.curse_add_line(msg, splittable=True)) + # Second line is memory info + if 'memory_info_ex' in p and p['memory_info_ex'] is not None: + ret.append(self.curse_new_line()) + msg = xpad + _('Memory info: ') + for k, v in p['memory_info_ex']._asdict().items(): + # Ignore rss and vms (already displayed) + if k not in ['rss', 'vms'] and v is not None: + msg += k + ' ' + self.auto_unit(v, low_precision=False) + ' ' + if 'memory_swap' in p and p['memory_swap'] is not None: + msg += _('swap ') + self.auto_unit(p['memory_swap'], low_precision=False) + ret.append(self.curse_add_line(msg, splittable=True)) + # Third line is for openned files/network sessions + ret.append(self.curse_new_line()) + msg = xpad + _('Openned: ') + if 'num_threads' in p and p['num_threads'] is not None: + msg += _('threads ') + str(p['num_threads']) + ' ' + if 'num_fds' in p and p['num_fds'] is not None: + msg += _('files ') + str(p['num_fds']) + ' ' + if 'num_handles' in p and p['num_handles'] is not None: + msg += _('handles ') + str(p['num_handles']) + ' ' + if 'tcp' in p and p['tcp'] is not None: + msg += _('TCP ') + str(p['tcp']) + ' ' + if 'udp' in p and p['udp'] is not None: + msg += _('UDP ') + str(p['udp']) + ' ' + ret.append(self.curse_add_line(msg, splittable=True)) + # Fouth line is IO nice level (only Linux and Windows OS) + if 'ionice' in p and p['ionice'] is not None: + ret.append(self.curse_new_line()) + msg = xpad + _('IO nice: ') + k = _('Class is ') + v = p['ionice'].ioclass + # Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle. + # Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low). + if is_windows: + if v == 0: + msg += k + 'Very Low' + elif v == 1: + msg += k + 'Low' + elif v == 2: + msg += _('No specific I/O priority') + else: + msg += k + str(v) + else: + if v == 0: + msg += _('No specific I/O priority') + elif v == 1: + msg += k + 'Real Time' + elif v == 2: + msg += k + 'Best Effort' + elif v == 3: + msg += k + 'IDLE' + else: + msg += k + str(v) + # value is a number which goes from 0 to 7. + # The higher the value, the lower the I/O priority of the process. + if hasattr(p['ionice'], 'value') and p['ionice'].value != 0: + msg += _(' (value %s/7)') % str(p['ionice'].value) + ret.append(self.curse_add_line(msg, splittable=True)) + # End of extended stats + first = False + # Return the message with decoration return ret @@ -250,9 +339,14 @@ class Plugin(GlancesPlugin): reverse=sortedreverse) else: # Others sorts - listsorted = sorted(self.stats, - key=lambda process: process[sortedby], - reverse=sortedreverse) + try: + listsorted = sorted(self.stats, + key=lambda process: process[sortedby], + reverse=sortedreverse) + except KeyError: + listsorted = sorted(self.stats, + key=lambda process: process['name'], + reverse=False) self.stats = listsorted diff --git a/glances/plugins/glances_sensors.py b/glances/plugins/glances_sensors.py index 5380445d..171b054d 100644 --- a/glances/plugins/glances_sensors.py +++ b/glances/plugins/glances_sensors.py @@ -27,7 +27,7 @@ except ImportError: pass # Import Glances lib -from glances.core.glances_globals import is_py3 +from glances.core.glances_globals import is_py3, logger from glances.plugins.glances_batpercent import Plugin as BatPercentPlugin from glances.plugins.glances_hddtemp import Plugin as HddTempPlugin from glances.plugins.glances_plugin import GlancesPlugin @@ -50,19 +50,13 @@ class Plugin(GlancesPlugin): self.glancesgrabsensors = GlancesGrabSensors() # Instance for the HDDTemp Plugin in order to display the hard disks temperatures - self.hddtemp_plugin = HddTempPlugin() + self.hddtemp_plugin = HddTempPlugin(args=args) # Instance for the BatPercent in order to display the batteries capacities - self.batpercent_plugin = BatPercentPlugin() + self.batpercent_plugin = BatPercentPlugin(args=args) # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 5 # Init the stats self.reset() @@ -140,18 +134,24 @@ class Plugin(GlancesPlugin): ret.append(self.curse_add_line(msg)) for item in self.stats: - # New line - ret.append(self.curse_new_line()) - msg = '{0:18}'.format(item['label'][:18]) - ret.append(self.curse_add_line(msg)) - msg = '{0:>5}'.format(item['value']) - if item['type'] == 'battery': - try: - ret.append(self.curse_add_line(msg, self.get_alert(100 - item['value'], header=item['type']))) - except TypeError: - pass - else: - ret.append(self.curse_add_line(msg, self.get_alert(item['value'], header=item['type']))) + if item['value'] is not None and item['value'] != []: + # New line + ret.append(self.curse_new_line()) + # Alias for the lable name ? + label = self.has_alias(item['label'].lower()) + if label is None: + label = item['label'] + label = label[:18] + msg = '{0:18}'.format(label) + ret.append(self.curse_add_line(msg)) + msg = '{0:>5}'.format(item['value']) + if item['type'] == 'battery': + try: + ret.append(self.curse_add_line(msg, self.get_alert(100 - item['value'], header=item['type']))) + except TypeError: + pass + else: + ret.append(self.curse_add_line(msg, self.get_alert(item['value'], header=item['type']))) return ret diff --git a/glances/plugins/glances_system.py b/glances/plugins/glances_system.py index d51ace3c..5f789aec 100644 --- a/glances/plugins/glances_system.py +++ b/glances/plugins/glances_system.py @@ -22,13 +22,25 @@ # Import system libs import os import platform +import re # Import Glances libs from glances.plugins.glances_plugin import GlancesPlugin # SNMP OID -snmp_oid = {'hostname': '1.3.6.1.2.1.1.5.0', - 'os_name': '1.3.6.1.2.1.1.1.0'} +snmp_oid = {'default': {'hostname': '1.3.6.1.2.1.1.5.0', + 'system_name': '1.3.6.1.2.1.1.1.0'}} + +# SNMP to human read +# Dict (key: OS short name) of dict (reg exp OID to human) +# Windows: http://msdn.microsoft.com/en-us/library/windows/desktop/ms724832%28v=vs.85%29.aspx +snmp_to_human = {'windows': {'Windows Version 6.3': 'Windows 8.1 or Server 2012R2', + 'Windows Version 6.2': 'Windows 8 or Server 2012', + 'Windows Version 6.1': 'Windows 7 or Server 2008R2', + 'Windows Version 6.0': 'Windows Vista or Server 2008', + 'Windows Version 5.2': 'Windows XP 64bits or 2003 server', + 'Windows Version 5.1': 'Windows XP', + 'Windows Version 5.0': 'Windows 2000'}} class Plugin(GlancesPlugin): @@ -44,12 +56,6 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True - # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = 0 - # Enter -1 to diplay bottom - self.line_curse = 0 # Init the stats self.reset() @@ -90,7 +96,18 @@ class Plugin(GlancesPlugin): self.stats['os_version'] = "" elif self.get_input() == 'snmp': # Update stats using SNMP - self.stats = self.set_stats_snmp(snmp_oid=snmp_oid) + try: + self.stats = self.set_stats_snmp(snmp_oid=snmp_oid[self.get_short_system_name()]) + except KeyError: + self.stats = self.set_stats_snmp(snmp_oid=snmp_oid['default']) + # Default behavor: display all the information + self.stats['os_name'] = self.stats['system_name'] + # Windows OS tips + if self.get_short_system_name() == 'windows': + for r,v in snmp_to_human['windows'].iteritems(): + if re.search(r, self.stats['system_name']): + self.stats['os_name'] = v + break return self.stats diff --git a/glances/plugins/glances_uptime.py b/glances/plugins/glances_uptime.py index 85a59beb..ad268538 100644 --- a/glances/plugins/glances_uptime.py +++ b/glances/plugins/glances_uptime.py @@ -45,12 +45,10 @@ class Plugin(GlancesPlugin): # We want to display the stat in the curse interface self.display_curse = True + # Set the message position - # It is NOT the curse position but the Glances column/line - # Enter -1 to right align - self.column_curse = -1 - # Enter -1 to diplay bottom - self.line_curse = 0 + self.set_align('right') + # Init the stats self.reset() diff --git a/i18n-gen.sh b/i18n-gen.sh index c55fdd5f..a530281e 100755 --- a/i18n-gen.sh +++ b/i18n-gen.sh @@ -34,13 +34,13 @@ function gen_pot() { xgettext --language=Python --keyword=_ --output=${ROOT}i18n/glances.pot `find ${ROOT}glances/ -name "*.py"` } -OPERATION="$1" -shift - -if [ -z "$1" ]; then +if [ $# != 2 ]; then usage fi +OPERATION="$1" +shift + case "$OPERATION" in init) # If there is already a language file for specified language there is no need to generate a new one diff --git a/man/glances.1 b/man/glances.1 index bd26c265..c2389d83 100644 --- a/man/glances.1 +++ b/man/glances.1 @@ -1,4 +1,4 @@ -.TH glances 1 "June, 2014" "version 2.0.1" "USER COMMANDS" +.TH glances 1 "September, 2014" "version 2.1" "USER COMMANDS" .SH NAME glances \- A cross-platform curses-based system monitoring tool .SH SYNOPSIS @@ -25,18 +25,18 @@ display the help and exit .B \-V, \-\-version show program's version number and exit .TP +.B \-d, \-\-debug +Enable debug mode (log file is /tmp/glances.log) +.TP .B \-b, \-\-byte display network rate in byte per second [default: bit per second] .TP -.B \-B BIND_ADDRESS, \-\-bind BIND_ADDRESS -bind server to the given IPv4/IPv6 address or hostname -.TP -.B \-c CLIENT, \-\-client CLIENT -connect to a Glances server by IPv4/IPv6 address or hostname -.TP .B \-C CONF_FILE, \-\-config CONF_FILE path to the configuration file .TP +.B \-\-enable-history +enable the history mode +.TP .B \-\-disable-bold disable bold mode in the terminal .TP @@ -58,20 +58,29 @@ disable sensors module .B \-\-disable-process disable process module .TP +.B \-\-disable-process-extended +disable extended stats on top process +.TP .B \-\-disable-log disable log module .TP .B \-\-output-csv OUTPUT_CSV export stats to a CSV file .TP +.B \-s, \-\-server +run Glances in server mode +.TP +.B \-c CLIENT, \-\-client CLIENT +connect to a Glances server by IPv4/IPv6 address or hostname +.TP .B \-p PORT, \-\-port PORT define the client/server TCP port [default: 61209] .TP .B \-\-password define a client/server password from the prompt or file .TP -.B \-s, \-\-server -run Glances in server mode +.B \-B BIND_ADDRESS, \-\-bind BIND_ADDRESS +bind server to the given IPv4/IPv6 address or hostname .TP .B \-\-snmp-community SNMP_COMMUNITY SNMP community @@ -88,6 +97,9 @@ SNMP username (only for SNMPv3) .B \-\-snmp-auth SNMP_AUTH SNMP authentication key (only for SNMPv3) .TP +.B \-\-snmp-force +Force the SNMP mode (do not try Glances server) +.TP .B \-t TIME, \-\-time TIME set refresh time in seconds [default: 3 sec] .TP @@ -96,9 +108,18 @@ run Glances in Web server mode .TP .B \-1, \-\-percpu start Glances in per CPU mode +.TP +.B \-1, \-\-process-short-name +Force short name for processes name +.TP +.B \-1, \-\-theme-white +Optimize display for white background .SH INTERACTIVE COMMANDS You can use the following keys while in Glances: .TP +.B ENTER +Set the process filter patern (as a regular expression) +.TP .B a Sort process list automatically .TP @@ -111,8 +132,13 @@ Sort processes by CPU usage .B d Show/hide disk I/O stats .TP +.B e +Enable/disable top extended stats +.TP .B f Show/hide file system stats +.B g +Generate graphs for current history .TP .B h Show/hide the help screen @@ -135,6 +161,9 @@ Sort processes by name .B q Quit .TP +.B r +Reset history +.TP .B s Show/hide sensors stats .TP diff --git a/setup.py b/setup.py index ba2759a9..1ef63962 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,6 @@ import sys from setuptools import setup - def get_data_files(): data_files = [ ('share/doc/glances', ['AUTHORS', 'COPYING', 'NEWS', 'README.rst', @@ -30,7 +29,6 @@ def get_data_files(): return data_files - def get_requires(): requires = ['psutil>=2.0.0'] if sys.platform.startswith('win'): @@ -42,13 +40,13 @@ def get_requires(): setup( name='Glances', - version='2.0.1', + version='2.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.0.1.tar.gz', + # download_url='https://s3.amazonaws.com/glances/glances-2.1.tar.gz', license="LGPL", keywords="cli curses monitoring system", install_requires=get_requires(), @@ -56,7 +54,8 @@ setup( 'WEB': ['bottle'], 'SENSORS': ['py3sensors'], 'BATINFO': ['batinfo'], - 'SNMP': ['pysnmp'] + 'SNMP': ['pysnmp'], + 'CHART': ['matplotlib'] }, packages=['glances'], include_package_data=True, diff --git a/unitest-restful.py b/unitest-restful.py new file mode 100755 index 00000000..6f413c8f --- /dev/null +++ b/unitest-restful.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Glances - An eye on your system +# +# Copyright (C) 2014 Nicolargo +# +# 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 . + +"""Glances unitary tests suite for the RESTFul API.""" + +import gettext +import locale +import sys +import time +import unittest +import shlex +import subprocess +import requests +import json +import types + +from glances.core.glances_globals import ( + appname, + is_linux, + version +) + +SERVER_PORT = 61234 +URL = "http://localhost:%s/api/2" % SERVER_PORT +pid = None + +# Global variables +# ================= + +# Unitary test is only available from a GNU/Linus machine +if not is_linux: + print('ERROR: RESTFul API unitaries tests should be ran on GNU/Linux operating system') + sys.exit(2) +else: + print('Unitary tests for {0} {1}'.format(appname, version)) + +# Import local settings +from glances.core.glances_globals import gettext_domain, locale_dir +locale.setlocale(locale.LC_ALL, '') +gettext.install(gettext_domain, locale_dir) + +# Init Glances core +from glances.core.glances_main import GlancesMain +core = GlancesMain() +if not core.is_standalone(): + print('ERROR: Glances core should be ran in standalone mode') + sys.exit(1) + +# Init Glances stats +from glances.core.glances_stats import GlancesStats +stats = GlancesStats() + + +# Unitest class +# ============== + +class TestGlances(unittest.TestCase): + + """Test Glances class.""" + + def setUp(self): + """The function is called *every time* before test_*.""" + print('\n' + '=' * 78) + + def test_000_start_server(self): + """Start the Glances Web Server""" + print('INFO: [TEST_000] Start the Glances Web Server') + + global pid + + cmdline = "/usr/bin/python -m glances -w -p %s" % SERVER_PORT + print("Run the Glances Web Server on port %s" % SERVER_PORT) + args = shlex.split(cmdline) + pid = subprocess.Popen(args) + print("Please wait...") + time.sleep(1) + + self.assertTrue(pid is not None) + + def test_001_all(self): + """All""" + method = "all" + print('INFO: [TEST_001] Connection test') + + print("HTTP RESTFul request: %s/%s" % (URL, method)) + req = requests.get("%s/%s" % (URL, method)) + + self.assertTrue(req.ok) + + def test_002_pluginslist(self): + """Plugins list""" + method = "pluginslist" + print('INFO: [TEST_002] Plugins list') + + print("HTTP RESTFul request: %s/%s" % (URL, method)) + req = requests.get("%s/%s" % (URL, method)) + + self.assertTrue(req.ok) + self.assertIsInstance(req.json(), types.ListType) + self.assertIn('cpu', req.json()) + + def test_003_plugins(self): + """Plugins""" + method = "pluginslist" + print('INFO: [TEST_003] Plugins') + + plist = requests.get("%s/%s" % (URL, method)) + + print plist.json() + + for p in plist.json(): + print("HTTP RESTFul request: %s/%s" % (URL, p)) + req = requests.get("%s/%s" % (URL, p)) + self.assertTrue(req.ok) + if p in ('uptime', 'now'): + self.assertIsInstance(req.json(), types.UnicodeType) + elif p in ('fs', 'monitor', 'percpu', 'sensors', 'alert', 'processlist', 'diskio', 'hddtemp', 'batpercent', 'network'): + self.assertIsInstance(req.json(), types.ListType) + elif p in ('psutilversion', 'help'): + pass + else: + self.assertIsInstance(req.json(), types.DictType) + + def test_004_items(self): + """Items""" + method = "cpu" + print('INFO: [TEST_004] Items for the CPU method') + + ilist = requests.get("%s/%s" % (URL, method)) + + for i in ilist.json(): + print("HTTP RESTFul request: %s/%s/%s" % (URL, method,i)) + req = requests.get("%s/%s/%s" % (URL, method, i)) + self.assertTrue(req.ok) + self.assertIsInstance(req.json(), types.DictType) + self.assertIsInstance(req.json()[i], types.FloatType) + + def test_005_values(self): + """Valuess""" + method = "processlist" + print('INFO: [TEST_005] Item=Value for the PROCESSLIST method') + + print("%s/%s/pid/0" % (URL, method)) + req = requests.get("%s/%s/pid/0" % (URL, method)) + + self.assertTrue(req.ok) + self.assertIsInstance(req.json(), types.DictType) + + def test_999_stop_server(self): + """Stop the Glances Web Server""" + print('INFO: [TEST_999] Stop the Glances Web Server') + + print("Stop the Glances Web Server") + pid.terminate() + print("Please wait...") + time.sleep(1) + + self.assertTrue(True) + +if __name__ == '__main__': + unittest.main() diff --git a/unitest-xmlrpc.py b/unitest-xmlrpc.py new file mode 100755 index 00000000..d1e56d4f --- /dev/null +++ b/unitest-xmlrpc.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Glances - An eye on your system +# +# Copyright (C) 2014 Nicolargo +# +# 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 . + +"""Glances unitary tests suite for the XML/RPC API.""" + +import gettext +import locale +import sys +import time +import unittest +import shlex +import subprocess +import xmlrpclib +import json +import types + +from glances.core.glances_globals import ( + appname, + is_linux, + version +) + +SERVER_PORT = 61234 +URL = "http://localhost:%s" % SERVER_PORT +pid = None + +# Global variables +# ================= + +# Unitary test is only available from a GNU/Linus machine +if not is_linux: + print('ERROR: XML/RPC API unitaries tests should be ran on GNU/Linux operating system') + sys.exit(2) +else: + print('Unitary tests for {0} {1}'.format(appname, version)) + +# Import local settings +from glances.core.glances_globals import gettext_domain, locale_dir +locale.setlocale(locale.LC_ALL, '') +gettext.install(gettext_domain, locale_dir) + +# Init Glances core +from glances.core.glances_main import GlancesMain +core = GlancesMain() +if not core.is_standalone(): + print('ERROR: Glances core should be ran in standalone mode') + sys.exit(1) + +# Init Glances stats +from glances.core.glances_stats import GlancesStats +stats = GlancesStats() + +# Init the XML/RCP client +client = xmlrpclib.ServerProxy(URL) + +# Unitest class +# ============== + +class TestGlances(unittest.TestCase): + + """Test Glances class.""" + + def setUp(self): + """The function is called *every time* before test_*.""" + print('\n' + '=' * 78) + + def test_000_start_server(self): + """Start the Glances Web Server""" + print('INFO: [TEST_000] Start the Glances Web Server') + + global pid + + cmdline = "/usr/bin/python -m glances -s -p %s" % SERVER_PORT + print("Run the Glances Server on port %s" % SERVER_PORT) + args = shlex.split(cmdline) + pid = subprocess.Popen(args) + print("Please wait...") + time.sleep(1) + + self.assertTrue(pid is not None) + + def test_001_all(self): + """All""" + method = "getAll()" + print('INFO: [TEST_001] Connection test') + + print("XML/RPC request: %s" % method) + req = json.loads(client.getAll()) + + self.assertIsInstance(req, types.DictType) + + def test_002_pluginslist(self): + """Plugins list""" + method = "getAllPlugins()" + print('INFO: [TEST_002] Get plugins list') + + print("XML/RPC request: %s" % method) + req = json.loads(client.getAllPlugins()) + + self.assertIsInstance(req, types.ListType) + + def test_003_system(self): + """System""" + method = "getSystem()" + print('INFO: [TEST_003] Method: %s' % method) + + req = json.loads(client.getSystem()) + + self.assertIsInstance(req, types.DictType) + + def test_004_cpu(self): + """CPU""" + method = "getCpu(), getPerCpu(), getLoad() and getCore()" + print('INFO: [TEST_004] Method: %s' % method) + + req = json.loads(client.getCpu()) + self.assertIsInstance(req, types.DictType) + + req = json.loads(client.getPerCpu()) + self.assertIsInstance(req, types.ListType) + + req = json.loads(client.getLoad()) + self.assertIsInstance(req, types.DictType) + + req = json.loads(client.getCore()) + self.assertIsInstance(req, types.DictType) + + def test_005_mem(self): + """MEM""" + method = "getMem() and getMemSwap()" + print('INFO: [TEST_005] Method: %s' % method) + + req = json.loads(client.getMem()) + self.assertIsInstance(req, types.DictType) + + req = json.loads(client.getMemSwap()) + self.assertIsInstance(req, types.DictType) + + def test_006_net(self): + """NETWORK""" + method = "getNetwork()" + print('INFO: [TEST_006] Method: %s' % method) + + req = json.loads(client.getNetwork()) + self.assertIsInstance(req, types.ListType) + + def test_007_disk(self): + """DISK""" + method = "getFs() and getDiskIO()" + print('INFO: [TEST_007] Method: %s' % method) + + req = json.loads(client.getFs()) + self.assertIsInstance(req, types.ListType) + + req = json.loads(client.getDiskIO()) + self.assertIsInstance(req, types.ListType) + + def test_008_sensors(self): + """SENSORS""" + method = "getSensors()" + print('INFO: [TEST_008] Method: %s' % method) + + req = json.loads(client.getSensors()) + self.assertIsInstance(req, types.ListType) + + def test_009_process(self): + """PROCESS""" + method = "getProcessCount() and getProcessList()" + print('INFO: [TEST_009] Method: %s' % method) + + req = json.loads(client.getProcessCount()) + self.assertIsInstance(req, types.DictType) + + req = json.loads(client.getProcessList()) + self.assertIsInstance(req, types.ListType) + + def test_999_stop_server(self): + """Stop the Glances Web Server""" + print('INFO: [TEST_999] Stop the Glances Server') + + print("Stop the Glances Server") + pid.terminate() + print("Please wait...") + time.sleep(1) + + self.assertTrue(True) + +if __name__ == '__main__': + unittest.main()