Merge branch 'develop' into issue2183

This commit is contained in:
nicolargo 2023-12-21 15:53:27 +01:00
commit 70250981b9
134 changed files with 9239 additions and 8777 deletions

View File

@ -44,16 +44,16 @@ jobs:
--outdir dist/
- name: Publish distribution package to Test PyPI
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
skip_existing: true
repository-url: https://test.pypi.org/legacy/
skip-existing: true
- name: Publish distribution package to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

View File

@ -80,8 +80,11 @@ test-min: ## Run unit tests in minimal environment
test-min-with-upgrade: venv-min-upgrade ## Upgrade deps and run unit tests in minimal environment
./venv-min/bin/python ./unitest.py
test-restful-api: ## Run unit tests of the RESTful API
./venv/bin/python ./unitest-restful.py
# ===================================================================
# Linters and profilers
# Linters, profilers and cyber security
# ===================================================================
format: ## Format the code
@ -99,7 +102,7 @@ codespell: ## Run codespell to fix common misspellings in text files
./venv-dev/bin/codespell -S .git,./docs/_build,./Glances.egg-info,./venv*,./glances/outputs,*.svg -L hart,bu,te,statics
semgrep: ## Run semgrep to find bugs and enforce code standards
./venv-dev/bin/semgrep --config=auto --lang python --use-git-ignore ./glances
./venv-dev/bin/semgrep scan --config=auto
profiling: ## How to start the profiling of the Glances software
@echo "Please complete and run: sudo ./venv-dev/bin/py-spy record -o ./docs/_static/glances-flame.svg -d 60 -s --pid <GLANCES PID>"
@ -123,6 +126,10 @@ memory-profiling: ## Profile memory usage
./venv-dev/bin/mprof plot --output ./docs/_static/glances-memory-profiling-without-history.png
rm -f mprofile_*.dat
# Trivy installation: https://aquasecurity.github.io/trivy/latest/getting-started/installation/
trivy: ## Run Trivy to find vulnerabilities in container images
trivy fs .
# ===================================================================
# Docs
# ===================================================================

View File

@ -8,6 +8,17 @@ Version 4.0.0
Under development: https://github.com/nicolargo/glances/issues?q=is%3Aopen+is%3Aissue+milestone%3A%22Glances+4.0.0%22
**BREAKING CHANGES:**
* The Glances API version 3 is replaced by the version 4. So Restfull API URL is now /api/4/ #2610
* Alias definition change in the configuration file #1735
Glances version 3.x and lower:
sda1_alias=InternalDisk
sdb1_alias=ExternalDisk
Glances version 4.x and higher:
alias=sda1:InternalDisk,sdb1:ExternalDisk
* Alias can now be used to redefine FS name #1735
===============
Version 3.4.0.3
===============
@ -899,7 +910,7 @@ Processes list Nice value:
[processlist]
# Nice priorities range from -20 to 19.
# Configure nice levels using a comma separated list.
# Configure nice levels using a comma-separated list.
#
# Nice: Example 1, non-zero is warning (default behavior)
nice_warning=-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19

View File

@ -92,17 +92,19 @@ Optional dependencies:
- ``batinfo`` (for battery monitoring)
- ``bernhard`` (for the Riemann export module)
- ``bottle`` (for Web server mode)
- ``cassandra-driver`` (for the Cassandra export module)
- ``chevron`` (for the action script feature)
- ``docker`` (for the Containers Docker monitoring support)
- ``elasticsearch`` (for the Elastic Search export module)
- ``FastAPI`` and ``Uvicorn`` (for Web server mode)
- ``graphitesender`` (For the Graphite export module)
- ``hddtemp`` (for HDD temperature monitoring support) [Linux-only]
- ``influxdb`` (for the InfluxDB version 1 export module)
- ``influxdb-client`` (for the InfluxDB version 2 export module)
- ``jinja2`` (for templating, used under the hood by FastAPI)
- ``kafka-python`` (for the Kafka export module)
- ``netifaces`` (for the IP plugin)
- ``orjson`` (fast JSON library, used under the hood by FastAPI)
- ``py3nvml`` (for the GPU plugin)
- ``pycouchdb`` (for the CouchDB export module)
- ``pika`` (for the RabbitMQ/ActiveMQ export module)
@ -207,10 +209,10 @@ Get the Glances container:
The following tags are availables:
- *latest-full* for a full Alpine Glances image (latest release) with all dependencies
- *latest* for a basic Alpine Glances (latest release) version with minimal dependencies (Bottle and Docker)
- *latest* for a basic Alpine Glances (latest release) version with minimal dependencies (FastAPI and Docker)
- *dev* for a basic Alpine Glances image (based on development branch) with all dependencies (Warning: may be instable)
- *ubuntu-latest-full* for a full Ubuntu Glances image (latest release) with all dependencies
- *ubuntu-latest* for a basic Ubuntu Glances (latest release) version with minimal dependencies (Bottle and Docker)
- *ubuntu-latest* for a basic Ubuntu Glances (latest release) version with minimal dependencies (FastAPI and Docker)
- *ubuntu-dev* for a basic Ubuntu Glances image (based on development branch) with all dependencies (Warning: may be instable)
Run last version of Glances container in *console mode*:
@ -319,7 +321,7 @@ Start Termux on your device and enter:
$ apt update
$ apt upgrade
$ apt install clang python
$ pip install bottle
$ pip install fastapi uvicorn orjson jinja2
$ pip install glances
And start Glances:

View File

@ -23,7 +23,7 @@ history_size=1200
# Theme name for the Curses interface: black or white
curse_theme=black
# Limit the number of processes to display (for the WebUI)
max_processes_display=30
max_processes_display=25
# Set the URL prefix (for the WebUI and the API)
# Example: url_prefix=/glances/ => http://localhost/glances/
# The final / is mandatory
@ -167,8 +167,6 @@ tx_critical=90
#hide=docker.*,lo
# Define the list of wireless network interfaces to be show (comma-separated)
#show=docker.*
# WLAN 0 alias
#wlan0_alias=Wireless
# It is possible to overwrite the bitrate thresholds per interface
# WLAN 0 Default limits (in bits per second aka bps) for interface bitrate
#wlan0_rx_careful=4000000
@ -179,6 +177,8 @@ tx_critical=90
#wlan0_tx_warning=900000
#wlan0_tx_critical=1000000
#wlan0_tx_log=True
# Alias for network interface name
alias=wlp2s0:WIFI
[ip]
disable=False
@ -218,8 +218,8 @@ disable=False
hide=loop.*,/dev/loop.*
# Define the list of disks to be show (comma-separated)
#show=sda.*
# Alias for sda1
#sda1_alias=InternalDisk
# Alias for sda1 and sdb1
alias=sda1:InternalDisk,sdb1:ExternalDisk
[fs]
disable=False
@ -236,6 +236,8 @@ warning=70
critical=90
# Allow additional file system types (comma-separated FS type)
#allow=shm
# Alias for root file system
alias=/:Root
[irq]
# Documentation: https://glances.readthedocs.io/en/latest/aoa/irq.html
@ -307,13 +309,7 @@ battery_careful=80
battery_warning=90
battery_critical=95
# Sensors alias
#temp1_alias=Motherboard 0
#temp2_alias=Motherboard 1
#core 0_temperature_core_alias=CPU Core 0 temp
#core 0_fans_speed_alias=CPU Core 0 fan
#or
#core 0_alias=CPU Core 0
#core 1_alias=CPU Core 1
#alias=core 0:CPU Core 0,core 1:CPU Core 1
[processcount]
disable=False
@ -336,7 +332,7 @@ mem_warning=70
mem_critical=90
#
# Nice priorities range from -20 to 19.
# Configure nice levels using a comma separated list.
# Configure nice levels using a comma-separated list.
#
# Nice: Example 1, non-zero is warning (default behavior)
nice_warning=-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
@ -394,10 +390,10 @@ port_default_gateway=True
[containers]
disable=False
# Only show specific containers (comma separated list of container name or regular expression)
# Only show specific containers (comma-separated list of container name or regular expression)
# Comment this line to display all containers (default configuration)
; show=telegraf
# Hide some containers (comma separated list of container name or regular expression)
# Hide some containers (comma-separated list of container name or regular expression)
# Comment this line to display all containers (default configuration)
; hide=telegraf
# Define the maximum docker size name (default is 20 chars)
@ -428,7 +424,7 @@ disable=False
[alert]
disable=False
# Maximum number of alerts to display (default is 10)
; max_events=10
;max_events=10
##############################################################################
# Client/server

View File

@ -1,3 +1,3 @@
FROM nicolargo/glances:latest as glancesminimal
FROM glances:local-alpine-minimal as glancesminimal
COPY glances.conf /glances/conf/glances.conf
CMD python -m glances -C /glances/conf/glances.conf $GLANCES_OPT

View File

@ -25,15 +25,16 @@ services:
- "/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro"
- "./glances.conf:/glances/conf/glances.conf"
environment:
- GLANCES_OPT: "-C /glances/conf/glances.conf -w"
- TZ: "${TZ}"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
- TZ=${TZ}
- "GLANCES_OPT=-C /glances/conf/glances.conf -w"
# Uncomment for GPU compatibilty (Nvidia) inside the container
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: 1
# capabilities: [gpu]
labels:
- "traefik.port=61208"
- "traefik.frontend.rule=Host:glances.docker.localhost"

View File

@ -13,12 +13,13 @@ services:
- "/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro"
- "./glances.conf:/glances/conf/glances.conf"
environment:
- GLANCES_OPT: "-C /glances/conf/glances.conf -w"
- TZ: "${TZ}"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
- TZ=${TZ}
- "GLANCES_OPT=-C /glances/conf/glances.conf -w"
# Uncomment for GPU compatibilty (Nvidia) inside the container
# deploy:
# resources:
# reservations:
# devices:
# - driver: nvidia
# count: 1
# capabilities: [gpu]

View File

@ -23,7 +23,7 @@ history_size=1200
# Theme name for the Curses interface: black or white
curse_theme=black
# Limit the number of processes to display (for the WebUI)
max_processes_display=30
max_processes_display=25
# Set the URL prefix (for the WebUI and the API)
# Example: url_prefix=/glances/ => http://localhost/glances/
# The final / is mandatory
@ -340,7 +340,7 @@ mem_warning=70
mem_critical=90
#
# Nice priorities range from -20 to 19.
# Configure nice levels using a comma separated list.
# Configure nice levels using a comma-separated list.
#
# Nice: Example 1, non-zero is warning (default behavior)
nice_warning=-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
@ -398,10 +398,10 @@ port_default_gateway=True
[containers]
disable=False
# Only show specific containers (comma separated list of container name or regular expression)
# Only show specific containers (comma-separated list of container name or regular expression)
# Comment this line to display all containers (default configuration)
; show=telegraf
# Hide some containers (comma separated list of container name or regular expression)
# Hide some containers (comma-separated list of container name or regular expression)
# Comment this line to display all containers (default configuration)
; hide=telegraf
# Define the maximum docker size name (default is 20 chars)

View File

@ -96,7 +96,7 @@ EXPOSE 61209 61208
# Define default command.
WORKDIR /app
CMD /venv/bin/python3 -m glances -C /etc/glances.conf $GLANCES_OPT
CMD /venv/bin/python3 -m glances $GLANCES_OPT
################################################################################
# RELEASE: minimal

View File

@ -90,7 +90,7 @@ EXPOSE 61209 61208
# Define default command.
WORKDIR /app
CMD /venv/bin/python3 -m glances -C /etc/glances.conf $GLANCES_OPT
CMD /venv/bin/python3 -m glances $GLANCES_OPT
################################################################################
# RELEASE: minimal

View File

@ -6,5 +6,5 @@ podman; python_version >= "3.6"
packaging; python_version >= "3.7"
python-dateutil
six
urllib3<2.0 # See issue https://github.com/nicolargo/glances/issues/2392
requests # See issue - https://github.com/nicolargo/glances/issues/2233
urllib3<2.0 # See issue https://github.com/nicolargo/glances/issues/2617
requests

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -21,11 +21,11 @@ under the ``[containers]`` section:
[containers]
disable=False
# Only show specific containers (comma separated list of container name or regular expression)
# Only show specific containers (comma-separated list of container name or regular expression)
show=thiscontainer,andthisone,andthoseones.*
# Hide some containers (comma separated list of container name or regular expression)
# Hide some containers (comma-separated list of container name or regular expression)
hide=donotshowthisone,andthose.*
# Show only specific containers (comma separated list of container name or regular expression)
# Show only specific containers (comma-separated list of container name or regular expression)
#show=showthisone,andthose.*
# Define the maximum containers size name (default is 20 chars)
max_name_size=20

View File

@ -190,7 +190,7 @@ In curses/standalone mode, you can select a process using ``UP`` and ``DOWN`` an
.. note::
Limit for CPU and MEM percent values can be overwritten in the
configuration file under the ``[processlist]`` section. It is also
possible to define limit for Nice values (comma separated list).
possible to define limit for Nice values (comma-separated list).
For example: nice_warning=-20,-19,-18
Accumulated per program — key 'j'

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ Command-Line Options
.. option:: -V, --version
show program's version number and exit
show the program's version number and exit
.. option:: -d, --debug
@ -22,25 +22,29 @@ Command-Line Options
path to the configuration file
.. option:: -P PLUGIN_DIRECTORY, --plugins PLUGIN_DIRECTORY
path to a directory containing additional plugins
.. option:: --modules-list
display modules (plugins & exports) list and exit
.. option:: --disable-plugin PLUGIN
disable PLUGIN (comma separated list)
disable PLUGIN (comma-separated list)
.. option:: --enable-plugin PLUGIN
enable PLUGIN (comma separated list)
enable PLUGIN (comma-separated list)
.. option:: --stdout PLUGINS_STATS
display stats to stdout (comma separated list of plugins/plugins.attribute)
display stats to stdout (comma-separated list of plugins/plugins.attribute)
.. option:: --export EXPORT
enable EXPORT module (comma separated list)
enable EXPORT module (comma-separated list)
.. option:: --export-csv-file EXPORT_CSV_FILE
@ -60,7 +64,7 @@ Command-Line Options
.. option:: --light, --enable-light
light mode for Curses UI (disable all but top menu)
light mode for Curses UI (disable all but the top menu)
.. option:: -0, --disable-irix
@ -84,7 +88,7 @@ Command-Line Options
.. option:: -5, --disable-top
disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
disable top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
.. option:: -6, --meangpu
@ -168,7 +172,7 @@ Command-Line Options
.. option:: -w, --webserver
run Glances in web server mode (bottle lib needed)
run Glances in web server mode (FastAPI lib needed)
.. option:: --cached-time CACHED_TIME
@ -192,11 +196,11 @@ Command-Line Options
.. option:: --hide-kernel-threads
hide kernel threads in process list (not available on Windows)
hide kernel threads in the process list (not available on Windows)
.. option:: -b, --byte
display network rate in byte per second
display network rate in bytes per second
.. option:: --diskio-show-ramfs
@ -216,11 +220,11 @@ Command-Line Options
.. option:: --theme-white
optimize display colors for white background
optimize display colors for a white background
.. option:: --disable-check-update
disable online Glances version ckeck
disable online Glances version check
Interactive Commands
--------------------
@ -232,7 +236,7 @@ The following commands (key pressed) are supported while in Glances:
.. note:: On macOS please use ``CTRL-H`` to delete filter.
Filter is a regular expression pattern:
The filter is a regular expression pattern:
- ``gnome``: matches all processes starting with the ``gnome``
string
@ -250,7 +254,7 @@ The following commands (key pressed) are supported while in Glances:
- If CPU iowait ``>60%``, sort processes by I/O read and write
``A``
Enable/disable Application Monitoring Process
Enable/disable the Application Monitoring Process
``b``
Switch between bit/s or Byte/s for network I/O
@ -274,7 +278,7 @@ The following commands (key pressed) are supported while in Glances:
Enable/disable top extended stats
``E``
Erase current process filter
Erase the current process filter
``f``
Show/hide file system and folder monitoring stats
@ -301,7 +305,7 @@ The following commands (key pressed) are supported while in Glances:
Increase selected process nice level / Lower the priority (need right) - Only in standalone mode.
``-``
Decrease selected process nice level / Higher the priority (need right) - Only in standalone mode.
Decrease selected process nice level / Higher the priority (need right) - Only in standalone mode.
``k``
Kill selected process (need right) - Only in standalone mode.
@ -352,7 +356,7 @@ The following commands (key pressed) are supported while in Glances:
Sort process by CPU times (TIME+)
``T``
View network I/O as combination
View network I/O as a combination
``u``
Sort processes by USER
@ -375,13 +379,13 @@ The following commands (key pressed) are supported while in Glances:
``0``
Enable/disable Irix/Solaris mode
Task's CPU usage will be divided by the total number of CPUs
The task's CPU usage will be divided by the total number of CPUs
``1``
Switch between global CPU and per-CPU stats
``2``
Enable/disable left sidebar
Enable/disable the left sidebar
``3``
Enable/disable the quick look module
@ -390,7 +394,7 @@ The following commands (key pressed) are supported while in Glances:
Enable/disable all but quick look and load module
``5``
Enable/disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
Enable/disable the top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
``6``
Enable/disable mean GPU mode
@ -405,10 +409,10 @@ The following commands (key pressed) are supported while in Glances:
Refresh user interface
``LEFT``
Navigation left through process sort
Navigation left through the process sort
``RIGHT``
Navigation right through process sort
Navigation right through the process sort
``UP``
Up in the processes list

View File

@ -5,7 +5,7 @@ Configuration
No configuration file is mandatory to use Glances.
Furthermore a configuration file is needed to access more settings.
Furthermore, a configuration file is needed to access more settings.
Location
--------
@ -14,7 +14,7 @@ Location
A template is available in the ``/usr{,/local}/share/doc/glances``
(Unix-like) directory or directly on `GitHub`_.
You can put your own ``glances.conf`` file in the following locations:
You can place your ``glances.conf`` file in the following locations:
==================== =============================================================
``Linux``, ``SunOS`` ~/.config/glances/, /etc/glances/, /usr/share/docs/glances/
@ -26,13 +26,13 @@ You can put your own ``glances.conf`` file in the following locations:
- On Windows XP, ``%APPDATA%`` is: ``C:\Documents and Settings\<USERNAME>\Application Data``.
- On Windows Vista and later: ``C:\Users\<USERNAME>\AppData\Roaming``.
User-specific options override system-wide options and options given on
the command line override either.
User-specific options override system-wide options, and options given on
the command line overrides both.
Syntax
------
Glances reads configuration files in the *ini* syntax.
Glances read configuration files in the *ini* syntax.
A first section (called global) is available:
@ -40,17 +40,21 @@ A first section (called global) is available:
[global]
# Refresh rate (default is a minimum of 2 seconds)
# Can be overwrite by the -t <sec> option
# It is also possible to overwrite it in each plugin sections
# Can be overwritten by the -t <sec> option
# It is also possible to overwrite it in each plugin section
refresh=2
# Does Glances should check if a newer version is available on PyPI ?
# Should Glances check if a newer version is available on PyPI ?
check_update=false
# History size (maximum number of values)
# Default is 28800: 1 day with 1 point every 3 seconds
history_size=28800
# Define directory external to glances hierarchy for loading additional plugins
# The layout follows the glances standard for plugin definitions
# (see <install-dir>glances/plugins for details)
# plugin_dir=/home/user/dev/plugins
Each plugin, export module and application monitoring process (AMP) can
have a section. Below an example for the CPU plugin:
Each plugin, export module, and application monitoring process (AMP) can
have a section. Below is an example for the CPU plugin:
.. code-block:: ini
@ -90,16 +94,16 @@ or a Nginx AMP:
.. code-block:: ini
[amp_nginx]
# Nginx status page should be enable (https://easyengine.io/tutorials/nginx/status-page/)
# Nginx status page should be enabled (https://easyengine.io/tutorials/nginx/status-page/)
enable=true
regex=\/usr\/sbin\/nginx
refresh=60
one_line=false
status_url=http://localhost/nginx_status
With Glances 3.0 or higher it is also possible to use dynamic configuration
value using system command. For example, if you to set the prefix of an
InfluxDB export to the current hostname, use:
With Glances 3.0 or higher, you can use dynamic configuration values
by utilizing system commands. For example, if you want to set the prefix
of an InfluxDB export to the current hostname, use:
.. code-block:: ini
@ -120,16 +124,17 @@ Logging
Glances logs all of its internal messages to a log file.
``DEBUG`` messages can been logged using the ``-d`` option on the command
``DEBUG`` messages can be logged using the ``-d`` option on the command
line.
The location of the Glances depends of your operating system. You could
displayed the Glances log file full path using the``glances -V`` command line.
The location of the Glances log file depends on your operating system. You can
display the full path of the Glances log file using the ``glances -V``
command line.
The file is automatically rotate when the size is higher than 1 MB.
The file is automatically rotated when its size exceeds 1 MB.
If you want to use another system path or change the log message, you
can use your own logger configuration. First of all, you have to create
can use your logger configuration. First of all, you have to create
a ``glances.json`` file with, for example, the following content (JSON
format):
@ -201,7 +206,7 @@ and start Glances using the following command line:
LOG_CFG=<path>/glances.json glances
.. note::
Replace ``<path>`` by the folder where your ``glances.json`` file
Replace ``<path>`` with the directory where your ``glances.json`` file
is hosted.
.. _GitHub: https://raw.githubusercontent.com/nicolargo/glances/master/conf/glances.conf

View File

@ -3,7 +3,9 @@
Docker
======
Glances can be installed through Docker, allowing you to run it without installing all the python dependencies directly on your system. Once you have `docker installed <https://docs.docker.com/install/>`_, you can
Glances can be installed through Docker, allowing you to run it without
installing all the Python dependencies directly on your system. Once you
have `docker installed <https://docs.docker.com/install/>`_, you can
Get the Glances container:
@ -11,7 +13,7 @@ Get the Glances container:
docker pull nicolargo/glances:<version or tag>
Available tags (all images are based on both Alpine and Ubuntu Operating System):
Available tags (all images are based on both Alpine and Ubuntu Operating Systems):
.. list-table::
:widths: 25 15 25 35
@ -28,7 +30,7 @@ Available tags (all images are based on both Alpine and Ubuntu Operating System)
* - `latest`
- Alpine
- Latest Release
- Minimal + (Bottle & Docker)
- Minimal + (FastAPI & Docker)
* - `dev`
- Alpine
- develop
@ -40,20 +42,20 @@ Available tags (all images are based on both Alpine and Ubuntu Operating System)
* - `ubuntu-latest`
- Ubuntu
- Latest Release
- Minimal + (Bottle & Docker)
- Minimal + (FastAPI & Docker)
* - `ubuntu-dev`
- Ubuntu
- develop
- Full
.. warning::
Tags containing `dev` target the `develop` branch directly and could be unstable.
Tags containing `dev` directly target the `develop` branch and could be unstable.
For example, if you want a full Alpine Glances image (latest release) with all dependencies, go for `latest-full`.
You can also specify a version (example: 3.4.0). All available versions can be found on `DockerHub`_.
An Example to pull the `latest` tag:
An example of how to pull the `latest` tag:
.. code-block:: console
@ -81,7 +83,7 @@ Alternatively, you can specify something along the same lines with docker run op
Where \`pwd\`/glances.conf is a local directory containing your glances.conf file.
Glances by default, uses the container's OS information in the UI. If you want to display the host's OS info, you can do that by mounting `/etc/os-release` into the container.
Glances by default uses the container's OS information in the UI. If you want to display the host's OS info, you can do that by mounting `/etc/os-release` into the container.
Here is a simple docker run example for that:
@ -97,7 +99,7 @@ Run the container in *Web server mode* (notice the `GLANCES_OPT` environment var
Note: if you want to see the network interface stats within the container, add --net=host --privileged
You can also include Glances container in you own `docker-compose.yml`. Here's a realistic example including a "traefik" reverse proxy serving an "whoami" app container plus a Glances container, providing a simple and efficient monitoring webui.
You can also include Glances container in you own `docker-compose.yml`. A realistic example includes a "traefik" reverse proxy serving an "whoami" app container plus a Glances container, providing a simple and efficient monitoring webui.
.. code-block:: console

View File

@ -11,12 +11,12 @@ SYNOPSIS
DESCRIPTION
-----------
**glances** is a cross-platform curses-based monitoring tool which aims
to present a maximum of information in a minimum of space, ideally to
fit in a classical 80x24 terminal or higher to have additional
information. It can adapt dynamically the displayed information
depending on the terminal size. It can also work in client/server mode.
Remote monitoring could be done via terminal or web interface.
**glances** is a cross-platform curses-based monitoring tool that aims
to present a maximum of information in a minimum of space, ideally fitting
in a classic 80x24 terminal or larger for more details. It can adapt
dynamically to the displayed information depending on the terminal size.
It can also work in client/server mode.
Remote monitoring can be performed via a terminal or web interface.
**glances** is written in Python and uses the *psutil* library to get
information from your system.
@ -38,19 +38,20 @@ Monitor local machine (standalone mode):
$ glances
Monitor local machine with the web interface (Web UI), run the following command line:
To monitor the local machine with the web interface (Web UI),
, run the following command line:
$ glances -w
and open a Web browser with the returned URL
then, open a web browser to the provided URL.
Monitor local machine and export stats to a CSV file:
$ glances --export csv --export-csv-file /tmp/glances.csv
Monitor local machine and export stats to a InfluxDB server with 5s
Monitor local machine and export stats to an InfluxDB server with 5s
refresh time (also possible to export to OpenTSDB, Cassandra, Statsd,
ElasticSearch, RabbitMQ and Riemann):
ElasticSearch, RabbitMQ, and Riemann):
$ glances -t 5 --export influxdb

View File

@ -3,13 +3,13 @@ Glances
.. image:: _static/screenshot-wide.png
Glances is a cross-platform monitoring tool which aims to present a
maximum of information in a minimum of space through a curses or Web
based interface. It can adapt dynamically the displayed information
depending on the terminal size.
Glances is a cross-platform monitoring tool that aims to present
maximum information in minimal space through either a curses-based
or Web-based interface. It can dynamically adapt the displayed
information depending on the terminal size.
It can also work in client/server mode. Remote monitoring could be
done via terminal, Web interface or API (XMLRPC and RESTful).
It can also work in client/server mode. Remote monitoring can be
done via terminal, Web interface, or API (XMLRPC and RESTful).
Glances is written in Python and uses the `psutil`_ library to get
information from your system.

View File

@ -3,8 +3,8 @@
Install
=======
Glances is on ``PyPI``. By using PyPI, you are sure to have the latest
stable version.
Glances is available on ``PyPI``. By using PyPI, you are sure to have the
latest stable version.
To install, simply use ``pip``:
@ -12,13 +12,13 @@ To install, simply use ``pip``:
pip install glances
*Note*: Python headers are required to install `psutil`_. For example,
on Debian/Ubuntu you need to install first the *python-dev* package.
For Fedora/CentOS/RHEL install first *python-devel* package. For Windows,
just install psutil from the binary installation file.
*Note*: Python headers are required to install `psutil`_. For instance,
on Debian/Ubuntu, you must first install the *python-dev* package.
On Fedora/CentOS/RHEL, first, install the *python-devel* package. For Windows,
psutil can be installed from the binary installation file.
You can also install the following libraries in order to use optional
features (like the Web interface, export modules...):
You can also install the following libraries to use the optional
features (such as the web interface, export modules, etc.):
.. code-block:: console

View File

@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "GLANCES" "1" "Oct 07, 2023" "4.0.0_beta01" "Glances"
.TH "GLANCES" "1" "Dec 16, 2023" "4.0.0_beta01" "Glances"
.SH NAME
glances \- An eye on your system
.SH SYNOPSIS
@ -35,12 +35,12 @@ glances \- An eye on your system
\fBglances\fP [OPTIONS]
.SH DESCRIPTION
.sp
\fBglances\fP is a cross\-platform curses\-based monitoring tool which aims
to present a maximum of information in a minimum of space, ideally to
fit in a classical 80x24 terminal or higher to have additional
information. It can adapt dynamically the displayed information
depending on the terminal size. It can also work in client/server mode.
Remote monitoring could be done via terminal or web interface.
\fBglances\fP is a cross\-platform curses\-based monitoring tool that aims
to present a maximum of information in a minimum of space, ideally fitting
in a classic 80x24 terminal or larger for more details. It can adapt
dynamically to the displayed information depending on the terminal size.
It can also work in client/server mode.
Remote monitoring can be performed via a terminal or web interface.
.sp
\fBglances\fP is written in Python and uses the \fIpsutil\fP library to get
information from your system.
@ -54,7 +54,7 @@ show this help message and exit
.INDENT 0.0
.TP
.B \-V, \-\-version
show programs version number and exit
show the programs version number and exit
.UNINDENT
.INDENT 0.0
.TP
@ -68,28 +68,33 @@ path to the configuration file
.UNINDENT
.INDENT 0.0
.TP
.B \-P PLUGIN_DIRECTORY, \-\-plugins PLUGIN_DIRECTORY
path to a directory containing additional plugins
.UNINDENT
.INDENT 0.0
.TP
.B \-\-modules\-list
display modules (plugins & exports) list and exit
.UNINDENT
.INDENT 0.0
.TP
.B \-\-disable\-plugin PLUGIN
disable PLUGIN (comma separated list)
disable PLUGIN (comma\-separated list)
.UNINDENT
.INDENT 0.0
.TP
.B \-\-enable\-plugin PLUGIN
enable PLUGIN (comma separated list)
enable PLUGIN (comma\-separated list)
.UNINDENT
.INDENT 0.0
.TP
.B \-\-stdout PLUGINS_STATS
display stats to stdout (comma separated list of plugins/plugins.attribute)
display stats to stdout (comma\-separated list of plugins/plugins.attribute)
.UNINDENT
.INDENT 0.0
.TP
.B \-\-export EXPORT
enable EXPORT module (comma separated list)
enable EXPORT module (comma\-separated list)
.UNINDENT
.INDENT 0.0
.TP
@ -114,7 +119,7 @@ disable the Web UI (only the RESTful API will respond)
.INDENT 0.0
.TP
.B \-\-light, \-\-enable\-light
light mode for Curses UI (disable all but top menu)
light mode for Curses UI (disable all but the top menu)
.UNINDENT
.INDENT 0.0
.TP
@ -144,7 +149,7 @@ disable all but quick look and load
.INDENT 0.0
.TP
.B \-5, \-\-disable\-top
disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
disable top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
.UNINDENT
.INDENT 0.0
.TP
@ -249,7 +254,7 @@ set refresh time in seconds [default: 3 sec]
.INDENT 0.0
.TP
.B \-w, \-\-webserver
run Glances in web server mode (bottle lib needed)
run Glances in web server mode (FastAPI lib needed)
.UNINDENT
.INDENT 0.0
.TP
@ -279,12 +284,12 @@ force short name for processes name
.INDENT 0.0
.TP
.B \-\-hide\-kernel\-threads
hide kernel threads in process list (not available on Windows)
hide kernel threads in the process list (not available on Windows)
.UNINDENT
.INDENT 0.0
.TP
.B \-b, \-\-byte
display network rate in byte per second
display network rate in bytes per second
.UNINDENT
.INDENT 0.0
.TP
@ -309,12 +314,12 @@ display FS free space instead of used
.INDENT 0.0
.TP
.B \-\-theme\-white
optimize display colors for white background
optimize display colors for a white background
.UNINDENT
.INDENT 0.0
.TP
.B \-\-disable\-check\-update
disable online Glances version ckeck
disable online Glances version check
.UNINDENT
.SH INTERACTIVE COMMANDS
.sp
@ -331,7 +336,7 @@ On macOS please use \fBCTRL\-H\fP to delete filter.
.UNINDENT
.UNINDENT
.sp
Filter is a regular expression pattern:
The filter is a regular expression pattern:
.INDENT 7.0
.IP \(bu 2
\fBgnome\fP: matches all processes starting with the \fBgnome\fP
@ -353,7 +358,7 @@ If CPU iowait \fB>60%\fP, sort processes by I/O read and write
.UNINDENT
.TP
.B \fBA\fP
Enable/disable Application Monitoring Process
Enable/disable the Application Monitoring Process
.TP
.B \fBb\fP
Switch between bit/s or Byte/s for network I/O
@ -377,7 +382,7 @@ Enable/disable Docker stats
Enable/disable top extended stats
.TP
.B \fBE\fP
Erase current process filter
Erase the current process filter
.TP
.B \fBf\fP
Show/hide file system and folder monitoring stats
@ -404,7 +409,7 @@ Show/hide IP module
Increase selected process nice level / Lower the priority (need right) \- Only in standalone mode.
.TP
.B \fB\-\fP
Decrease selected process nice level / Higher the priority (need right) \- Only in standalone mode.
Decrease selected process nice level / Higher the priority (need right) \- Only in standalone mode.
.TP
.B \fBk\fP
Kill selected process (need right) \- Only in standalone mode.
@ -455,7 +460,7 @@ Enable/disable spark lines
Sort process by CPU times (TIME+)
.TP
.B \fBT\fP
View network I/O as combination
View network I/O as a combination
.TP
.B \fBu\fP
Sort processes by USER
@ -478,13 +483,13 @@ Show/hide processes stats
.B \fB0\fP
Enable/disable Irix/Solaris mode
.sp
Tasks CPU usage will be divided by the total number of CPUs
The tasks CPU usage will be divided by the total number of CPUs
.TP
.B \fB1\fP
Switch between global CPU and per\-CPU stats
.TP
.B \fB2\fP
Enable/disable left sidebar
Enable/disable the left sidebar
.TP
.B \fB3\fP
Enable/disable the quick look module
@ -493,7 +498,7 @@ Enable/disable the quick look module
Enable/disable all but quick look and load module
.TP
.B \fB5\fP
Enable/disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
Enable/disable the top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
.TP
.B \fB6\fP
Enable/disable mean GPU mode
@ -508,10 +513,10 @@ Switch between process command line or command name
Refresh user interface
.TP
.B \fBLEFT\fP
Navigation left through process sort
Navigation left through the process sort
.TP
.B \fBRIGHT\fP
Navigation right through process sort
Navigation right through the process sort
.TP
.B \fBUP\fP
Up in the processes list
@ -540,7 +545,7 @@ Quit Glances
.sp
No configuration file is mandatory to use Glances.
.sp
Furthermore a configuration file is needed to access more settings.
Furthermore, a configuration file is needed to access more settings.
.SH LOCATION
.sp
\fBNOTE:\fP
@ -551,7 +556,7 @@ A template is available in the \fB/usr{,/local}/share/doc/glances\fP
.UNINDENT
.UNINDENT
.sp
You can put your own \fBglances.conf\fP file in the following locations:
You can place your \fBglances.conf\fP file in the following locations:
.TS
center;
|l|l|.
@ -588,11 +593,11 @@ On Windows XP, \fB%APPDATA%\fP is: \fBC:\eDocuments and Settings\e<USERNAME>\eAp
On Windows Vista and later: \fBC:\eUsers\e<USERNAME>\eAppData\eRoaming\fP\&.
.UNINDENT
.sp
User\-specific options override system\-wide options and options given on
the command line override either.
User\-specific options override system\-wide options, and options given on
the command line overrides both.
.SH SYNTAX
.sp
Glances reads configuration files in the \fIini\fP syntax.
Glances read configuration files in the \fIini\fP syntax.
.sp
A first section (called global) is available:
.INDENT 0.0
@ -602,21 +607,25 @@ A first section (called global) is available:
.ft C
[global]
# Refresh rate (default is a minimum of 2 seconds)
# Can be overwrite by the \-t <sec> option
# It is also possible to overwrite it in each plugin sections
# Can be overwritten by the \-t <sec> option
# It is also possible to overwrite it in each plugin section
refresh=2
# Does Glances should check if a newer version is available on PyPI ?
# Should Glances check if a newer version is available on PyPI ?
check_update=false
# History size (maximum number of values)
# Default is 28800: 1 day with 1 point every 3 seconds
history_size=28800
# Define directory external to glances hierarchy for loading additional plugins
# The layout follows the glances standard for plugin definitions
# (see <install\-dir>glances/plugins for details)
# plugin_dir=/home/user/dev/plugins
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
Each plugin, export module and application monitoring process (AMP) can
have a section. Below an example for the CPU plugin:
Each plugin, export module, and application monitoring process (AMP) can
have a section. Below is an example for the CPU plugin:
.INDENT 0.0
.INDENT 3.5
.sp
@ -670,7 +679,7 @@ or a Nginx AMP:
.nf
.ft C
[amp_nginx]
# Nginx status page should be enable (https://easyengine.io/tutorials/nginx/status\-page/)
# Nginx status page should be enabled (https://easyengine.io/tutorials/nginx/status\-page/)
enable=true
regex=\e/usr\e/sbin\e/nginx
refresh=60
@ -681,9 +690,9 @@ status_url=http://localhost/nginx_status
.UNINDENT
.UNINDENT
.sp
With Glances 3.0 or higher it is also possible to use dynamic configuration
value using system command. For example, if you to set the prefix of an
InfluxDB export to the current hostname, use:
With Glances 3.0 or higher, you can use dynamic configuration values
by utilizing system commands. For example, if you want to set the prefix
of an InfluxDB export to the current hostname, use:
.INDENT 0.0
.INDENT 3.5
.sp
@ -714,16 +723,17 @@ tags=system:\(gauname \-a\(ga
.sp
Glances logs all of its internal messages to a log file.
.sp
\fBDEBUG\fP messages can been logged using the \fB\-d\fP option on the command
\fBDEBUG\fP messages can be logged using the \fB\-d\fP option on the command
line.
.sp
The location of the Glances depends of your operating system. You could
displayed the Glances log file full path using the\(ga\(gaglances \-V\(ga\(ga command line.
The location of the Glances log file depends on your operating system. You can
display the full path of the Glances log file using the \fBglances \-V\fP
command line.
.sp
The file is automatically rotate when the size is higher than 1 MB.
The file is automatically rotated when its size exceeds 1 MB.
.sp
If you want to use another system path or change the log message, you
can use your own logger configuration. First of all, you have to create
can use your logger configuration. First of all, you have to create
a \fBglances.json\fP file with, for example, the following content (JSON
format):
.INDENT 0.0
@ -809,7 +819,7 @@ LOG_CFG=<path>/glances.json glances
\fBNOTE:\fP
.INDENT 0.0
.INDENT 3.5
Replace \fB<path>\fP by the folder where your \fBglances.json\fP file
Replace \fB<path>\fP with the directory where your \fBglances.json\fP file
is hosted.
.UNINDENT
.UNINDENT
@ -822,14 +832,15 @@ $ glances
.UNINDENT
.UNINDENT
.sp
Monitor local machine with the web interface (Web UI), run the following command line:
To monitor the local machine with the web interface (Web UI),
, run the following command line:
.INDENT 0.0
.INDENT 3.5
$ glances \-w
.UNINDENT
.UNINDENT
.sp
and open a Web browser with the returned URL
then, open a web browser to the provided URL.
.sp
Monitor local machine and export stats to a CSV file:
.INDENT 0.0
@ -838,9 +849,9 @@ $ glances export csv export\-csv\-file /tmp/glances.csv
.UNINDENT
.UNINDENT
.sp
Monitor local machine and export stats to a InfluxDB server with 5s
Monitor local machine and export stats to an InfluxDB server with 5s
refresh time (also possible to export to OpenTSDB, Cassandra, Statsd,
ElasticSearch, RabbitMQ and Riemann):
ElasticSearch, RabbitMQ, and Riemann):
.INDENT 0.0
.INDENT 3.5
$ glances \-t 5 export influxdb

View File

@ -3,8 +3,8 @@
Quickstart
==========
This page gives a good introduction in how to get started with Glances.
Glances offers 3 modes:
This page gives a good introduction to how to get started with Glances.
Glances offers three modes:
- Standalone
- Client/Server
@ -61,7 +61,7 @@ Note: It will display one line per stat per refresh.
Client/Server Mode
------------------
If you want to remotely monitor a machine, called ``server``, from
If you want to remotely monitor a machine called ``server``, from
another one, called ``client``, just run on the server:
.. code-block:: console
@ -118,7 +118,7 @@ To start the central client, use the following option:
.. note::
Use ``--disable-autodiscover`` to disable the auto discovery mode.
Use ``--disable-autodiscover`` to disable the auto-discovery mode.
When the list is displayed, you can navigate through the Glances servers with
up/down keys. It is also possible to sort the server using:
@ -137,7 +137,7 @@ client, the latter will try to grab stats using the ``SNMP`` protocol:
client$ glances -c @snmpserver
.. note::
Stats grabbed by SNMP request are limited and OS dependent.
Stats grabbed by SNMP request are limited and OS-dependent.
A SNMP server should be installed and configured...
@ -152,14 +152,14 @@ Web Server Mode
.. image:: _static/screenshot-web.png
If you want to remotely monitor a machine, called ``server``, from any
If you want to remotely monitor a machine called ``server``, from any
device with a web browser, just run the server with the ``-w`` option:
.. code-block:: console
server$ glances -w
then on the client enter the following URL in your favorite web browser:
then, on the client, enter the following URL in your favorite web browser:
::
@ -167,7 +167,7 @@ then on the client enter the following URL in your favorite web browser:
where ``@server`` is the IP address or hostname of the server.
To change the refresh rate of the page, just add the period in seconds
To change the refresh rate of the page, add the period in seconds
at the end of the URL. For example, to refresh the page every ``10``
seconds:
@ -181,10 +181,10 @@ Here's a screenshot from Chrome on Android:
.. image:: _static/screenshot-web2.png
How to protect your server (or Web server) with a login/password ?
How do you protect your server (or Web server) with a login/password ?
------------------------------------------------------------------
You can set a password to access to the server using the ``--password``.
You can set a password to access the server using the ``--password``.
By default, the login is ``glances`` but you can change it with
``--username``.
@ -192,8 +192,8 @@ If you want, the SHA password will be stored in ``<login>.pwd`` file (in
the same folder where the Glances configuration file is stored, so
~/.config/glances/ on GNU Linux operating system).
Next time your run the server/client, password will not be asked. To set a
specific username you can use the -u <username> option.
Next time you run the server/client, password will not be asked. To set a
specific username, you can use the -u <username> option.
It is also possible to set the default password in the Glances configuration
file:

View File

@ -7,7 +7,7 @@ To post a question about Glances use cases, please post it to the
official Q&A `forum
<https://groups.google.com/forum/?hl=en#!forum/glances-users>`_.
To report a bug or a feature request use the GitHub `issue
To report a bug or a feature request, use the GitHub `issue
<https://github.com/nicolargo/glances/issues>`_ tracker.
Feel free to contribute!

View File

@ -12,7 +12,7 @@ globals.py Share variables upon modules
main.py Main script to rule them up...
client.py Glances client
server.py Glances server
webserver.py Glances web server (Bottle-based)
webserver.py Glances web server (Based on FastAPI)
autodiscover.py Glances autodiscover module (via zeroconf)
standalone.py Glances standalone (curses interface)
password.py Manage password for Glances client/server
@ -27,7 +27,7 @@ plugins
outputs
=> Glances UI
glances_curses.py The curses interface
glances_bottle.py The web interface
glances_restful-api.py The HTTP/API & Web based interface
...
exports
=> Glances exports

View File

@ -21,6 +21,7 @@ import sys
# Version should start and end with a numerical char
# See https://packaging.python.org/specifications/core-metadata/#version
__version__ = '4.0.0_beta01'
__apiversion__ = '4'
__author__ = 'Nicolas Hennion <nicolas@nicolargo.com>'
__license__ = 'LGPLv3'
@ -108,7 +109,7 @@ def start(config, args):
# Start the main loop
logger.debug("Glances started in {} seconds".format(start_duration.get()))
if args.stop_after:
logger.info('Glances will be stopped in ~{} seconds'.format(args.stop_after * args.time * args.memory_leak * 2))
logger.info('Glances will be stopped in ~{} seconds'.format(args.stop_after * args.time))
if args.memory_leak:
print(

View File

@ -42,7 +42,7 @@ class GlancesAmp(object):
# AMP name (= module name without glances_)
if name is None:
self.amp_name = self.__class__.__module__[len('glances_') :]
self.amp_name = self.__class__.__module__
else:
self.amp_name = name

View File

@ -29,7 +29,7 @@ from subprocess import check_output, STDOUT, CalledProcessError
from glances.globals import u, to_ascii
from glances.logger import logger
from glances.amps.glances_amp import GlancesAmp
from glances.amps.amp import GlancesAmp
class Amp(GlancesAmp):

View File

@ -47,7 +47,7 @@ status_url=http://localhost/nginx_status
import requests
from glances.logger import logger
from glances.amps.glances_amp import GlancesAmp
from glances.amps.amp import GlancesAmp
class Amp(GlancesAmp):

View File

@ -39,7 +39,7 @@ from subprocess import check_output, CalledProcessError
from glances.logger import logger
from glances.globals import iteritems, to_ascii
from glances.amps.glances_amp import GlancesAmp
from glances.amps.amp import GlancesAmp
class Amp(GlancesAmp):

View File

@ -38,7 +38,7 @@ from subprocess import check_output, STDOUT
from glances.logger import logger
from glances.globals import iteritems
from glances.amps.glances_amp import GlancesAmp
from glances.amps.amp import GlancesAmp
class Amp(GlancesAmp):

View File

@ -45,41 +45,30 @@ class AmpsList(object):
if self.config is None:
return False
# Display a warning (deprecated) message if the monitor section exist
if "monitor" in self.config.sections():
logger.warning(
"A deprecated [monitor] section exists in the Glances configuration file. You should use the new \
Applications Monitoring Process module instead \
(http://glances.readthedocs.io/en/develop/aoa/amps.html)."
)
# TODO: Change the way AMP are loaded (use folder/module instead of glances_foo.py file)
# See https://github.com/nicolargo/glances/issues/1930
header = "glances_"
# For each AMP script, call the load_config method
for s in self.config.sections():
if s.startswith("amp_"):
# An AMP section exists in the configuration file
# If an AMP script exist in the glances/amps folder, use it
amp_conf_name = s[4:]
amp_script = os.path.join(amps_path, header + s[4:] + ".py")
if not os.path.exists(amp_script):
# If an AMP module exist in amps_path (glances/amps) folder then use it
amp_name = s[4:]
amp_module = os.path.join(amps_path, amp_name)
if not os.path.exists(amp_module):
# If not, use the default script
amp_script = os.path.join(amps_path, "glances_default.py")
amp_module = os.path.join(amps_path, "default")
try:
amp = __import__(os.path.basename(amp_script)[:-3])
amp = __import__(os.path.basename(amp_module))
except ImportError as e:
logger.warning("Missing Python Lib ({}), cannot load {} AMP".format(e, amp_conf_name))
logger.warning("Missing Python Lib ({}), cannot load AMP {}".format(e, amp_name))
except Exception as e:
logger.warning("Cannot load {} AMP ({})".format(amp_conf_name, e))
logger.warning("Cannot load AMP {} ({})".format(amp_name, e))
else:
# Add the AMP to the dictionary
# The key is the AMP name
# for example, the file glances_xxx.py
# generate self._amps_list["xxx"] = ...
self.__amps_dict[amp_conf_name] = amp.Amp(name=amp_conf_name, args=self.args)
self.__amps_dict[amp_name] = amp.Amp(name=amp_name, args=self.args)
# Load the AMP configuration
self.__amps_dict[amp_conf_name].load_config(self.config)
self.__amps_dict[amp_name].load_config(self.config)
# Log AMPs list
logger.debug("AMPs list: {}".format(self.getList()))

View File

@ -105,14 +105,14 @@ class GlancesEvents(object):
event_index = self.__event_exist(event_type)
if event_index < 0:
# Event did not exist, add it
self._create_event(event_state, event_type, event_value, proc_list, proc_desc, peak_time)
self._create_event(event_state, event_type, event_value, proc_desc)
else:
# Event exist, update it
self._update_event(event_index, event_state, event_type, event_value, proc_list, proc_desc, peak_time)
return self.len()
def _create_event(self, event_state, event_type, event_value, proc_list, proc_desc, peak_time):
def _create_event(self, event_state, event_type, event_value, proc_desc):
"""Add a new item in the log list.
Item is added only if the criticality (event_state) is WARNING or CRITICAL.

View File

@ -36,9 +36,7 @@ class Export(GlancesExport):
# Load the CouchDB configuration file section
# User and Password are mandatory with CouchDB 3.0 and higher
self.export_enable = self.load_conf('couchdb',
mandatories=['host', 'port', 'db',
'user', 'password'])
self.export_enable = self.load_conf('couchdb', mandatories=['host', 'port', 'db', 'user', 'password'])
if not self.export_enable:
sys.exit(2)
@ -51,8 +49,7 @@ class Export(GlancesExport):
return None
# @TODO: https
server_uri = 'http://{}:{}@{}:{}/'.format(self.user, self.password,
self.host, self.port)
server_uri = 'http://{}:{}@{}:{}/'.format(self.user, self.password, self.host, self.port)
try:
s = pycouchdb.Server(server_uri)

View File

@ -36,12 +36,13 @@ class GlancesExport(object):
'processlist',
'psutilversion',
'quicklook',
'version',
]
def __init__(self, config=None, args=None):
"""Init the export class."""
# Export name (= module name without glances_)
self.export_name = self.__class__.__module__[len('glances_') :]
self.export_name = self.__class__.__module__
logger.debug("Init export module %s" % self.export_name)
# Init the config & args
@ -115,7 +116,7 @@ class GlancesExport(object):
def parse_tags(self, tags):
"""Parse tags into a dict.
:param tags: a comma separated list of 'key:value' pairs. Example: foo:bar,spam:eggs
:param tags: a comma-separated list of 'key:value' pairs. Example: foo:bar,spam:eggs
:return: a dict of tags. Example: {'foo': 'bar', 'spam': 'eggs'}
"""
d_tags = {}

View File

@ -10,7 +10,6 @@
"""Manage the folder list."""
from __future__ import unicode_literals
import os
from glances.timer import Timer
from glances.globals import nativestr, folder_size
@ -132,11 +131,12 @@ class FolderList(object):
# Get folder size
self.__folder_list[i]['size'], self.__folder_list[i]['errno'] = folder_size(self.path(i))
if self.__folder_list[i]['errno'] != 0:
logger.debug('Folder size ({} ~ {}) may not be correct. Error: {}'.format(
self.path(i),
self.__folder_list[i]['size'],
self.__folder_list[i]['errno']))
# Reset the timer
logger.debug(
'Folder size ({} ~ {}) may not be correct. Error: {}'.format(
self.path(i), self.__folder_list[i]['size'], self.__folder_list[i]['errno']
)
)
# Reset the timer
self.timer_folders[i].reset()
# It is no more the first time...

View File

@ -25,6 +25,8 @@ import subprocess
from datetime import datetime
import re
import base64
import functools
import weakref
import queue
from configparser import ConfigParser, NoOptionError, NoSectionError
@ -315,10 +317,10 @@ def json_dumps(data):
return ujson.dumps(data, ensure_ascii=False)
def json_dumps_dictlist(data, item):
def dictlist(data, item):
if isinstance(data, dict):
try:
return json_dumps({item: data[item]})
return {item: data[item]}
except (TypeError, IndexError, KeyError):
return None
elif isinstance(data, list):
@ -326,13 +328,21 @@ def json_dumps_dictlist(data, item):
# Source:
# http://stackoverflow.com/questions/4573875/python-get-index-of-dictionary-item-in-list
# But https://github.com/nicolargo/glances/issues/1401
return json_dumps({item: list(map(itemgetter(item), data))})
return {item: list(map(itemgetter(item), data))}
except (TypeError, IndexError, KeyError):
return None
else:
return None
def json_dumps_dictlist(data, item):
dl = dictlist(data, item)
if dl is None:
return None
else:
return json_dumps(dl)
def string_value_to_float(s):
"""Convert a string with a value and an unit to a float.
Example:
@ -398,3 +408,21 @@ def folder_size(path, errno=0):
except OSError as e:
ret_err = e.errno
return ret_size, ret_err
def weak_lru_cache(maxsize=128, typed=False):
"""LRU Cache decorator that keeps a weak reference to self
Source: https://stackoverflow.com/a/55990799"""
def wrapper(func):
@functools.lru_cache(maxsize, typed)
def _func(_self, *args, **kwargs):
return func(_self(), *args, **kwargs)
@functools.wraps(func)
def inner(self, *args, **kwargs):
return _func(weakref.ref(self), *args, **kwargs)
return inner
return wrapper

View File

@ -15,7 +15,7 @@ import tempfile
from logging import DEBUG
from warnings import simplefilter
from glances import __version__, psutil_version
from glances import __version__, psutil_version, __apiversion__
from glances.globals import WINDOWS, disable, enable
from glances.config import Config
from glances.processes import sort_processes_key_list
@ -83,13 +83,13 @@ Examples of use:
Display CSV stats to stdout (all stats in one line):
$ glances --stdout-csv now,cpu.user,mem.used,load
Enable some plugins disabled by default (comma separated list):
Enable some plugins disabled by default (comma-separated list):
$ glances --enable-plugin sensors
Disable some plugins (comma separated list):
Disable some plugins (comma-separated list):
$ glances --disable-plugin network,ports
Disable all plugins except some (comma separated list):
Disable all plugins except some (comma-separated list):
$ glances --disable-plugin all --enable-plugin cpu,mem,load
"""
@ -99,18 +99,26 @@ Examples of use:
# Read the command line arguments
self.args = self.parse_args()
def version_msg(self):
"""Return the version message."""
version = 'Glances version:\t{}\n'.format(__version__)
version += 'Glances API version:\t{}\n'.format(__apiversion__)
version += 'PsUtil version:\t\t{}\n'.format(psutil_version)
version += 'Log file:\t\t{}\n'.format(LOG_FILENAME)
return version
def init_args(self):
"""Init all the command line arguments."""
version = 'Glances v{} with PsUtil v{}\nLog file: {}'.format(__version__, psutil_version, LOG_FILENAME)
parser = argparse.ArgumentParser(
prog='glances',
conflict_handler='resolve',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=self.example_of_use,
)
parser.add_argument('-V', '--version', action='version', version=version)
parser.add_argument('-V', '--version', action='version', version=self.version_msg())
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')
parser.add_argument('-P', '--plugins', dest='plugin_dir', help='path to additional plugin directory')
# Disable plugin
parser.add_argument(
'--modules-list',
@ -125,7 +133,7 @@ Examples of use:
'--disable-plugins',
'--disable',
dest='disable_plugin',
help='disable plugin (comma separated list or all). If all is used, \
help='disable plugin (comma-separated list or all). If all is used, \
then you need to configure --enable-plugin.',
)
parser.add_argument(
@ -133,7 +141,7 @@ Examples of use:
'--enable-plugins',
'--enable',
dest='enable_plugin',
help='enable plugin (comma separated list)'
help='enable plugin (comma-separated list)',
)
parser.add_argument(
'--disable-process',
@ -156,7 +164,7 @@ Examples of use:
action='store_true',
default=False,
dest='enable_light',
help='light mode for Curses UI (disable all but top menu)',
help='light mode for Curses UI (disable all but the top menu)',
)
parser.add_argument(
'-0',
@ -267,7 +275,7 @@ Examples of use:
help='Accumulate processes by program',
)
# Export modules feature
parser.add_argument('--export', dest='export', help='enable export module (comma separated list)')
parser.add_argument('--export', dest='export', help='enable export module (comma-separated list)')
parser.add_argument(
'--export-csv-file', default='./glances.csv', dest='export_csv_file', help='file path for CSV exporter'
)
@ -362,7 +370,7 @@ Examples of use:
action='store_true',
default=False,
dest='webserver',
help='run Glances in web server mode (bottle needed)',
help='run Glances in web server mode (FastAPI, Uvicorn, Jinja2 and OrJsonLib needed)',
)
parser.add_argument(
'--cached-time',
@ -420,19 +428,19 @@ Examples of use:
'--stdout',
default=None,
dest='stdout',
help='display stats to stdout, one stat per line (comma separated list of plugins/plugins.attribute)',
help='display stats to stdout, one stat per line (comma-separated list of plugins/plugins.attribute)',
)
parser.add_argument(
'--stdout-json',
default=None,
dest='stdout_json',
help='display stats to stdout, JSON format (comma separated list of plugins/plugins.attribute)',
help='display stats to stdout, JSON format (comma-separated list of plugins/plugins.attribute)',
)
parser.add_argument(
'--stdout-csv',
default=None,
dest='stdout_csv',
help='display stats to stdout, CSV format (comma separated list of plugins/plugins.attribute)',
help='display stats to stdout, CSV format (comma-separated list of plugins/plugins.attribute)',
)
parser.add_argument(
'--issue',
@ -464,7 +472,7 @@ Examples of use:
action='store_true',
default=False,
dest='no_kernel_threads',
help='hide kernel threads in process list (not available on Windows)',
help='hide kernel threads in the process list (not available on Windows)',
)
parser.add_argument(
'-b',
@ -472,7 +480,7 @@ Examples of use:
action='store_true',
default=False,
dest='byte',
help='display network rate in byte per second',
help='display network rate in bytes per second',
)
parser.add_argument(
'--diskio-show-ramfs',
@ -521,7 +529,7 @@ Examples of use:
action='store_true',
default=False,
dest='theme_white',
help='optimize display colors for white background',
help='optimize display colors for a white background',
)
# Globals options
parser.add_argument(

View File

@ -1,668 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""RestFull API interface class."""
import os
import sys
import tempfile
from io import open
import webbrowser
import zlib
import socket
from urllib.parse import urljoin
from glances.globals import b, json_dumps
from glances.timer import Timer
from glances.logger import logger
try:
from bottle import Bottle, static_file, abort, response, request, auth_basic, template, TEMPLATE_PATH
except ImportError:
logger.critical('Bottle module not found. Glances cannot start in web server mode.')
sys.exit(2)
def compress(func):
"""Compress result with deflate algorithm if the client ask for it."""
def wrapper(*args, **kwargs):
"""Wrapper that take one function and return the compressed result."""
ret = func(*args, **kwargs)
logger.debug(
'Receive {} {} request with header: {}'.format(
request.method,
request.url,
['{}: {}'.format(h, request.headers.get(h)) for h in request.headers.keys()],
)
)
if 'deflate' in request.headers.get('Accept-Encoding', ''):
response.headers['Content-Encoding'] = 'deflate'
ret = deflate_compress(ret)
else:
response.headers['Content-Encoding'] = 'identity'
return ret
def deflate_compress(data, compress_level=6):
"""Compress given data using the DEFLATE algorithm"""
# Init compression
zobj = zlib.compressobj(
compress_level, zlib.DEFLATED, zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, zlib.Z_DEFAULT_STRATEGY
)
# Return compressed object
return zobj.compress(b(data)) + zobj.flush()
return wrapper
class GlancesBottle(object):
"""This class manages the Bottle Web server."""
API_VERSION = '3'
def __init__(self, config=None, args=None):
# Init config
self.config = config
# Init args
self.args = args
# Init stats
# Will be updated within Bottle route
self.stats = None
# cached_time is the minimum time interval between stats updates
# i.e. HTTP/RESTful calls will not retrieve updated info until the time
# since last update is passed (will retrieve old cached info instead)
self.timer = Timer(0)
# Load configuration file
self.load_config(config)
# Set the bind URL (only used for log information purpose)
self.bind_url = urljoin('http://{}:{}/'.format(self.args.bind_address, self.args.port), self.url_prefix)
# Init Bottle
self._app = Bottle()
# Enable CORS (issue #479)
self._app.install(EnableCors())
# Password
if args.password != '':
self._app.install(auth_basic(self.check_auth))
# Define routes
self._route()
# Path where the statics files are stored
self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public')
# Paths for templates
TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates'))
def load_config(self, config):
"""Load the outputs section of the configuration file."""
# Limit the number of processes to display in the WebUI
self.url_prefix = '/'
if config is not None and config.has_section('outputs'):
n = config.get_value('outputs', 'max_processes_display', default=None)
logger.debug('Number of processes to display in the WebUI: {}'.format(n))
self.url_prefix = config.get_value('outputs', 'url_prefix', default='/')
logger.debug('URL prefix: {}'.format(self.url_prefix))
def __update__(self):
# Never update more than 1 time per cached_time
if self.timer.finished():
self.stats.update()
self.timer = Timer(self.args.cached_time)
def app(self):
return self._app()
def check_auth(self, username, password):
"""Check if a username/password combination is valid."""
if username == self.args.username:
from glances.password import GlancesPassword
pwd = GlancesPassword(username=username, config=self.config)
return pwd.check_password(self.args.password, pwd.get_hash(password))
else:
return False
def _route(self):
"""Define route."""
# REST API
self._app.route('/api/%s/status' % self.API_VERSION, method="GET", callback=self._api_status)
self._app.route('/api/%s/config' % self.API_VERSION, method="GET", callback=self._api_config)
self._app.route('/api/%s/config/<item>' % self.API_VERSION, method="GET", callback=self._api_config_item)
self._app.route('/api/%s/args' % self.API_VERSION, method="GET", callback=self._api_args)
self._app.route('/api/%s/args/<item>' % self.API_VERSION, method="GET", callback=self._api_args_item)
self._app.route('/api/%s/help' % self.API_VERSION, method="GET", callback=self._api_help)
self._app.route('/api/%s/pluginslist' % self.API_VERSION, method="GET", callback=self._api_plugins)
self._app.route('/api/%s/all' % self.API_VERSION, method="GET", callback=self._api_all)
self._app.route('/api/%s/all/limits' % self.API_VERSION, method="GET", callback=self._api_all_limits)
self._app.route('/api/%s/all/views' % self.API_VERSION, method="GET", callback=self._api_all_views)
self._app.route('/api/%s/<plugin>' % self.API_VERSION, method="GET", callback=self._api)
self._app.route('/api/%s/<plugin>/history' % self.API_VERSION, method="GET", callback=self._api_history)
self._app.route(
'/api/%s/<plugin>/history/<nb:int>' % self.API_VERSION, method="GET", callback=self._api_history
)
self._app.route('/api/%s/<plugin>/top/<nb:int>' % self.API_VERSION, method="GET", callback=self._api_top)
self._app.route('/api/%s/<plugin>/limits' % self.API_VERSION, method="GET", callback=self._api_limits)
self._app.route('/api/%s/<plugin>/views' % self.API_VERSION, method="GET", callback=self._api_views)
self._app.route('/api/%s/<plugin>/<item>' % self.API_VERSION, method="GET", callback=self._api_item)
self._app.route(
'/api/%s/<plugin>/<item>/history' % self.API_VERSION, method="GET", callback=self._api_item_history
)
self._app.route(
'/api/%s/<plugin>/<item>/history/<nb:int>' % self.API_VERSION, method="GET", callback=self._api_item_history
)
self._app.route('/api/%s/<plugin>/<item>/<value>' % self.API_VERSION, method="GET", callback=self._api_value)
self._app.route(
'/api/%s/<plugin>/<item>/<value:path>' % self.API_VERSION, method="GET", callback=self._api_value
)
bindmsg = 'Glances RESTful API Server started on {}api/{}'.format(self.bind_url, self.API_VERSION)
logger.info(bindmsg)
# WEB UI
if not self.args.disable_webui:
self._app.route('/', method="GET", callback=self._index)
self._app.route('/<refresh_time:int>', method=["GET"], callback=self._index)
self._app.route('/<filepath:path>', method="GET", callback=self._resource)
bindmsg = 'Glances Web User Interface started on {}'.format(self.bind_url)
else:
bindmsg = 'The WebUI is disable (--disable-webui)'
logger.info(bindmsg)
print(bindmsg)
def start(self, stats):
"""Start the bottle."""
# Init stats
self.stats = stats
# Init plugin list
self.plugins_list = self.stats.getPluginsList()
# Bind the Bottle TCP address/port
if self.args.open_web_browser:
# Implementation of the issue #946
# Try to open the Glances Web UI in the default Web browser if:
# 1) --open-web-browser option is used
# 2) Glances standalone mode is running on Windows OS
webbrowser.open(self.bind_url, new=2, autoraise=1)
# Run the Web application
if self.url_prefix != '/':
# Create an outer Bottle class instance to manage url_prefix
self.main_app = Bottle()
self.main_app.mount(self.url_prefix, self._app)
try:
self.main_app.run(host=self.args.bind_address, port=self.args.port, quiet=not self.args.debug)
except socket.error as e:
logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
else:
try:
self._app.run(host=self.args.bind_address, port=self.args.port, quiet=not self.args.debug)
except socket.error as e:
logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
def end(self):
"""End the bottle."""
logger.info("Close the Web server")
self._app.close()
if self.url_prefix != '/':
self.main_app.close()
def _index(self, refresh_time=None):
"""Bottle callback for index.html (/) file."""
if refresh_time is None or refresh_time < 1:
refresh_time = int(self.args.time)
# Update the stat
self.__update__()
# Display
return template("index.html", refresh_time=refresh_time)
def _resource(self, filepath):
"""Bottle callback for resources files."""
# Return the static file
return static_file(filepath, root=self.STATIC_PATH)
@compress
def _api_status(self):
"""Glances API RESTful implementation.
Return a 200 status code.
This entry point should be used to check the API health.
See related issue: Web server health check endpoint #1988
"""
response.status = 200
return "Active"
@compress
def _api_help(self):
"""Glances API RESTful implementation.
Return the help data or 404 error.
"""
response.content_type = 'application/json; charset=utf-8'
# Update the stat
view_data = self.stats.get_plugin("help").get_view_data()
try:
plist = json_dumps(view_data)
except Exception as e:
abort(404, "Cannot get help view data (%s)" % str(e))
return plist
@compress
def _api_plugins(self):
"""Glances API RESTFul implementation.
@api {get} /api/%s/pluginslist Get plugins list
@apiVersion 2.0
@apiName pluginslist
@apiGroup plugin
@apiSuccess {String[]} Plugins list.
@apiSuccessExample Success-Response:
HTTP/1.1 200 OK
[
"load",
"help",
"ip",
"memswap",
"processlist",
...
]
@apiError Cannot get plugin list.
@apiErrorExample Error-Response:
HTTP/1.1 404 Not Found
"""
response.content_type = 'application/json; charset=utf-8'
# Update the stat
self.__update__()
try:
plist = json_dumps(self.plugins_list)
except Exception as e:
abort(404, "Cannot get plugin list (%s)" % str(e))
return plist
@compress
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; charset=utf-8'
if self.args.debug:
fname = os.path.join(tempfile.gettempdir(), 'glances-debug.json')
try:
with open(fname) as f:
return f.read()
except IOError:
logger.debug("Debug file (%s) not found" % fname)
# Update the stat
self.__update__()
try:
# Get the JSON value of the stat ID
statval = json_dumps(self.stats.getAllAsDict())
except Exception as e:
abort(404, "Cannot get stats (%s)" % str(e))
return statval
@compress
def _api_all_limits(self):
"""Glances API RESTful implementation.
Return the JSON representation of all the plugins limits
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
try:
# Get the JSON value of the stat limits
limits = json_dumps(self.stats.getAllLimitsAsDict())
except Exception as e:
abort(404, "Cannot get limits (%s)" % (str(e)))
return limits
@compress
def _api_all_views(self):
"""Glances API RESTful implementation.
Return the JSON representation of all the plugins views
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
try:
# Get the JSON value of the stat view
limits = json_dumps(self.stats.getAllViewsAsDict())
except Exception as e:
abort(404, "Cannot get views (%s)" % (str(e)))
return limits
@compress
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; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
self.__update__()
try:
# Get the JSON value of the stat ID
statval = self.stats.get_plugin(plugin).get_stats()
except Exception as e:
abort(404, "Cannot get plugin %s (%s)" % (plugin, str(e)))
return statval
@compress
def _api_top(self, plugin, nb=0):
"""Glances API RESTful implementation.
Return the JSON representation of a given plugin limited to the top nb items.
It is used to reduce the payload of the HTTP response (example: processlist).
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
self.__update__()
try:
# Get the value of the stat ID
statval = self.stats.get_plugin(plugin).get_export()
except Exception as e:
abort(404, "Cannot get plugin %s (%s)" % (plugin, str(e)))
if isinstance(statval, list):
return json_dumps(statval[:nb])
else:
return json_dumps(statval)
@compress
def _api_history(self, plugin, nb=0):
"""Glances API RESTful implementation.
Return the JSON representation of a given plugin history
Limit to the last nb items (all if nb=0)
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
self.__update__()
try:
# Get the JSON value of the stat ID
statval = self.stats.get_plugin(plugin).get_stats_history(nb=int(nb))
except Exception as e:
abort(404, "Cannot get plugin history %s (%s)" % (plugin, str(e)))
return statval
@compress
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; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
# self.__update__()
try:
# Get the JSON value of the stat limits
ret = self.stats.get_plugin(plugin).limits
except Exception as e:
abort(404, "Cannot get limits for plugin %s (%s)" % (plugin, str(e)))
return ret
@compress
def _api_views(self, plugin):
"""Glances API RESTful implementation.
Return the JSON views 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; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
# self.__update__()
try:
# Get the JSON value of the stat views
ret = self.stats.get_plugin(plugin).get_views()
except Exception as e:
abort(404, "Cannot get views for plugin %s (%s)" % (plugin, str(e)))
return ret
# No compression see issue #1228
# @compress
def _api_itemvalue(self, plugin, item, value=None, history=False, nb=0):
"""Father method for _api_item and _api_value."""
response.content_type = 'application/json; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
self.__update__()
if value is None:
if history:
ret = self.stats.get_plugin(plugin).get_stats_history(item, nb=int(nb))
else:
ret = self.stats.get_plugin(plugin).get_stats_item(item)
if ret is None:
abort(404, "Cannot get item %s%s in plugin %s" % (item, 'history ' if history else '', plugin))
else:
if history:
# Not available
ret = None
else:
ret = self.stats.get_plugin(plugin).get_stats_value(item, value)
if ret is None:
abort(
404, "Cannot get item %s(%s=%s) in plugin %s" % ('history ' if history else '', item, value, plugin)
)
return ret
@compress
def _api_item(self, plugin, item):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/item
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
return self._api_itemvalue(plugin, item)
@compress
def _api_item_history(self, plugin, item, nb=0):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/history of item
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
return self._api_itemvalue(plugin, item, history=True, nb=int(nb))
@compress
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
"""
return self._api_itemvalue(plugin, item, value)
@compress
def _api_config(self):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration file
HTTP/200 if OK
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
try:
# Get the JSON value of the config' dict
args_json = json_dumps(self.config.as_dict())
except Exception as e:
abort(404, "Cannot get config (%s)" % str(e))
return args_json
@compress
def _api_config_item(self, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
config_dict = self.config.as_dict()
if item not in config_dict:
abort(400, "Unknown configuration item %s" % item)
try:
# Get the JSON value of the config' dict
args_json = json_dumps(config_dict[item])
except Exception as e:
abort(404, "Cannot get config item (%s)" % str(e))
return args_json
@compress
def _api_args(self):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments
HTTP/200 if OK
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
try:
# Get the JSON value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = json_dumps(vars(self.args))
except Exception as e:
abort(404, "Cannot get args (%s)" % str(e))
return args_json
@compress
def _api_args_item(self, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
if item not in self.args:
abort(400, "Unknown argument item %s" % item)
try:
# Get the JSON value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = json_dumps(vars(self.args)[item])
except Exception as e:
abort(404, "Cannot get args item (%s)" % str(e))
return args_json
class EnableCors(object):
name = 'enable_cors'
api = 2
def apply(self, fn, context):
def _enable_cors(*args, **kwargs):
# set CORS headers
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
response.headers[
'Access-Control-Allow-Headers'
] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
if request.method != 'OPTIONS':
# actual request; reply with the actual response
return fn(*args, **kwargs)
return _enable_cors

View File

@ -139,10 +139,21 @@ class _GlancesCurses(object):
self.space_between_line = 2
# Init the curses screen
self.screen = curses.initscr()
if not self.screen:
logger.critical("Cannot init the curses library.\n")
sys.exit(1)
try:
self.screen = curses.initscr()
if not self.screen:
logger.critical("Cannot init the curses library.\n")
sys.exit(1)
else:
logger.debug("Curses library initialized with term: {}".format(curses.longname()))
except Exception as e:
if args.export:
logger.info("Cannot init the curses library, quiet mode on and export.")
args.quiet = True
return
else:
logger.critical("Cannot init the curses library ({})".format(e))
sys.exit(1)
# Load the 'outputs' section of the configuration file
# - Init the theme (default is black)
@ -236,6 +247,9 @@ class _GlancesCurses(object):
if curses.has_colors():
# The screen is compatible with a colored design
# ex: export TERM=xterm-256color
# export TERM=xterm-color
if self.is_theme('white'):
# White theme: black ==> white
curses.init_pair(1, curses.COLOR_BLACK, -1)
@ -244,35 +258,36 @@ class _GlancesCurses(object):
if self.args.disable_bg:
curses.init_pair(2, curses.COLOR_RED, -1)
curses.init_pair(3, curses.COLOR_GREEN, -1)
curses.init_pair(4, curses.COLOR_BLUE, -1)
curses.init_pair(5, curses.COLOR_MAGENTA, -1)
else:
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)
curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
curses.init_pair(4, curses.COLOR_BLUE, -1)
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(8, curses.COLOR_MAGENTA, -1)
# Colors text styles
self.no_color = curses.color_pair(1)
self.default_color = curses.color_pair(3) | A_BOLD
self.nice_color = curses.color_pair(5)
self.cpu_time_color = curses.color_pair(5)
self.nice_color = curses.color_pair(8)
self.cpu_time_color = curses.color_pair(8)
self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD
self.ifWARNING_color = curses.color_pair(5) | A_BOLD
self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD
self.default_color2 = curses.color_pair(7)
self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
self.ifWARNING_color2 = curses.color_pair(5) | A_BOLD
self.ifCAREFUL_color2 = curses.color_pair(4)
self.ifWARNING_color2 = curses.color_pair(8) | A_BOLD
self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
self.ifINFO_color = curses.color_pair(8)
self.ifINFO_color = curses.color_pair(4)
self.filter_color = A_BOLD
self.selected_color = A_BOLD
self.separator = curses.color_pair(1)
if curses.COLOR_PAIRS > 8:
colors_list = [curses.COLOR_MAGENTA, curses.COLOR_CYAN, curses.COLOR_YELLOW]
if curses.COLORS > 8:
# ex: export TERM=xterm-256color
colors_list = [curses.COLOR_CYAN, curses.COLOR_YELLOW]
for i in range(0, 3):
try:
curses.init_pair(i + 9, colors_list[i], -1)
@ -281,29 +296,32 @@ class _GlancesCurses(object):
curses.init_pair(i + 9, curses.COLOR_BLACK, -1)
else:
curses.init_pair(i + 9, curses.COLOR_WHITE, -1)
self.nice_color = curses.color_pair(9)
self.cpu_time_color = curses.color_pair(9)
self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
self.filter_color = curses.color_pair(10) | A_BOLD
self.selected_color = curses.color_pair(11) | A_BOLD
self.filter_color = curses.color_pair(9) | A_BOLD
self.selected_color = curses.color_pair(10) | A_BOLD
# Define separator line style
curses.init_color(11, 500, 500, 500)
curses.init_pair(11, curses.COLOR_BLACK, -1)
self.separator = curses.color_pair(11)
else:
# The screen is NOT compatible with a colored design
# switch to B&W text styles
# ex: export TERM=xterm-mono
self.no_color = curses.A_NORMAL
self.default_color = curses.A_NORMAL
self.nice_color = A_BOLD
self.cpu_time_color = A_BOLD
self.ifCAREFUL_color = curses.A_UNDERLINE
self.ifWARNING_color = A_BOLD
self.ifCAREFUL_color = A_BOLD
self.ifWARNING_color = curses.A_UNDERLINE
self.ifCRITICAL_color = curses.A_REVERSE
self.default_color2 = curses.A_NORMAL
self.ifCAREFUL_color2 = curses.A_UNDERLINE
self.ifWARNING_color2 = A_BOLD
self.ifCAREFUL_color2 = A_BOLD
self.ifWARNING_color2 = curses.A_UNDERLINE
self.ifCRITICAL_color2 = curses.A_REVERSE
self.ifINFO_color = A_BOLD
self.filter_color = A_BOLD
self.selected_color = A_BOLD
self.separator = curses.COLOR_BLACK
# Define the colors list (hash table) for stats
self.colors_list = {
@ -331,6 +349,7 @@ class _GlancesCurses(object):
'SELECTED': self.selected_color,
'INFO': self.ifINFO_color,
'ERROR': self.selected_color,
'SEPARATOR': self.separator,
}
def set_cursor(self, value):
@ -428,9 +447,7 @@ class _GlancesCurses(object):
)
def _handle_sort_key(self, hotkey):
glances_processes.set_sort_key(
self._hotkeys[hotkey]['sort_key'], self._hotkeys[hotkey]['sort_key'] == 'auto'
)
glances_processes.set_sort_key(self._hotkeys[hotkey]['sort_key'], self._hotkeys[hotkey]['sort_key'] == 'auto')
def _handle_enter(self):
self.edit_filter = not self.edit_filter
@ -578,7 +595,7 @@ class _GlancesCurses(object):
"""New column in the curses interface."""
self.column = self.next_column
def separator_line(self, color='TITLE'):
def separator_line(self, color='SEPARATOR'):
"""New separator line in the curses interface."""
if not self.args.enable_separator:
return
@ -1240,7 +1257,7 @@ class _GlancesCurses(object):
def wait(self, delay=100):
"""Wait delay in ms"""
curses.napms(100)
curses.napms(delay)
def get_stats_display_width(self, curse_msg, without_option=False):
"""Return the width of the formatted curses message."""

View File

@ -0,0 +1,740 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""RestFull API interface class."""
import os
import sys
import tempfile
from io import open
import webbrowser
import socket
from urllib.parse import urljoin
# Replace typing_extensions by typing when Python 3.8 support will be dropped
from typing import Annotated
from glances import __version__, __apiversion__
from glances.password import GlancesPassword
from glances.timer import Timer
from glances.logger import logger
# FastAPI import
try:
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Request
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import HTMLResponse, ORJSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
except ImportError:
logger.critical('FastAPI import error. Glances cannot start in web server mode.')
sys.exit(2)
try:
import uvicorn
except ImportError:
logger.critical('Uvicorn import error. Glances cannot start in web server mode.')
sys.exit(2)
security = HTTPBasic()
class GlancesRestfulApi(object):
"""This class manages the Restful API server."""
API_VERSION = __apiversion__
def __init__(self, config=None, args=None):
# Init config
self.config = config
# Init args
self.args = args
# Init stats
# Will be updated within Bottle route
self.stats = None
# cached_time is the minimum time interval between stats updates
# i.e. HTTP/RESTful calls will not retrieve updated info until the time
# since last update is passed (will retrieve old cached info instead)
self.timer = Timer(0)
# Load configuration file
self.load_config(config)
# Set the bind URL
self.bind_url = urljoin('http://{}:{}/'.format(self.args.bind_address, self.args.port), self.url_prefix)
# FastAPI Init
if self.args.password:
self._app = FastAPI(dependencies=[Depends(self.authentication)])
self._password = GlancesPassword(username=args.username, config=config)
else:
self._app = FastAPI()
self._password = None
# Change the default root path
if self.url_prefix != '/':
self._app.include_router(APIRouter(prefix=self.url_prefix))
# Set path for WebUI
self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public')
self.TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates')
self._templates = Jinja2Templates(directory=self.TEMPLATE_PATH)
# FastAPI Enable CORS
# https://fastapi.tiangolo.com/tutorial/cors/
self._app.add_middleware(
CORSMiddleware,
# allow_origins=["*"],
allow_origins=[self.bind_url],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# FastAPI Enable GZIP compression
# https://fastapi.tiangolo.com/advanced/middleware/
self._app.add_middleware(GZipMiddleware, minimum_size=1000)
# FastAPI Define routes
self._app.include_router(self._router())
def load_config(self, config):
"""Load the outputs section of the configuration file."""
# Limit the number of processes to display in the WebUI
self.url_prefix = '/'
if config is not None and config.has_section('outputs'):
n = config.get_value('outputs', 'max_processes_display', default=None)
logger.debug('Number of processes to display in the WebUI: {}'.format(n))
self.url_prefix = config.get_value('outputs', 'url_prefix', default='/')
logger.debug('URL prefix: {}'.format(self.url_prefix))
def __update__(self):
# Never update more than 1 time per cached_time
if self.timer.finished():
self.stats.update()
self.timer = Timer(self.args.cached_time)
def app(self):
return self._app()
def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
"""Check if a username/password combination is valid."""
if creds.username == self.args.username:
# check_password and get_hash are (lru) cached to optimize the requests
if self._password.check_password(self.args.password, self._password.get_hash(creds.password)):
return creds.username
# If the username/password combination is invalid, return an HTTP 401
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
def _router(self):
"""Define a custom router for Glances path."""
router = APIRouter()
# REST API
router.add_api_route(
'/api/%s/status' % self.API_VERSION,
status_code=status.HTTP_200_OK,
response_class=ORJSONResponse,
endpoint=self._api_status,
)
router.add_api_route(
'/api/%s/config' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_config
)
router.add_api_route(
'/api/%s/config/{section}' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_config_section,
)
router.add_api_route(
'/api/%s/config/{section}/{item}' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_config_section_item,
)
router.add_api_route('/api/%s/args' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_args)
router.add_api_route(
'/api/%s/args/{item}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_args_item
)
router.add_api_route(
'/api/%s/pluginslist' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_plugins
)
router.add_api_route('/api/%s/all' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_all)
router.add_api_route(
'/api/%s/all/limits' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_all_limits
)
router.add_api_route(
'/api/%s/all/views' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_all_views
)
router.add_api_route('/api/%s/help' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_help)
router.add_api_route('/api/%s/{plugin}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api)
router.add_api_route(
'/api/%s/{plugin}/history' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_history
)
router.add_api_route(
'/api/%s/{plugin}/history/{nb}' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_history,
)
router.add_api_route(
'/api/%s/{plugin}/top/{nb}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_top
)
router.add_api_route(
'/api/%s/{plugin}/limits' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_limits
)
router.add_api_route(
'/api/%s/{plugin}/views' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_views
)
router.add_api_route(
'/api/%s/{plugin}/{item}' % self.API_VERSION, response_class=ORJSONResponse, endpoint=self._api_item
)
router.add_api_route(
'/api/%s/{plugin}/{item}/history' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_item_history,
)
router.add_api_route(
'/api/%s/{plugin}/{item}/history/{nb}' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_item_history,
)
router.add_api_route(
'/api/%s/{plugin}/{item}/{value}' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_value,
)
# Restful API
bindmsg = 'Glances RESTful API Server started on {}api/{}'.format(self.bind_url, self.API_VERSION)
logger.info(bindmsg)
# WEB UI
if not self.args.disable_webui:
# Template for the root index.html file
router.add_api_route('/', response_class=HTMLResponse, endpoint=self._index)
# Statics files
self._app.mount("/static", StaticFiles(directory=self.STATIC_PATH), name="static")
bindmsg = 'Glances Web User Interface started on {}'.format(self.bind_url)
else:
bindmsg = 'The WebUI is disable (--disable-webui)'
logger.info(bindmsg)
print(bindmsg)
return router
def start(self, stats):
"""Start the bottle."""
# Init stats
self.stats = stats
# Init plugin list
self.plugins_list = self.stats.getPluginsList()
# Bind the Bottle TCP address/port
if self.args.open_web_browser:
# Implementation of the issue #946
# Try to open the Glances Web UI in the default Web browser if:
# 1) --open-web-browser option is used
# 2) Glances standalone mode is running on Windows OS
webbrowser.open(self.bind_url, new=2, autoraise=1)
# Run the Web application
try:
uvicorn.run(self._app, host=self.args.bind_address, port=self.args.port, access_log=self.args.debug)
except socket.error as e:
logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
def end(self):
"""End the Web server"""
logger.info("Close the Web server")
def _index(self, request: Request):
"""Return main index.html (/) file.
Parameters are available through the request object.
Example: http://localhost:61208/?refresh=5
Note: This function is only called the first time the page is loaded.
"""
refresh_time = request.query_params.get('refresh', default=max(1, int(self.args.time)))
# Update the stat
self.__update__()
# Display
return self._templates.TemplateResponse(
"index.html",
{
"request": request,
"refresh_time": refresh_time,
},
)
def _api_status(self):
"""Glances API RESTful implementation.
Return a 200 status code.
This entry point should be used to check the API health.
See related issue: Web server health check endpoint #1988
"""
return ORJSONResponse({'version': __version__})
def _api_help(self):
"""Glances API RESTful implementation.
Return the help data or 404 error.
"""
try:
plist = self.stats.get_plugin("help").get_view_data()
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get help view data (%s)" % str(e))
return ORJSONResponse(plist)
def _api_plugins(self):
"""Glances API RESTFul implementation.
@api {get} /api/%s/pluginslist Get plugins list
@apiVersion 2.0
@apiName pluginslist
@apiGroup plugin
@apiSuccess {String[]} Plugins list.
@apiSuccessExample Success-Response:
HTTP/1.1 200 OK
[
"load",
"help",
"ip",
"memswap",
"processlist",
...
]
@apiError Cannot get plugin list.
@apiErrorExample Error-Response:
HTTP/1.1 404 Not Found
"""
# Update the stat
self.__update__()
try:
plist = self.plugins_list
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get plugin list (%s)" % str(e))
return ORJSONResponse(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
"""
if self.args.debug:
fname = os.path.join(tempfile.gettempdir(), 'glances-debug.json')
try:
with open(fname) as f:
return f.read()
except IOError:
logger.debug("Debug file (%s) not found" % fname)
# Update the stat
self.__update__()
try:
# Get the RAW value of the stat ID
statval = self.stats.getAllAsDict()
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get stats (%s)" % str(e))
return ORJSONResponse(statval)
def _api_all_limits(self):
"""Glances API RESTful implementation.
Return the JSON representation of all the plugins limits
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
try:
# Get the RAW value of the stat limits
limits = self.stats.getAllLimitsAsDict()
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get limits (%s)" % str(e))
return ORJSONResponse(limits)
def _api_all_views(self):
"""Glances API RESTful implementation.
Return the JSON representation of all the plugins views
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
try:
# Get the RAW value of the stat view
limits = self.stats.getAllViewsAsDict()
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get views (%s)" % str(e))
return ORJSONResponse(limits)
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
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
# Update the stat
self.__update__()
try:
# Get the RAW value of the stat ID
statval = self.stats.get_plugin(plugin).get_raw()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get plugin %s (%s)" % (plugin, str(e))
)
return ORJSONResponse(statval)
def _api_top(self, plugin, nb: int = 0):
"""Glances API RESTful implementation.
Return the JSON representation of a given plugin limited to the top nb items.
It is used to reduce the payload of the HTTP response (example: processlist).
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
# Update the stat
self.__update__()
try:
# Get the RAW value of the stat ID
statval = self.stats.get_plugin(plugin).get_export()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get plugin %s (%s)" % (plugin, str(e))
)
if isinstance(statval, list):
statval = statval[:nb]
return ORJSONResponse(statval)
def _api_history(self, plugin, nb: int = 0):
"""Glances API RESTful implementation.
Return the JSON representation of a given plugin history
Limit to the last nb items (all if nb=0)
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
# Update the stat
self.__update__()
try:
# Get the RAW value of the stat ID
statval = self.stats.get_plugin(plugin).get_raw_history(nb=int(nb))
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get plugin history %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
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
try:
# Get the RAW value of the stat limits
ret = self.stats.get_plugin(plugin).limits
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get limits for plugin %s (%s)" % (plugin, str(e))
)
return ORJSONResponse(ret)
def _api_views(self, plugin):
"""Glances API RESTful implementation.
Return the JSON views of a given plugin
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
try:
# Get the RAW value of the stat views
ret = self.stats.get_plugin(plugin).get_views()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get views for plugin %s (%s)" % (plugin, str(e))
)
return ORJSONResponse(ret)
def _api_item(self, plugin, item):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/item
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
# Update the stat
self.__update__()
try:
# Get the RAW value of the stat views
ret = self.stats.get_plugin(plugin).get_raw_stats_item(item)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cannot get item %s in plugin %s (%s)" % (item, plugin, str(e)),
)
return ORJSONResponse(ret)
def _api_item_history(self, plugin, item, nb: int = 0):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/history of item
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
# Update the stat
self.__update__()
try:
# Get the RAW value of the stat history
ret = self.stats.get_plugin(plugin).get_raw_history(item, nb=nb)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get history for plugin %s (%s)" % (plugin, str(e))
)
return ORJSONResponse(ret)
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
"""
if plugin not in self.plugins_list:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list),
)
# Update the stat
self.__update__()
try:
# Get the RAW value
ret = self.stats.get_plugin(plugin).get_raw_stats_value(item, value)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cannot get %s = %s for plugin %s (%s)" % (item, value, plugin, str(e)),
)
return ORJSONResponse(ret)
def _api_config(self):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration file
HTTP/200 if OK
HTTP/404 if others error
"""
try:
# Get the RAW value of the config' dict
args_json = self.config.as_dict()
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get config (%s)" % str(e))
return ORJSONResponse(args_json)
def _api_config_section(self, section):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration section
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
config_dict = self.config.as_dict()
if section not in config_dict:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown configuration item %s" % section
)
try:
# Get the RAW value of the config' dict
ret_section = config_dict[section]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get config section %s (%s)" % (section, str(e))
)
return ORJSONResponse(ret_section)
def _api_config_section_item(self, section, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration section/item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
config_dict = self.config.as_dict()
if section not in config_dict:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown configuration item %s" % section
)
try:
# Get the RAW value of the config' dict section
ret_section = config_dict[section]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get config section %s (%s)" % (section, str(e))
)
try:
# Get the RAW value of the config' dict item
ret_item = ret_section[item]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cannot get item %s in config section %s (%s)" % (item, section, str(e)),
)
return ORJSONResponse(ret_item)
def _api_args(self):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments
HTTP/200 if OK
HTTP/404 if others error
"""
try:
# Get the RAW value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = vars(self.args)
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get args (%s)" % str(e))
return ORJSONResponse(args_json)
def _api_args_item(self, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
if item not in self.args:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown argument item %s" % item)
try:
# Get the RAW value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = vars(self.args)[item]
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get args item (%s)" % str(e))
return ORJSONResponse(args_json)

View File

@ -2,7 +2,7 @@
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
@ -13,10 +13,12 @@ from pprint import pformat
import json
import time
from glances import __apiversion__
from glances.logger import logger
from glances.globals import iteritems
API_URL = "http://localhost:61208/api/3"
API_URL = "http://localhost:61208/api/{api_version}".format(api_version=__apiversion__)
APIDOC_HEADER = """\
.. _api:
@ -33,12 +35,14 @@ The Glances Restfull/API server could be ran using the following command line:
API URL
-------
The default root API URL is ``http://localhost:61208/api/3``.
The default root API URL is ``http://localhost:61208/api/{api_version}``.
The bind address and port could be changed using the ``--bind`` and ``--port`` command line options.
It is also possible to define an URL prefix using the ``url_prefix`` option from the [outputs] section
of the Glances configuration file. The url_prefix should always end with a slash (``/``).
of the Glances configuration file.
Note: The url_prefix should always end with a slash (``/``).
For example:
@ -46,10 +50,23 @@ For example:
[outputs]
url_prefix = /glances/
will change the root API URL to ``http://localhost:61208/glances/api/3`` and the Web UI URL to
will change the root API URL to ``http://localhost:61208/glances/api/{api_version}`` and the Web UI URL to
``http://localhost:61208/glances/``
"""
API documentation
-----------------
The API documentation is available at the following URL: ``http://localhost:61208/docs#/``.
WebUI refresh
-------------
It is possible to change the Web UI refresh rate (default is 2 seconds) using the following option in the URL:
``http://localhost:61208/glances/?refresh=5``
""".format(
api_version=__apiversion__
)
def indent_stat(stat, indent=' '):
@ -67,7 +84,7 @@ def print_api_status():
print('-' * len(sub_title))
print('')
print('This entry point should be used to check the API status.')
print('It will return nothing but a 200 return code if everything is OK.')
print('It will the Glances version and a 200 return code if everything is OK.')
print('')
print('Get the Rest API status::')
print('')

View File

@ -65,7 +65,7 @@ static
|
|--- public # path where builds are put
|
|--- templates (bottle)
|--- templates
```
## Data

View File

@ -19,6 +19,15 @@ body {
display: table-cell;
text-align: right;
}
.width-50 {
width: 50px;
}
.width-75 {
width: 75px;
}
.width-100 {
width: 100px;
}
.plugin {
margin-bottom: 20px;
@ -116,6 +125,7 @@ body {
}
/* Plugins */
#processlist-plugin .table-cell {
padding: 0px 5px 0px 5px;
white-space: nowrap;

View File

@ -266,7 +266,7 @@ export default {
};
},
mounted() {
fetch('api/3/help', { method: 'GET' })
fetch('api/4/help', { method: 'GET' })
.then((response) => response.json())
.then((response) => (this.help = response));
}

View File

@ -11,6 +11,7 @@
<div class="table-row" v-for="(alert, alertId) in alerts" :key="alertId">
<div class="table-cell text-left">
{{ formatDate(alert.begin) }}
{{ alert.tz }}
({{ alert.ongoing ? 'ongoing' : alert.duration }}) -
<span v-show="!alert.ongoing"> {{ alert.level }} on </span>
<span :class="alert.level.toLowerCase()">
@ -41,10 +42,11 @@ export default {
alerts() {
return (this.stats || []).map((alertalertStats) => {
const alert = {};
var tzoffset = new Date().getTimezoneOffset();
alert.name = alertalertStats[3];
alert.level = alertalertStats[2];
alert.begin = alertalertStats[0] * 1000;
alert.end = alertalertStats[1] * 1000;
alert.begin = alertalertStats[0] * 1000 - tzoffset * 60 * 1000;
alert.end = alertalertStats[1] * 1000 - tzoffset * 60 * 1000;
alert.ongoing = alertalertStats[1] == -1;
alert.min = alertalertStats[6];
alert.mean = alertalertStats[5];

View File

@ -10,8 +10,8 @@
</div>
<div class="table-row" v-for="(fs, fsId) in fileSystems" :key="fsId">
<div class="table-cell text-left">
{{ fs.shortMountPoint }}
<span v-if="fs.shortMountPoint.length <= 12" class="visible-lg-inline">
{{ $filters.minSize(fs.alias ? fs.alias : fs.mountPoint, 36, begin=false) }}
<span v-if="(fs.alias ? fs.alias : fs.mountPoint).length + fs.name.length <= 34" class="visible-lg-inline">
({{ fs.name }})
</span>
</div>
@ -55,18 +55,14 @@ export default {
},
fileSystems() {
const fileSystems = this.stats.map((fsData) => {
let shortMountPoint = fsData['mnt_point'];
if (shortMountPoint.length > 22) {
shortMountPoint = '_' + fsData['mnt_point'].slice(-21);
}
return {
name: fsData['device_name'],
mountPoint: fsData['mnt_point'],
shortMountPoint: shortMountPoint,
percent: fsData['percent'],
size: fsData['size'],
used: fsData['used'],
free: fsData['free']
free: fsData['free'],
alias: fsData['alias'] !== undefined ? fsData['alias'] : null
};
});
return orderBy(fileSystems, ['mnt_point']);

View File

@ -64,13 +64,13 @@ export default {
}
function getColumnLabel(value) {
const labels = {
io_counters: 'disk IO',
cpu_percent: 'CPU consumption',
memory_percent: 'memory consumption',
cpu_times: 'process time',
username: 'user name',
name: 'process name',
timemillis: 'process time',
cpu_times: 'process time',
io_counters: 'disk IO',
name: 'process name',
None: 'None'
};
return labels[value] || value;

View File

@ -3,112 +3,88 @@
<section id="processlist-plugin" class="plugin">
<div class="table">
<div class="table-row">
<div
class="table-cell"
:class="['sortable', sorter.column === 'cpu_percent' && 'sort']"
@click="$emit('update:sorter', 'cpu_percent')"
>
<div class="table-cell width-50" :class="['sortable', sorter.column === 'cpu_percent' && 'sort']"
@click="$emit('update:sorter', 'cpu_percent')">
CPU%
</div>
<div
class="table-cell"
:class="['sortable', sorter.column === 'memory_percent' && 'sort']"
@click="$emit('update:sorter', 'memory_percent')"
>
<div class="table-cell width-50" :class="['sortable', sorter.column === 'memory_percent' && 'sort']"
@click="$emit('update:sorter', 'memory_percent')">
MEM%
</div>
<div class="table-cell hidden-xs hidden-sm">VIRT</div>
<div class="table-cell hidden-xs hidden-sm">RES</div>
<div class="table-cell">PID</div>
<div
class="table-cell text-left"
:class="['sortable', sorter.column === 'username' && 'sort']"
@click="$emit('update:sorter', 'username')"
>
<div class="table-cell width-75 hidden-xs hidden-sm">VIRT</div>
<div class="table-cell width-75 hidden-xs hidden-sm">RES</div>
<div class="table-cell width-75">PID</div>
<div class="table-cell width-100 text-left" :class="['sortable', sorter.column === 'username' && 'sort']"
@click="$emit('update:sorter', 'username')">
USER
</div>
<div
class="table-cell hidden-xs hidden-sm"
<div class="table-cell width-100 hidden-xs hidden-sm"
:class="['sortable', sorter.column === 'timemillis' && 'sort']"
@click="$emit('update:sorter', 'timemillis')"
>
@click="$emit('update:sorter', 'timemillis')">
TIME+
</div>
<div
class="table-cell text-left hidden-xs hidden-sm"
<div class="table-cell width-75 text-left hidden-xs hidden-sm"
:class="['sortable', sorter.column === 'num_threads' && 'sort']"
@click="$emit('update:sorter', 'num_threads')"
>
@click="$emit('update:sorter', 'num_threads')">
THR
</div>
<div class="table-cell">NI</div>
<div class="table-cell">S</div>
<div
v-show="ioReadWritePresent"
class="table-cell hidden-xs hidden-sm"
<div class="table-cell width-50">NI</div>
<div class="table-cell width-50">S</div>
<div v-show="ioReadWritePresent" class="table-cell width-75 hidden-xs hidden-sm"
:class="['sortable', sorter.column === 'io_counters' && 'sort']"
@click="$emit('update:sorter', 'io_counters')"
>
@click="$emit('update:sorter', 'io_counters')">
IOR/s
</div>
<div
v-show="ioReadWritePresent"
class="table-cell text-left hidden-xs hidden-sm"
<div v-show="ioReadWritePresent" class="table-cell width-75 text-left hidden-xs hidden-sm"
:class="['sortable', sorter.column === 'io_counters' && 'sort']"
@click="$emit('update:sorter', 'io_counters')"
>
@click="$emit('update:sorter', 'io_counters')">
IOW/s
</div>
<div
class="table-cell text-left"
:class="['sortable', sorter.column === 'name' && 'sort']"
@click="$emit('update:sorter', 'name')"
>
<div class="table-cell text-left" :class="['sortable', sorter.column === 'name' && 'sort']"
@click="$emit('update:sorter', 'name')">
Command
</div>
</div>
<div
class="table-row"
v-for="(process, processId) in processes"
:key="processId"
>
<div class="table-cell" :class="getCpuPercentAlert(process)">
<div class="table-row" v-for="(process, processId) in processes" :key="processId">
<div class="table-cell width-50" :class="getCpuPercentAlert(process)">
{{ process.cpu_percent == -1 ? '?' : $filters.number(process.cpu_percent, 1) }}
</div>
<div class="table-cell" :class="getMemoryPercentAlert(process)">
<div class="table-cell width-50" :class="getMemoryPercentAlert(process)">
{{ process.memory_percent == -1 ? '?' : $filters.number(process.memory_percent, 1) }}
</div>
<div class="table-cell hidden-xs hidden-sm">
<div class="table-cell width-75">
{{ $filters.bytes(process.memvirt) }}
</div>
<div class="table-cell hidden-xs hidden-sm">
<div class="table-cell width-75">
{{ $filters.bytes(process.memres) }}
</div>
<div class="table-cell">
<div class="table-cell width-75">
{{ process.pid }}
</div>
<div class="table-cell text-left">
<div class="table-cell width-100 text-left">
{{ process.username }}
</div>
<div class="table-cell hidden-xs hidden-sm" v-if="process.timeplus != '?'">
<div class="table-cell width-100 hidden-xs hidden-sm" v-if="process.timeplus != '?'">
<span v-show="process.timeplus.hours > 0" class="highlight">{{ process.timeplus.hours }}h</span>
{{ $filters.leftPad(process.timeplus.minutes, 2, '0') }}:{{ $filters.leftPad(process.timeplus.seconds, 2, '0') }}
<span v-show="process.timeplus.hours <= 0">.{{ $filters.leftPad(process.timeplus.milliseconds, 2, '0') }}</span>
{{ $filters.leftPad(process.timeplus.minutes, 2, '0') }}:{{ $filters.leftPad(process.timeplus.seconds,
2, '0') }}
<span v-show="process.timeplus.hours <= 0">.{{ $filters.leftPad(process.timeplus.milliseconds, 2, '0')
}}</span>
</div>
<div class="table-cell hidden-xs hidden-sm" v-if="process.timeplus == '?'">?</div>
<div class="table-cell text-left hidden-xs hidden-sm">
<div class="table-cell width-75 hidden-xs hidden-sm" v-if="process.timeplus == '?'">?</div>
<div class="table-cell width-75 text-left hidden-xs hidden-sm">
{{ process.num_threads == -1 ? '?' : process.num_threads }}
</div>
<div class="table-cell" :class="{ nice: process.isNice }">
<div class="table-cell width-50" :class="{ nice: process.isNice }">
{{ $filters.exclamation(process.nice) }}
</div>
<div class="table-cell" :class="{ status: process.status == 'R' }">
<div class="table-cell width-50" :class="{ status: process.status == 'R' }">
{{ process.status }}
</div>
<div class="table-cell hidden-xs hidden-sm" v-show="ioReadWritePresent">
<div class="table-cell width-75 hidden-xs hidden-sm" v-show="ioReadWritePresent">
{{ $filters.bytes(process.io_read) }}
</div>
<div class="table-cell text-left hidden-xs hidden-sm" v-show="ioReadWritePresent">
<div class="table-cell width-75 text-left hidden-xs hidden-sm" v-show="ioReadWritePresent">
{{ $filters.bytes(process.io_write) }}
</div>
<div class="table-cell text-left" v-show="args.process_short_name">
@ -159,8 +135,8 @@ export default {
process.memvirt = '?';
process.memres = '?';
if (process.memory_info) {
process.memvirt = process.memory_info[1];
process.memres = process.memory_info[0];
process.memvirt = process.memory_info.vms;
process.memres = process.memory_info.rss;
}
process.timeplus = '?';

View File

@ -74,10 +74,14 @@ export function limitTo(value, limit) {
return value.slice(0, limit);
}
export function minSize(input, max) {
export function minSize(input, max, begin = true) {
max = max || 8;
if (input.length > max) {
return '_' + input.substring(input.length - max + 1);
if (begin) {
return input.substring(0, max - 1) + '_';
} else {
return '_' + input.substring(input.length - max + 1);
}
}
return input;
}

View File

@ -2,15 +2,15 @@ import { store } from './store.js';
import Favico from 'favico.js';
// prettier-ignore
const fetchAll = () => fetch('api/3/all', { method: 'GET' }).then((response) => response.json());
const fetchAll = () => fetch('api/4/all', { method: 'GET' }).then((response) => response.json());
// prettier-ignore
const fetchAllViews = () => fetch('api/3/all/views', { method: 'GET' }).then((response) => response.json());
const fetchAllViews = () => fetch('api/4/all/views', { method: 'GET' }).then((response) => response.json());
// prettier-ignore
const fetchAllLimits = () => fetch('api/3/all/limits', { method: 'GET' }).then((response) => response.json());
const fetchAllLimits = () => fetch('api/4/all/limits', { method: 'GET' }).then((response) => response.json());
// prettier-ignore
const fetchArgs = () => fetch('api/3/args', { method: 'GET' }).then((response) => response.json());
const fetchArgs = () => fetch('api/4/args', { method: 'GET' }).then((response) => response.json());
// prettier-ignore
const fetchConfig = () => fetch('api/3/config', { method: 'GET' }).then((response) => response.json());
const fetchConfig = () => fetch('api/4/config', { method: 'GET' }).then((response) => response.json());
class GlancesHelperService {
limits = {};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Glances</title>
<link rel="icon" type="image/x-icon" href="static/favicon.ico" />
<script>
window.__GLANCES__ = {
'refresh-time': '{{ refresh_time }}'
}
</script>
<script src="static/glances.js" defer></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Glances</title>
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<script>
window.__GLANCES__ = {
'refresh-time': '{{ refresh_time }}'
}
</script>
<script src="glances.js" defer></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -2,7 +2,7 @@
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
@ -16,7 +16,7 @@ import sys
import uuid
from io import open
from glances.globals import b, safe_makedirs
from glances.globals import b, safe_makedirs, weak_lru_cache
from glances.config import user_config_dir
from glances.logger import logger
@ -42,21 +42,25 @@ class GlancesPassword(object):
else:
return self.config.get_value('passwords', 'local_password_path', default=user_config_dir())
@weak_lru_cache(maxsize=32)
def get_hash(self, plain_password, salt=''):
"""Return the hashed password, salt + pbkdf2_hmac."""
return hashlib.pbkdf2_hmac('sha256', plain_password.encode(), salt.encode(), 100000, dklen=128).hex()
@weak_lru_cache(maxsize=32)
def hash_password(self, plain_password):
"""Hash password with a salt based on UUID (universally unique identifier)."""
salt = uuid.uuid4().hex
encrypted_password = self.get_hash(plain_password, salt=salt)
return salt + '$' + encrypted_password
@weak_lru_cache(maxsize=32)
def check_password(self, hashed_password, plain_password):
"""Encode the plain_password with the salt of the hashed_password.
Return the comparison with the encrypted_password.
"""
logger.info("Check password")
salt, encrypted_password = hashed_password.split('$')
re_encrypted_password = self.get_hash(plain_password, salt=salt)
return encrypted_password == re_encrypted_password

View File

@ -6,23 +6,16 @@ This is the Glances plugins folder.
A Glances plugin is a Python module hosted in a folder.
It should be based on the MVC model.
- model: data model (where the stats will be updated)
- view: input for UI (where the stats are displayed)
- controler: output from UI (where the stats are controled)
It should implement a Class named PluginModel (inherited from GlancesPluginModel).
////
TODO
////
This class should be based on the MVC model.
- model: where the stats are updated (update method)
- view: where the stats are prepare to be displayed (update_views)
- controler: where the stats are displayed (msg_curse method)
A plugin should define the following global variables:
- fields_description: a dict twith the field description/option
- items_history_list: define items history
- items_history_list (optional): define items history
A plugin should implement the following methods:
- update(): update the self.stats variable (most of the time a dict or a list of dict)
- msg_curse(): return a list of messages to display in UI
Have a look of all Glances plugin's methods in the plugin.py file.
Have a look of all Glances plugin's methods in the plugin folder (where the GlancesPluginModel is defined).

View File

@ -0,0 +1,250 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Alert plugin."""
from datetime import datetime
from time import tzname
import pytz
from glances.events import glances_events
from glances.thresholds import glances_thresholds
# from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# Static decision tree for the global alert message
# - msg: Message to be displayed (result of the decision tree)
# - thresholds: a list of stats to take into account
# - thresholds_min: minimal value of the thresholds sum
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree = [
{'msg': 'No warning or critical alert detected', 'thresholds': [], 'thresholds_min': 0},
{'msg': 'High CPU user mode', 'thresholds': ['cpu_user'], 'thresholds_min': 2},
{'msg': 'High CPU kernel usage', 'thresholds': ['cpu_system'], 'thresholds_min': 2},
{'msg': 'High CPU I/O waiting', 'thresholds': ['cpu_iowait'], 'thresholds_min': 2},
{
'msg': 'Large CPU stolen time. System running the hypervisor is too busy.',
'thresholds': ['cpu_steal'],
'thresholds_min': 2,
},
{'msg': 'High CPU niced value', 'thresholds': ['cpu_niced'], 'thresholds_min': 2},
{'msg': 'System overloaded in the last 5 minutes', 'thresholds': ['load'], 'thresholds_min': 2},
{'msg': 'High swap (paging) usage', 'thresholds': ['memswap'], 'thresholds_min': 2},
{'msg': 'High memory consumption', 'thresholds': ['mem'], 'thresholds_min': 2},
]
# TODO: change the algo to use the following decision tree
# Source: Inspire by https://scoutapm.com/blog/slow_server_flow_chart
# _yes means threshold >= 2
# _no means threshold < 2
# With threshold:
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree_new = {
'cpu_iowait': {
'_yes': {
'memswap': {
'_yes': {
'mem': {
'_yes': {
# Once you've identified the offenders, the resolution will again
# depend on whether their memory usage seems business-as-usual or not.
# For example, a memory leak can be satisfactorily addressed by a one-time
# or periodic restart of the process.
# - if memory usage seems anomalous: kill the offending processes.
# - if memory usage seems business-as-usual: add RAM to the server,
# or split high-memory using services to other servers.
'_msg': "Memory issue"
},
'_no': {
# ???
'_msg': "Swap issue"
},
}
},
'_no': {
# Low swap means you have a "real" IO wait problem. The next step is to see what's hogging your IO.
# iotop is an awesome tool for identifying io offenders. Two things to note:
# unless you've already installed iotop, it's probably not already on your system.
# Recommendation: install it before you need it - - it's no fun trying to install a troubleshooting
# tool on an overloaded machine (iotop requires a Linux of 2.62 or above)
'_msg': "I/O issue"
},
}
},
'_no': {
'cpu_total': {
'_yes': {
'cpu_user': {
'_yes': {
# We expect the user-time percentage to be high.
# There's most likely a program or service you've configured on you server that's
# hogging CPU.
# Checking the % user time just confirms this. When you see that the % user-time is high,
# it's time to see what executable is monopolizing the CPU
# Once you've confirmed that the % usertime is high, check the process list(also provided
# by top).
# Be default, top sorts the process list by % CPU, so you can just look at the top process
# or processes.
# If there's a single process hogging the CPU in a way that seems abnormal, it's an
# anomalous situation
# that a service restart can fix. If there are are multiple processes taking up CPU
# resources, or it
# there's one process that takes lots of resources while otherwise functioning normally,
# than your setup
# may just be underpowered. You'll need to upgrade your server(add more cores),
# or split services out onto
# other boxes. In either case, you have a resolution:
# - if situation seems anomalous: kill the offending processes.
# - if situation seems typical given history: upgrade server or add more servers.
'_msg': "CPU issue with user process(es)"
},
'_no': {
'cpu_steal': {
'_yes': {
'_msg': "CPU issue with stolen time. System running the hypervisor may be too busy."
},
'_no': {'_msg': "CPU issue with system process(es)"},
}
},
}
},
'_no': {
'_yes': {
# ???
'_msg': "Memory issue"
},
'_no': {
# Your slowness isn't due to CPU or IO problems, so it's likely an application-specific issue.
# It's also possible that the slowness is being caused by another server in your cluster, or
# by an external service you rely on.
# start by checking important applications for uncharacteristic slowness(the DB is a good place
# to start), think through which parts of your infrastructure could be slowed down externally.
# For example, do you use an externally hosted email service that could slow down critical
# parts of your application ?
# If you suspect another server in your cluster, strace and lsof can provide information on
# what the process is doing or waiting on. Strace will show you which file descriptors are
# being read or written to (or being attempted to be read from) and lsof can give you a
# mapping of those file descriptors to network connections.
'_msg': "External issue"
},
},
}
},
}
}
def global_message():
"""Parse the decision tree and return the message.
Note: message corresponding to the current thresholds values
"""
# Compute the weight for each item in the tree
current_thresholds = glances_thresholds.get()
for i in tree:
i['weight'] = sum([current_thresholds[t].value() for t in i['thresholds'] if t in current_thresholds])
themax = max(tree, key=lambda d: d['weight'])
if themax['weight'] >= themax['thresholds_min']:
# Check if the weight is > to the minimal threshold value
return themax['msg']
else:
return tree[0]['msg']
class PluginModel(GlancesPluginModel):
"""Glances alert plugin.
Only for display.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
# Set the maximum number of events to display
if config is not None and (config.has_section('alert') or config.has_section('alerts')):
glances_events.set_max_events(config.get_int_value('alert', 'max_events', default=10))
def update(self):
"""Nothing to do here. Just return the global glances_log."""
# Set the stats to the glances_events
self.stats = glances_events.get()
# Define the global message thanks to the current thresholds
# and the decision tree
# !!! Call directly in the msg_curse function
# global_message()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Build the string message
# Header
ret.append(self.curse_add_line(global_message(), "TITLE"))
# Loop over alerts
for alert in self.stats:
# New line
ret.append(self.curse_new_line())
# Start
msg = str(datetime.fromtimestamp(alert[0], tz=pytz.timezone(tzname[0] if tzname[0] else 'UTC')))
ret.append(self.curse_add_line(msg))
# Duration
if alert[1] > 0:
# If finished display duration
msg = ' ({})'.format(datetime.fromtimestamp(alert[1]) - datetime.fromtimestamp(alert[0]))
else:
msg = ' (ongoing)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_add_line(" - "))
# Infos
if alert[1] > 0:
# If finished do not display status
msg = '{} on {}'.format(alert[2], alert[3])
ret.append(self.curse_add_line(msg))
else:
msg = str(alert[3])
ret.append(self.curse_add_line(msg, decoration=alert[2]))
# Min / Mean / Max
if self.approx_equal(alert[6], alert[4], tolerance=0.1):
msg = ' ({:.1f})'.format(alert[5])
else:
msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert[6], alert[5], alert[4])
ret.append(self.curse_add_line(msg))
# Top processes
top_process = ', '.join([p['name'] for p in alert[9]])
if top_process != '':
msg = ': {}'.format(top_process)
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

View File

@ -1,251 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Alert plugin."""
from datetime import datetime
from glances.logger import logger
from glances.events import glances_events
from glances.thresholds import glances_thresholds
# from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# Static decision tree for the global alert message
# - msg: Message to be displayed (result of the decision tree)
# - thresholds: a list of stats to take into account
# - thresholds_min: minimal value of the thresholds sum
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree = [
{'msg': 'No warning or critical alert detected', 'thresholds': [], 'thresholds_min': 0},
{'msg': 'High CPU user mode', 'thresholds': ['cpu_user'], 'thresholds_min': 2},
{'msg': 'High CPU kernel usage', 'thresholds': ['cpu_system'], 'thresholds_min': 2},
{'msg': 'High CPU I/O waiting', 'thresholds': ['cpu_iowait'], 'thresholds_min': 2},
{
'msg': 'Large CPU stolen time. System running the hypervisor is too busy.',
'thresholds': ['cpu_steal'],
'thresholds_min': 2,
},
{'msg': 'High CPU niced value', 'thresholds': ['cpu_niced'], 'thresholds_min': 2},
{'msg': 'System overloaded in the last 5 minutes', 'thresholds': ['load'], 'thresholds_min': 2},
{'msg': 'High swap (paging) usage', 'thresholds': ['memswap'], 'thresholds_min': 2},
{'msg': 'High memory consumption', 'thresholds': ['mem'], 'thresholds_min': 2},
]
# TODO: change the algo to use the following decision tree
# Source: Inspire by https://scoutapm.com/blog/slow_server_flow_chart
# _yes means threshold >= 2
# _no means threshold < 2
# With threshold:
# - 0: OK
# - 1: CAREFUL
# - 2: WARNING
# - 3: CRITICAL
tree_new = {
'cpu_iowait': {
'_yes': {
'memswap': {
'_yes': {
'mem': {
'_yes': {
# Once you've identified the offenders, the resolution will again
# depend on whether their memory usage seems business-as-usual or not.
# For example, a memory leak can be satisfactorily addressed by a one-time
# or periodic restart of the process.
# - if memory usage seems anomalous: kill the offending processes.
# - if memory usage seems business-as-usual: add RAM to the server,
# or split high-memory using services to other servers.
'_msg': "Memory issue"
},
'_no': {
# ???
'_msg': "Swap issue"
},
}
},
'_no': {
# Low swap means you have a "real" IO wait problem. The next step is to see what's hogging your IO.
# iotop is an awesome tool for identifying io offenders. Two things to note:
# unless you've already installed iotop, it's probably not already on your system.
# Recommendation: install it before you need it - - it's no fun trying to install a troubleshooting
# tool on an overloaded machine (iotop requires a Linux of 2.62 or above)
'_msg': "I/O issue"
},
}
},
'_no': {
'cpu_total': {
'_yes': {
'cpu_user': {
'_yes': {
# We expect the user-time percentage to be high.
# There's most likely a program or service you've configured on you server that's
# hogging CPU.
# Checking the % user time just confirms this. When you see that the % user-time is high,
# it's time to see what executable is monopolizing the CPU
# Once you've confirmed that the % usertime is high, check the process list(also provided
# by top).
# Be default, top sorts the process list by % CPU, so you can just look at the top process
# or processes.
# If there's a single process hogging the CPU in a way that seems abnormal, it's an
# anomalous situation
# that a service restart can fix. If there are are multiple processes taking up CPU
# resources, or it
# there's one process that takes lots of resources while otherwise functioning normally,
# than your setup
# may just be underpowered. You'll need to upgrade your server(add more cores),
# or split services out onto
# other boxes. In either case, you have a resolution:
# - if situation seems anomalous: kill the offending processes.
# - if situation seems typical given history: upgrade server or add more servers.
'_msg': "CPU issue with user process(es)"
},
'_no': {
'cpu_steal': {
'_yes': {
'_msg': "CPU issue with stolen time. System running the hypervisor may be too busy."
},
'_no': {'_msg': "CPU issue with system process(es)"},
}
},
}
},
'_no': {
'_yes': {
# ???
'_msg': "Memory issue"
},
'_no': {
# Your slowness isn't due to CPU or IO problems, so it's likely an application-specific issue.
# It's also possible that the slowness is being caused by another server in your cluster, or
# by an external service you rely on.
# start by checking important applications for uncharacteristic slowness(the DB is a good place
# to start), think through which parts of your infrastructure could be slowed down externally.
# For example, do you use an externally hosted email service that could slow down critical
# parts of your application ?
# If you suspect another server in your cluster, strace and lsof can provide information on
# what the process is doing or waiting on. Strace will show you which file descriptors are
# being read or written to (or being attempted to be read from) and lsof can give you a
# mapping of those file descriptors to network connections.
'_msg': "External issue"
},
},
}
},
}
}
def global_message():
"""Parse the decision tree and return the message.
Note: message corresponding to the current thresholds values
"""
# Compute the weight for each item in the tree
current_thresholds = glances_thresholds.get()
for i in tree:
i['weight'] = sum([current_thresholds[t].value() for t in i['thresholds'] if t in current_thresholds])
themax = max(tree, key=lambda d: d['weight'])
if themax['weight'] >= themax['thresholds_min']:
# Check if the weight is > to the minimal threshold value
return themax['msg']
else:
return tree[0]['msg']
class PluginModel(GlancesPluginModel):
"""Glances alert plugin.
Only for display.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args,
config=config,
stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
# Set the maximum number of events to display
if config is not None and (config.has_section('alert') or config.has_section('alerts')):
glances_events.set_max_events(config.get_int_value('alert', 'max_events'))
def update(self):
"""Nothing to do here. Just return the global glances_log."""
# Set the stats to the glances_events
self.stats = glances_events.get()
# Define the global message thanks to the current thresholds
# and the decision tree
# !!! Call directly in the msg_curse function
# global_message()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Build the string message
# Header
ret.append(self.curse_add_line(global_message(), "TITLE"))
# Loop over alerts
for alert in self.stats:
# New line
ret.append(self.curse_new_line())
# Start
msg = str(datetime.fromtimestamp(alert[0]))
ret.append(self.curse_add_line(msg))
# Duration
if alert[1] > 0:
# If finished display duration
msg = ' ({})'.format(datetime.fromtimestamp(alert[1]) - datetime.fromtimestamp(alert[0]))
else:
msg = ' (ongoing)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_add_line(" - "))
# Infos
if alert[1] > 0:
# If finished do not display status
msg = '{} on {}'.format(alert[2], alert[3])
ret.append(self.curse_add_line(msg))
else:
msg = str(alert[3])
ret.append(self.curse_add_line(msg, decoration=alert[2]))
# Min / Mean / Max
if self.approx_equal(alert[6], alert[4], tolerance=0.1):
msg = ' ({:.1f})'.format(alert[5])
else:
msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert[6], alert[5], alert[4])
ret.append(self.curse_add_line(msg))
# Top processes
top_process = ', '.join([p['name'] for p in alert[9]])
if top_process != '':
msg = ': {}'.format(top_process)
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

View File

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Monitor plugin."""
from glances.globals import iteritems
from glances.amps_list import AmpsList as glancesAmpsList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances AMPs plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the list of AMP (classes define in the glances/amps_list.py script)
self.glances_amps = glancesAmpsList(self.args, self.config)
def get_key(self):
"""Return the key of the list."""
return 'name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the AMP list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
for k, v in iteritems(self.glances_amps.update()):
stats.append(
{
'key': self.get_key(),
'name': v.NAME,
'result': v.result(),
'refresh': v.refresh(),
'timer': v.time_until_refresh(),
'count': v.count(),
'countmin': v.count_min(),
'countmax': v.count_max(),
'regex': v.regex() is not None,
},
)
else:
# Not available in SNMP mode
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, nbprocess=0, countmin=None, countmax=None, header="", log=False):
"""Return the alert status relative to the process number."""
if nbprocess is None:
return 'OK'
if countmin is None:
countmin = nbprocess
if countmax is None:
countmax = nbprocess
if nbprocess > 0:
if int(countmin) <= int(nbprocess) <= int(countmax):
return 'OK'
else:
return 'WARNING'
else:
if int(countmin) == 0:
return 'OK'
else:
return 'CRITICAL'
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_process or self.is_disabled():
return ret
# Build the string message
for m in self.stats:
# Only display AMP if a result exist
if m['result'] is None:
continue
# Display AMP
first_column = '{}'.format(m['name'])
first_column_style = self.get_alert(m['count'], m['countmin'], m['countmax'])
second_column = '{}'.format(m['count'] if m['regex'] else '')
for line in m['result'].split('\n'):
# Display first column with the process name...
msg = '{:<16} '.format(first_column)
ret.append(self.curse_add_line(msg, first_column_style))
# ... and second column with the number of matching processes...
msg = '{:<4} '.format(second_column)
ret.append(self.curse_add_line(msg))
# ... only on the first line
first_column = second_column = ''
# Display AMP result in the third column
ret.append(self.curse_add_line(line, splittable=True))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret

View File

@ -1,123 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Monitor plugin."""
from glances.globals import iteritems
from glances.amps_list import AmpsList as glancesAmpsList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances AMPs plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the list of AMP (classes define in the glances/amps_list.py script)
self.glances_amps = glancesAmpsList(self.args, self.config)
def get_key(self):
"""Return the key of the list."""
return 'name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the AMP list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
for k, v in iteritems(self.glances_amps.update()):
stats.append(
{
'key': self.get_key(),
'name': v.NAME,
'result': v.result(),
'refresh': v.refresh(),
'timer': v.time_until_refresh(),
'count': v.count(),
'countmin': v.count_min(),
'countmax': v.count_max(),
'regex': v.regex() is not None,
},
)
else:
# Not available in SNMP mode
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, nbprocess=0, countmin=None, countmax=None, header="", log=False):
"""Return the alert status relative to the process number."""
if nbprocess is None:
return 'OK'
if countmin is None:
countmin = nbprocess
if countmax is None:
countmax = nbprocess
if nbprocess > 0:
if int(countmin) <= int(nbprocess) <= int(countmax):
return 'OK'
else:
return 'WARNING'
else:
if int(countmin) == 0:
return 'OK'
else:
return 'CRITICAL'
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_process or self.is_disabled():
return ret
# Build the string message
for m in self.stats:
# Only display AMP if a result exist
if m['result'] is None:
continue
# Display AMP
first_column = '{}'.format(m['name'])
first_column_style = self.get_alert(m['count'], m['countmin'], m['countmax'])
second_column = '{}'.format(m['count'] if m['regex'] else '')
for line in m['result'].split('\n'):
# Display first column with the process name...
msg = '{:<16} '.format(first_column)
ret.append(self.curse_add_line(msg, first_column_style))
# ... and second column with the number of matching processes...
msg = '{:<4} '.format(second_column)
ret.append(self.curse_add_line(msg))
# ... only on the first line
first_column = second_column = ''
# Display AMP result in the third column
ret.append(self.curse_add_line(line, splittable=True))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret

View File

@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Cloud plugin.
Supported Cloud API:
- OpenStack meta data (class ThreadOpenStack) - Vanilla OpenStack
- OpenStackEC2 meta data (class ThreadOpenStackEC2) - Amazon EC2 compatible
"""
import threading
from glances.globals import iteritems, to_ascii
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Import plugin specific dependency
try:
import requests
except ImportError as e:
import_error_tag = True
# Display debug message if import error
logger.warning("Missing Python Lib ({}), Cloud plugin is disabled".format(e))
else:
import_error_tag = False
class PluginModel(GlancesPluginModel):
"""Glances' cloud plugin.
The goal of this plugin is to retrieve additional information
concerning the datacenter where the host is connected.
See https://github.com/nicolargo/glances/issues/1029
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.reset()
# Init thread to grab OpenStack stats asynchronously
self.OPENSTACK = ThreadOpenStack()
self.OPENSTACKEC2 = ThreadOpenStackEC2()
# Run the thread
self.OPENSTACK.start()
self.OPENSTACKEC2.start()
def exit(self):
"""Overwrite the exit method to close threads."""
self.OPENSTACK.stop()
self.OPENSTACKEC2.stop()
# Call the father class
super(PluginModel, self).exit()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the cloud stats.
Return the stats (dict)
"""
# Init new stats
stats = self.get_init_value()
# Requests lib is needed to get stats from the Cloud API
if import_error_tag:
return stats
# Update the stats
if self.input_method == 'local':
stats = self.OPENSTACK.stats
if not stats:
stats = self.OPENSTACKEC2.stats
# Example:
# Uncomment to test on physical computer (only for test purpose)
# stats = {'id': 'ami-id',
# 'name': 'My VM',
# 'type': 'Gold',
# 'region': 'France',
# 'platform': 'OpenStack'}
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.stats == {} or self.is_disabled():
return ret
# Generate the output
msg = self.stats.get('platform', 'Unknown')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {} instance {} ({})'.format(
self.stats.get('type', 'Unknown'), self.stats.get('name', 'Unknown'), self.stats.get('region', 'Unknown')
)
ret.append(self.curse_add_line(msg))
# Return the message with decoration
# logger.info(ret)
return ret
class ThreadOpenStack(threading.Thread):
"""
Specific thread to grab OpenStack stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "OpenStack"
OPENSTACK_API_URL = 'http://169.254.169.254/openstack/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'project_id',
'name': 'name',
'type': 'meta/role',
'region': 'availability_zone',
}
def __init__(self):
"""Init the class."""
logger.debug("cloud plugin - Create thread for OpenStack metadata")
super(ThreadOpenStack, self).__init__()
# Event needed to stop properly the thread
self._stopper = threading.Event()
# The class return the stats as a dict
self._stats = {}
def run(self):
"""Grab plugin's stats.
Infinite loop, should be stopped by calling the stop() method
"""
if import_error_tag:
self.stop()
return False
for k, v in iteritems(self.OPENSTACK_API_METADATA):
r_url = '{}/{}'.format(self.OPENSTACK_API_URL, v)
try:
# Local request, a timeout of 3 seconds is OK
r = requests.get(r_url, timeout=3)
except Exception as e:
logger.debug('cloud plugin - Cannot connect to the OpenStack metadata API {}: {}'.format(r_url, e))
break
else:
if r.ok:
self._stats[k] = to_ascii(r.content)
else:
# No break during the loop, so we can set the platform
self._stats['platform'] = self.OPENSTACK_PLATFORM
return True
@property
def stats(self):
"""Stats getter."""
return self._stats
@stats.setter
def stats(self, value):
"""Stats setter."""
self._stats = value
def stop(self, timeout=None):
"""Stop the thread."""
logger.debug("cloud plugin - Close thread for OpenStack metadata")
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()
class ThreadOpenStackEC2(ThreadOpenStack):
"""
Specific thread to grab OpenStack EC2 (Amazon cloud) stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "Amazon EC2"
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'ami-id',
'name': 'instance-id',
'type': 'instance-type',
'region': 'placement/availability-zone',
}

View File

@ -1,220 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Cloud plugin.
Supported Cloud API:
- OpenStack meta data (class ThreadOpenStack) - Vanilla OpenStack
- OpenStackEC2 meta data (class ThreadOpenStackEC2) - Amazon EC2 compatible
"""
import threading
from glances.globals import iteritems, to_ascii
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Import plugin specific dependency
try:
import requests
except ImportError as e:
import_error_tag = True
# Display debug message if import error
logger.warning("Missing Python Lib ({}), Cloud plugin is disabled".format(e))
else:
import_error_tag = False
class PluginModel(GlancesPluginModel):
"""Glances' cloud plugin.
The goal of this plugin is to retrieve additional information
concerning the datacenter where the host is connected.
See https://github.com/nicolargo/glances/issues/1029
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.reset()
# Init thread to grab OpenStack stats asynchronously
self.OPENSTACK = ThreadOpenStack()
self.OPENSTACKEC2 = ThreadOpenStackEC2()
# Run the thread
self.OPENSTACK.start()
self.OPENSTACKEC2.start()
def exit(self):
"""Overwrite the exit method to close threads."""
self.OPENSTACK.stop()
self.OPENSTACKEC2.stop()
# Call the father class
super(PluginModel, self).exit()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the cloud stats.
Return the stats (dict)
"""
# Init new stats
stats = self.get_init_value()
# Requests lib is needed to get stats from the Cloud API
if import_error_tag:
return stats
# Update the stats
if self.input_method == 'local':
stats = self.OPENSTACK.stats
if not stats:
stats = self.OPENSTACKEC2.stats
# Example:
# Uncomment to test on physical computer (only for test purpose)
# stats = {'id': 'ami-id',
# 'name': 'My VM',
# 'type': 'Gold',
# 'region': 'France',
# 'platform': 'OpenStack'}
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.stats == {} or self.is_disabled():
return ret
# Generate the output
msg = self.stats.get('platform', 'Unknown')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {} instance {} ({})'.format(
self.stats.get('type', 'Unknown'), self.stats.get('name', 'Unknown'), self.stats.get('region', 'Unknown')
)
ret.append(self.curse_add_line(msg))
# Return the message with decoration
# logger.info(ret)
return ret
class ThreadOpenStack(threading.Thread):
"""
Specific thread to grab OpenStack stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "OpenStack"
OPENSTACK_API_URL = 'http://169.254.169.254/openstack/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'project_id',
'name': 'name',
'type': 'meta/role',
'region': 'availability_zone',
}
def __init__(self):
"""Init the class."""
logger.debug("cloud plugin - Create thread for OpenStack metadata")
super(ThreadOpenStack, self).__init__()
# Event needed to stop properly the thread
self._stopper = threading.Event()
# The class return the stats as a dict
self._stats = {}
def run(self):
"""Grab plugin's stats.
Infinite loop, should be stopped by calling the stop() method
"""
if import_error_tag:
self.stop()
return False
for k, v in iteritems(self.OPENSTACK_API_METADATA):
r_url = '{}/{}'.format(self.OPENSTACK_API_URL, v)
try:
# Local request, a timeout of 3 seconds is OK
r = requests.get(r_url, timeout=3)
except Exception as e:
logger.debug('cloud plugin - Cannot connect to the OpenStack metadata API {}: {}'.format(r_url, e))
break
else:
if r.ok:
self._stats[k] = to_ascii(r.content)
else:
# No break during the loop, so we can set the platform
self._stats['platform'] = self.OPENSTACK_PLATFORM
return True
@property
def stats(self):
"""Stats getter."""
return self._stats
@stats.setter
def stats(self, value):
"""Stats setter."""
self._stats = value
def stop(self, timeout=None):
"""Stop the thread."""
logger.debug("cloud plugin - Close thread for OpenStack metadata")
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()
class ThreadOpenStackEC2(ThreadOpenStack):
"""
Specific thread to grab OpenStack EC2 (Amazon cloud) stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "Amazon EC2"
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'ami-id',
'name': 'instance-id',
'type': 'instance-type',
'region': 'placement/availability-zone',
}

View File

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Connections plugin."""
from __future__ import unicode_literals
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
from glances.globals import nativestr
import psutil
# Define the history items list
# items_history_list = [{'name': 'rx',
# 'description': 'Download rate per second',
# 'y_unit': 'bit/s'},
# {'name': 'tx',
# 'description': 'Upload rate per second',
# 'y_unit': 'bit/s'}]
class PluginModel(GlancesPluginModel):
"""Glances connections plugin.
stats is a dict
"""
status_list = [psutil.CONN_LISTEN, psutil.CONN_ESTABLISHED]
initiated_states = [psutil.CONN_SYN_SENT, psutil.CONN_SYN_RECV]
terminated_states = [
psutil.CONN_FIN_WAIT1,
psutil.CONN_FIN_WAIT2,
psutil.CONN_TIME_WAIT,
psutil.CONN_CLOSE,
psutil.CONN_CLOSE_WAIT,
psutil.CONN_LAST_ACK,
]
conntrack = {
'nf_conntrack_count': '/proc/sys/net/netfilter/nf_conntrack_count',
'nf_conntrack_max': '/proc/sys/net/netfilter/nf_conntrack_max',
}
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
# items_history_list=items_history_list,
stats_init_value={'net_connections_enabled': True, 'nf_conntrack_enabled': True},
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update connections stats using the input method.
Stats is a dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the PSUtils lib
# Grab network interface stat using the psutil net_connections method
if stats['net_connections_enabled']:
try:
net_connections = psutil.net_connections(kind="tcp")
except Exception as e:
logger.warning('Can not get network connections stats ({})'.format(e))
logger.info('Disable connections stats')
stats['net_connections_enabled'] = False
self.stats = stats
return self.stats
for s in self.status_list:
stats[s] = len([c for c in net_connections if c.status == s])
initiated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
initiated += stats[s]
stats['initiated'] = initiated
terminated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
terminated += stats[s]
stats['terminated'] = terminated
if stats['nf_conntrack_enabled']:
# Grab connections track directly from the /proc file
for i in self.conntrack:
try:
with open(self.conntrack[i], 'r') as f:
stats[i] = float(f.readline().rstrip("\n"))
except (IOError, FileNotFoundError) as e:
logger.warning('Can not get network connections track ({})'.format(e))
logger.info('Disable connections track')
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
if 'nf_conntrack_max' in stats and 'nf_conntrack_count' in stats:
stats['nf_conntrack_percent'] = stats['nf_conntrack_count'] * 100 / stats['nf_conntrack_max']
else:
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
elif self.input_method == 'snmp':
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specific information
try:
# Alert and log
if self.stats['nf_conntrack_enabled']:
self.views['nf_conntrack_percent']['decoration'] = self.get_alert(header='nf_conntrack_percent')
except KeyError:
# try/except mandatory for Windows compatibility (no conntrack stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Header
if self.stats['net_connections_enabled'] or self.stats['nf_conntrack_enabled']:
msg = '{}'.format('TCP CONNECTIONS')
ret.append(self.curse_add_line(msg, "TITLE"))
# Connections status
if self.stats['net_connections_enabled']:
for s in [psutil.CONN_LISTEN, 'initiated', psutil.CONN_ESTABLISHED, 'terminated']:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(self.stats[s], width=max_width - len(s) + 2)
ret.append(self.curse_add_line(msg))
# Connections track
if (
self.stats['nf_conntrack_enabled']
and 'nf_conntrack_count' in self.stats
and 'nf_conntrack_max' in self.stats
):
s = 'Tracked'
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(
'{:0.0f}/{:0.0f}'.format(self.stats['nf_conntrack_count'], self.stats['nf_conntrack_max']),
width=max_width - len(s) + 2,
)
ret.append(self.curse_add_line(msg, self.get_views(key='nf_conntrack_percent', option='decoration')))
return ret

View File

@ -1,176 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Connections plugin."""
from __future__ import unicode_literals
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
from glances.globals import nativestr
import psutil
# Define the history items list
# items_history_list = [{'name': 'rx',
# 'description': 'Download rate per second',
# 'y_unit': 'bit/s'},
# {'name': 'tx',
# 'description': 'Upload rate per second',
# 'y_unit': 'bit/s'}]
class PluginModel(GlancesPluginModel):
"""Glances connections plugin.
stats is a dict
"""
status_list = [psutil.CONN_LISTEN, psutil.CONN_ESTABLISHED]
initiated_states = [psutil.CONN_SYN_SENT, psutil.CONN_SYN_RECV]
terminated_states = [
psutil.CONN_FIN_WAIT1,
psutil.CONN_FIN_WAIT2,
psutil.CONN_TIME_WAIT,
psutil.CONN_CLOSE,
psutil.CONN_CLOSE_WAIT,
psutil.CONN_LAST_ACK,
]
conntrack = {
'nf_conntrack_count': '/proc/sys/net/netfilter/nf_conntrack_count',
'nf_conntrack_max': '/proc/sys/net/netfilter/nf_conntrack_max',
}
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
# items_history_list=items_history_list,
stats_init_value={'net_connections_enabled': True, 'nf_conntrack_enabled': True},
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update connections stats using the input method.
Stats is a dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the PSUtils lib
# Grab network interface stat using the psutil net_connections method
if stats['net_connections_enabled']:
try:
net_connections = psutil.net_connections(kind="tcp")
except Exception as e:
logger.warning('Can not get network connections stats ({})'.format(e))
logger.info('Disable connections stats')
stats['net_connections_enabled'] = False
self.stats = stats
return self.stats
for s in self.status_list:
stats[s] = len([c for c in net_connections if c.status == s])
initiated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
initiated += stats[s]
stats['initiated'] = initiated
terminated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
terminated += stats[s]
stats['terminated'] = terminated
if stats['nf_conntrack_enabled']:
# Grab connections track directly from the /proc file
for i in self.conntrack:
try:
with open(self.conntrack[i], 'r') as f:
stats[i] = float(f.readline().rstrip("\n"))
except (IOError, FileNotFoundError) as e:
logger.warning('Can not get network connections track ({})'.format(e))
logger.info('Disable connections track')
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
if 'nf_conntrack_max' in stats and 'nf_conntrack_count' in stats:
stats['nf_conntrack_percent'] = stats['nf_conntrack_count'] * 100 / stats['nf_conntrack_max']
else:
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
elif self.input_method == 'snmp':
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specific information
try:
# Alert and log
if self.stats['nf_conntrack_enabled']:
self.views['nf_conntrack_percent']['decoration'] = self.get_alert(header='nf_conntrack_percent')
except KeyError:
# try/except mandatory for Windows compatibility (no conntrack stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Header
if self.stats['net_connections_enabled'] or self.stats['nf_conntrack_enabled']:
msg = '{}'.format('TCP CONNECTIONS')
ret.append(self.curse_add_line(msg, "TITLE"))
# Connections status
if self.stats['net_connections_enabled']:
for s in [psutil.CONN_LISTEN, 'initiated', psutil.CONN_ESTABLISHED, 'terminated']:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(self.stats[s], width=max_width - len(s) + 2)
ret.append(self.curse_add_line(msg))
# Connections track
if (
self.stats['nf_conntrack_enabled']
and 'nf_conntrack_count' in self.stats
and 'nf_conntrack_max' in self.stats
):
s = 'Tracked'
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(
'{:0.0f}/{:0.0f}'.format(self.stats['nf_conntrack_count'], self.stats['nf_conntrack_max']),
width=max_width - len(s) + 2,
)
ret.append(self.curse_add_line(msg, self.get_views(key='nf_conntrack_percent', option='decoration')))
return ret

View File

@ -0,0 +1,430 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Containers plugin."""
import os
from copy import deepcopy
from glances.logger import logger
from glances.plugins.containers.engines.docker import DockerContainersExtension, import_docker_error_tag
from glances.plugins.containers.engines.podman import PodmanContainersExtension, import_podman_error_tag
from glances.plugins.plugin.model import GlancesPluginModel
from glances.processes import glances_processes
from glances.processes import sort_stats as sort_stats_processes
# Define the items history list (list of items to add to history)
# TODO: For the moment limited to the CPU. Had to change the graph exports
# method to display one graph per container.
# items_history_list = [{'name': 'cpu_percent',
# 'description': 'Container CPU consumption in %',
# 'y_unit': '%'},
# {'name': 'memory_usage',
# 'description': 'Container memory usage in bytes',
# 'y_unit': 'B'},
# {'name': 'network_rx',
# 'description': 'Container network RX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'network_tx',
# 'description': 'Container network TX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'io_r',
# 'description': 'Container IO bytes read per second',
# 'y_unit': 'Bps'},
# {'name': 'io_w',
# 'description': 'Container IO bytes write per second',
# 'y_unit': 'Bps'}]
items_history_list = [{'name': 'cpu_percent', 'description': 'Container CPU consumption in %', 'y_unit': '%'}]
# List of key to remove before export
export_exclude_list = ['cpu', 'io', 'memory', 'network']
# Sort dictionary for human
sort_for_human = {
'io_counters': 'disk IO',
'cpu_percent': 'CPU consumption',
'memory_usage': 'memory consumption',
'cpu_times': 'uptime',
'name': 'container name',
None: 'None',
}
class PluginModel(GlancesPluginModel):
"""Glances Docker plugin.
stats is a dict: {'version': {...}, 'containers': [{}, {}]}
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# The plugin can be disabled using: args.disable_docker
self.args = args
# Default config keys
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the Docker API
self.docker_extension = DockerContainersExtension() if not import_docker_error_tag else None
# Init the Podman API
if import_podman_error_tag:
self.podman_client = None
else:
self.podman_client = PodmanContainersExtension(podman_sock=self._podman_sock())
# Sort key
self.sort_key = None
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def _podman_sock(self):
"""Return the podman sock.
Could be desfined in the [docker] section thanks to the podman_sock option.
Default value: unix:///run/user/1000/podman/podman.sock
"""
conf_podman_sock = self.get_conf_value('podman_sock')
if len(conf_podman_sock) == 0:
return "unix:///run/user/1000/podman/podman.sock"
else:
return conf_podman_sock[0]
def exit(self):
"""Overwrite the exit method to close threads."""
if self.docker_extension:
self.docker_extension.stop()
if self.podman_client:
self.podman_client.stop()
# Call the father class
super(PluginModel, self).exit()
def get_key(self):
"""Return the key of the list."""
return 'name'
def get_export(self):
"""Overwrite the default export method.
- Only exports containers
- The key is the first container name
"""
try:
ret = deepcopy(self.stats['containers'])
except KeyError as e:
logger.debug("docker plugin - Docker export error {}".format(e))
ret = []
# Remove fields uses to compute rate
for container in ret:
for i in export_exclude_list:
container.pop(i)
return ret
def _all_tag(self):
"""Return the all tag of the Glances/Docker configuration file.
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=True
"""
all_tag = self.get_conf_value('all')
if len(all_tag) == 0:
return False
else:
return all_tag[0].lower() == 'true'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update Docker and podman stats using the input method."""
# Connection should be ok
if self.docker_extension is None and self.podman_client is None:
return self.get_init_value()
if self.input_method == 'local':
# Update stats
stats_docker = self.update_docker() if self.docker_extension else {}
stats_podman = self.update_podman() if self.podman_client else {}
stats = {
'version': stats_docker.get('version', {}),
'version_podman': stats_podman.get('version', {}),
'containers': stats_docker.get('containers', []) + stats_podman.get('containers', []),
}
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not available
pass
# Sort and update the stats
# @TODO: Have a look because sort did not work for the moment (need memory stats ?)
self.sort_key, self.stats = sort_docker_stats(stats)
return self.stats
def update_docker(self):
"""Update Docker stats using the input method."""
version, containers = self.docker_extension.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'docker'
return {"version": version, "containers": containers}
def update_podman(self):
"""Update Podman stats."""
version, containers = self.podman_client.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'podman'
return {"version": version, "containers": containers}
def get_user_ticks(self):
"""Return the user ticks by reading the environment variable."""
return os.sysconf(os.sysconf_names['SC_CLK_TCK'])
def get_stats_action(self):
"""Return stats for the action.
Docker will return self.stats['containers']
"""
return self.stats['containers']
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
if 'containers' not in self.stats:
return False
# Add specifics information
# Alert
for i in self.stats['containers']:
# Init the views for the current container (key = container name)
self.views[i[self.get_key()]] = {'cpu': {}, 'mem': {}}
# CPU alert
if 'cpu' in i and 'total' in i['cpu']:
# Looking for specific CPU container threshold in the conf file
alert = self.get_alert(i['cpu']['total'], header=i['name'] + '_cpu', action_key=i['name'])
if alert == 'DEFAULT':
# Not found ? Get back to default CPU threshold value
alert = self.get_alert(i['cpu']['total'], header='cpu')
self.views[i[self.get_key()]]['cpu']['decoration'] = alert
# MEM alert
if 'memory' in i and 'usage' in i['memory']:
# Looking for specific MEM container threshold in the conf file
alert = self.get_alert(
i['memory']['usage'], maximum=i['memory']['limit'], header=i['name'] + '_mem', action_key=i['name']
)
if alert == 'DEFAULT':
# Not found ? Get back to default MEM threshold value
alert = self.get_alert(i['memory']['usage'], maximum=i['memory']['limit'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist (and non null) and display plugin enable...
if not self.stats or 'containers' not in self.stats or len(self.stats['containers']) == 0 or self.is_disabled():
return ret
show_pod_name = False
if any(ct.get("pod_name") for ct in self.stats["containers"]):
show_pod_name = True
show_engine_name = False
if len(set(ct["engine"] for ct in self.stats["containers"])) > 1:
show_engine_name = True
# Build the string message
# Title
msg = '{}'.format('CONTAINERS')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {}'.format(len(self.stats['containers']))
ret.append(self.curse_add_line(msg))
msg = ' sorted by {}'.format(sort_for_human[self.sort_key])
ret.append(self.curse_add_line(msg))
# msg = ' (served by Docker {})'.format(self.stats['version']["Version"])
# ret.append(self.curse_add_line(msg))
ret.append(self.curse_new_line())
# Header
ret.append(self.curse_new_line())
# Get the maximum containers name
# Max size is configurable. See feature request #1723.
name_max_width = min(
self.config.get_int_value('containers', 'max_name_size', default=20) if self.config is not None else 20,
len(max(self.stats['containers'], key=lambda x: len(x['name']))['name']),
)
if show_engine_name:
msg = ' {:{width}}'.format('Engine', width=6)
ret.append(self.curse_add_line(msg))
if show_pod_name:
msg = ' {:{width}}'.format('Pod', width=12)
ret.append(self.curse_add_line(msg))
msg = ' {:{width}}'.format('Name', width=name_max_width)
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'name' else 'DEFAULT'))
msg = '{:>10}'.format('Status')
ret.append(self.curse_add_line(msg))
msg = '{:>10}'.format('Uptime')
ret.append(self.curse_add_line(msg))
msg = '{:>6}'.format('CPU%')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'cpu_percent' else 'DEFAULT'))
msg = '{:>7}'.format('MEM')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'memory_usage' else 'DEFAULT'))
msg = '/{:<7}'.format('MAX')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:8}'.format('Command')
ret.append(self.curse_add_line(msg))
# Data
for container in self.stats['containers']:
ret.append(self.curse_new_line())
if show_engine_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container["engine"], width=6)))
if show_pod_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container.get("pod_id", "-"), width=12)))
# Name
ret.append(self.curse_add_line(self._msg_name(container=container, max_width=name_max_width)))
# Status
status = self.container_alert(container['Status'])
msg = '{:>10}'.format(container['Status'][0:10])
ret.append(self.curse_add_line(msg, status))
# Uptime
if container['Uptime']:
msg = '{:>10}'.format(container['Uptime'])
else:
msg = '{:>10}'.format('_')
ret.append(self.curse_add_line(msg))
# CPU
try:
msg = '{:>6.1f}'.format(container['cpu']['total'])
except KeyError:
msg = '{:>6}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='cpu', option='decoration')))
# MEM
try:
msg = '{:>7}'.format(self.auto_unit(container['memory']['usage']))
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='mem', option='decoration')))
try:
msg = '/{:<7}'.format(self.auto_unit(container['memory']['limit']))
except KeyError:
msg = '/{:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# IO R/W
unit = 'B'
try:
value = self.auto_unit(int(container['io']['ior'] // container['io']['time_since_update'])) + unit
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = self.auto_unit(int(container['io']['iow'] // container['io']['time_since_update'])) + unit
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# NET RX/TX
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
try:
value = (
self.auto_unit(
int(container['network']['rx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = (
self.auto_unit(
int(container['network']['tx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# Command
if container['Command'] is not None:
msg = ' {}'.format(' '.join(container['Command']))
else:
msg = ' {}'.format('_')
ret.append(self.curse_add_line(msg, splittable=True))
return ret
def _msg_name(self, container, max_width):
"""Build the container name."""
name = container['name'][:max_width]
return ' {:{width}}'.format(name, width=max_width)
def container_alert(self, status):
"""Analyse the container status."""
if status == 'running':
return 'OK'
elif status == 'exited':
return 'WARNING'
elif status == 'dead':
return 'CRITICAL'
else:
return 'CAREFUL'
def sort_docker_stats(stats):
# Sort Docker stats using the same function than processes
sort_by = glances_processes.sort_key
sort_by_secondary = 'memory_usage'
if sort_by == 'memory_percent':
sort_by = 'memory_usage'
sort_by_secondary = 'cpu_percent'
elif sort_by in ['username', 'io_counters', 'cpu_times']:
sort_by = 'cpu_percent'
# Sort docker stats
sort_stats_processes(
stats['containers'],
sorted_by=sort_by,
sorted_by_secondary=sort_by_secondary,
# Reverse for all but name
reverse=glances_processes.sort_key != 'name',
)
# Return the main sort key and the sorted stats
return sort_by, stats

View File

@ -1,430 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Containers plugin."""
import os
from copy import deepcopy
from glances.logger import logger
from glances.plugins.containers.engines.docker import DockerContainersExtension, import_docker_error_tag
from glances.plugins.containers.engines.podman import PodmanContainersExtension, import_podman_error_tag
from glances.plugins.plugin.model import GlancesPluginModel
from glances.processes import glances_processes
from glances.processes import sort_stats as sort_stats_processes
# Define the items history list (list of items to add to history)
# TODO: For the moment limited to the CPU. Had to change the graph exports
# method to display one graph per container.
# items_history_list = [{'name': 'cpu_percent',
# 'description': 'Container CPU consumption in %',
# 'y_unit': '%'},
# {'name': 'memory_usage',
# 'description': 'Container memory usage in bytes',
# 'y_unit': 'B'},
# {'name': 'network_rx',
# 'description': 'Container network RX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'network_tx',
# 'description': 'Container network TX bitrate in bits per second',
# 'y_unit': 'bps'},
# {'name': 'io_r',
# 'description': 'Container IO bytes read per second',
# 'y_unit': 'Bps'},
# {'name': 'io_w',
# 'description': 'Container IO bytes write per second',
# 'y_unit': 'Bps'}]
items_history_list = [{'name': 'cpu_percent', 'description': 'Container CPU consumption in %', 'y_unit': '%'}]
# List of key to remove before export
export_exclude_list = ['cpu', 'io', 'memory', 'network']
# Sort dictionary for human
sort_for_human = {
'io_counters': 'disk IO',
'cpu_percent': 'CPU consumption',
'memory_usage': 'memory consumption',
'cpu_times': 'uptime',
'name': 'container name',
None: 'None',
}
class PluginModel(GlancesPluginModel):
"""Glances Docker plugin.
stats is a dict: {'version': {...}, 'containers': [{}, {}]}
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
# The plugin can be disabled using: args.disable_docker
self.args = args
# Default config keys
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the Docker API
self.docker_extension = DockerContainersExtension() if not import_docker_error_tag else None
# Init the Podman API
if import_podman_error_tag:
self.podman_client = None
else:
self.podman_client = PodmanContainersExtension(podman_sock=self._podman_sock())
# Sort key
self.sort_key = None
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def _podman_sock(self):
"""Return the podman sock.
Could be desfined in the [docker] section thanks to the podman_sock option.
Default value: unix:///run/user/1000/podman/podman.sock
"""
conf_podman_sock = self.get_conf_value('podman_sock')
if len(conf_podman_sock) == 0:
return "unix:///run/user/1000/podman/podman.sock"
else:
return conf_podman_sock[0]
def exit(self):
"""Overwrite the exit method to close threads."""
if self.docker_extension:
self.docker_extension.stop()
if self.podman_client:
self.podman_client.stop()
# Call the father class
super(PluginModel, self).exit()
def get_key(self):
"""Return the key of the list."""
return 'name'
def get_export(self):
"""Overwrite the default export method.
- Only exports containers
- The key is the first container name
"""
try:
ret = deepcopy(self.stats['containers'])
except KeyError as e:
logger.debug("docker plugin - Docker export error {}".format(e))
ret = []
# Remove fields uses to compute rate
for container in ret:
for i in export_exclude_list:
container.pop(i)
return ret
def _all_tag(self):
"""Return the all tag of the Glances/Docker configuration file.
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=True
"""
all_tag = self.get_conf_value('all')
if len(all_tag) == 0:
return False
else:
return all_tag[0].lower() == 'true'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update Docker and podman stats using the input method."""
# Connection should be ok
if self.docker_extension is None and self.podman_client is None:
return self.get_init_value()
if self.input_method == 'local':
# Update stats
stats_docker = self.update_docker() if self.docker_extension else {}
stats_podman = self.update_podman() if self.podman_client else {}
stats = {
'version': stats_docker.get('version', {}),
'version_podman': stats_podman.get('version', {}),
'containers': stats_docker.get('containers', []) + stats_podman.get('containers', []),
}
elif self.input_method == 'snmp':
# Update stats using SNMP
# Not available
pass
# Sort and update the stats
# @TODO: Have a look because sort did not work for the moment (need memory stats ?)
self.sort_key, self.stats = sort_docker_stats(stats)
return self.stats
def update_docker(self):
"""Update Docker stats using the input method."""
version, containers = self.docker_extension.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'docker'
return {"version": version, "containers": containers}
def update_podman(self):
"""Update Podman stats."""
version, containers = self.podman_client.update(all_tag=self._all_tag())
for container in containers:
container["engine"] = 'podman'
return {"version": version, "containers": containers}
def get_user_ticks(self):
"""Return the user ticks by reading the environment variable."""
return os.sysconf(os.sysconf_names['SC_CLK_TCK'])
def get_stats_action(self):
"""Return stats for the action.
Docker will return self.stats['containers']
"""
return self.stats['containers']
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
if 'containers' not in self.stats:
return False
# Add specifics information
# Alert
for i in self.stats['containers']:
# Init the views for the current container (key = container name)
self.views[i[self.get_key()]] = {'cpu': {}, 'mem': {}}
# CPU alert
if 'cpu' in i and 'total' in i['cpu']:
# Looking for specific CPU container threshold in the conf file
alert = self.get_alert(i['cpu']['total'], header=i['name'] + '_cpu', action_key=i['name'])
if alert == 'DEFAULT':
# Not found ? Get back to default CPU threshold value
alert = self.get_alert(i['cpu']['total'], header='cpu')
self.views[i[self.get_key()]]['cpu']['decoration'] = alert
# MEM alert
if 'memory' in i and 'usage' in i['memory']:
# Looking for specific MEM container threshold in the conf file
alert = self.get_alert(
i['memory']['usage'], maximum=i['memory']['limit'], header=i['name'] + '_mem', action_key=i['name']
)
if alert == 'DEFAULT':
# Not found ? Get back to default MEM threshold value
alert = self.get_alert(i['memory']['usage'], maximum=i['memory']['limit'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist (and non null) and display plugin enable...
if not self.stats or 'containers' not in self.stats or len(self.stats['containers']) == 0 or self.is_disabled():
return ret
show_pod_name = False
if any(ct.get("pod_name") for ct in self.stats["containers"]):
show_pod_name = True
show_engine_name = False
if len(set(ct["engine"] for ct in self.stats["containers"])) > 1:
show_engine_name = True
# Build the string message
# Title
msg = '{}'.format('CONTAINERS')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {}'.format(len(self.stats['containers']))
ret.append(self.curse_add_line(msg))
msg = ' sorted by {}'.format(sort_for_human[self.sort_key])
ret.append(self.curse_add_line(msg))
# msg = ' (served by Docker {})'.format(self.stats['version']["Version"])
# ret.append(self.curse_add_line(msg))
ret.append(self.curse_new_line())
# Header
ret.append(self.curse_new_line())
# Get the maximum containers name
# Max size is configurable. See feature request #1723.
name_max_width = min(
self.config.get_int_value('containers', 'max_name_size', default=20) if self.config is not None else 20,
len(max(self.stats['containers'], key=lambda x: len(x['name']))['name']),
)
if show_engine_name:
msg = ' {:{width}}'.format('Engine', width=6)
ret.append(self.curse_add_line(msg))
if show_pod_name:
msg = ' {:{width}}'.format('Pod', width=12)
ret.append(self.curse_add_line(msg))
msg = ' {:{width}}'.format('Name', width=name_max_width)
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'name' else 'DEFAULT'))
msg = '{:>10}'.format('Status')
ret.append(self.curse_add_line(msg))
msg = '{:>10}'.format('Uptime')
ret.append(self.curse_add_line(msg))
msg = '{:>6}'.format('CPU%')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'cpu_percent' else 'DEFAULT'))
msg = '{:>7}'.format('MEM')
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'memory_usage' else 'DEFAULT'))
msg = '/{:<7}'.format('MAX')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:<7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
msg = ' {:8}'.format('Command')
ret.append(self.curse_add_line(msg))
# Data
for container in self.stats['containers']:
ret.append(self.curse_new_line())
if show_engine_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container["engine"], width=6)))
if show_pod_name:
ret.append(self.curse_add_line(' {:{width}}'.format(container.get("pod_id", "-"), width=12)))
# Name
ret.append(self.curse_add_line(self._msg_name(container=container, max_width=name_max_width)))
# Status
status = self.container_alert(container['Status'])
msg = '{:>10}'.format(container['Status'][0:10])
ret.append(self.curse_add_line(msg, status))
# Uptime
if container['Uptime']:
msg = '{:>10}'.format(container['Uptime'])
else:
msg = '{:>10}'.format('_')
ret.append(self.curse_add_line(msg))
# CPU
try:
msg = '{:>6.1f}'.format(container['cpu']['total'])
except KeyError:
msg = '{:>6}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='cpu', option='decoration')))
# MEM
try:
msg = '{:>7}'.format(self.auto_unit(container['memory']['usage']))
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='mem', option='decoration')))
try:
msg = '/{:<7}'.format(self.auto_unit(container['memory']['limit']))
except KeyError:
msg = '/{:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# IO R/W
unit = 'B'
try:
value = self.auto_unit(int(container['io']['ior'] // container['io']['time_since_update'])) + unit
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = self.auto_unit(int(container['io']['iow'] // container['io']['time_since_update'])) + unit
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# NET RX/TX
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
try:
value = (
self.auto_unit(
int(container['network']['rx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = '{:>7}'.format(value)
except KeyError:
msg = '{:>7}'.format('_')
ret.append(self.curse_add_line(msg))
try:
value = (
self.auto_unit(
int(container['network']['tx'] // container['network']['time_since_update'] * to_bit)
)
+ unit
)
msg = ' {:<7}'.format(value)
except KeyError:
msg = ' {:<7}'.format('_')
ret.append(self.curse_add_line(msg))
# Command
if container['Command'] is not None:
msg = ' {}'.format(' '.join(container['Command']))
else:
msg = ' {}'.format('_')
ret.append(self.curse_add_line(msg, splittable=True))
return ret
def _msg_name(self, container, max_width):
"""Build the container name."""
name = container['name'][:max_width]
return ' {:{width}}'.format(name, width=max_width)
def container_alert(self, status):
"""Analyse the container status."""
if status == 'running':
return 'OK'
elif status == 'exited':
return 'WARNING'
elif status == 'dead':
return 'CRITICAL'
else:
return 'CAREFUL'
def sort_docker_stats(stats):
# Sort Docker stats using the same function than processes
sort_by = glances_processes.sort_key
sort_by_secondary = 'memory_usage'
if sort_by == 'memory_percent':
sort_by = 'memory_usage'
sort_by_secondary = 'cpu_percent'
elif sort_by in ['username', 'io_counters', 'cpu_times']:
sort_by = 'cpu_percent'
# Sort docker stats
sort_stats_processes(
stats['containers'],
sorted_by=sort_by,
sorted_by_secondary=sort_by_secondary,
# Reverse for all but name
reverse=glances_processes.sort_key != 'name',
)
# Return the main sort key and the sorted stats
return sort_by, stats

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU core plugin."""
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'phys': {'description': 'Number of physical cores (hyper thread CPUs are excluded).', 'unit': 'number'},
'log': {
'description': 'Number of logical CPUs. A logical CPU is the number of \
physical cores multiplied by the number of threads that can run on each core.',
'unit': 'number',
},
}
class PluginModel(GlancesPluginModel):
"""Glances CPU core plugin.
Get stats about CPU core number.
stats is integer (number of core)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, fields_description=fields_description)
# We dot not want to display the stat in the curse interface
# The core number is displayed by the load plugin
self.display_curse = False
# Do *NOT* uncomment the following line
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update core stats.
Stats is a dict (with both physical and log cpu number) instead of a integer.
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# The psutil 2.0 include psutil.cpu_count() and psutil.cpu_count(logical=False)
# Return a dict with:
# - phys: physical cores only (hyper thread CPUs are excluded)
# - log: logical CPUs in the system
# Return None if undefined
try:
stats["phys"] = psutil.cpu_count(logical=False)
stats["log"] = psutil.cpu_count()
except NameError:
self.reset()
elif self.input_method == 'snmp':
# Update stats using SNMP
# http://stackoverflow.com/questions/5662467/how-to-find-out-the-number-of-cpus-using-snmp
pass
# Update the stats
self.stats = stats
return self.stats

View File

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU core plugin."""
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'phys': {'description': 'Number of physical cores (hyper thread CPUs are excluded).', 'unit': 'number'},
'log': {
'description': 'Number of logical CPUs. A logical CPU is the number of \
physical cores multiplied by the number of threads that can run on each core.',
'unit': 'number',
},
}
class PluginModel(GlancesPluginModel):
"""Glances CPU core plugin.
Get stats about CPU core number.
stats is integer (number of core)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, fields_description=fields_description)
# We dot not want to display the stat in the curse interface
# The core number is displayed by the load plugin
self.display_curse = False
# Do *NOT* uncomment the following line
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update core stats.
Stats is a dict (with both physical and log cpu number) instead of a integer.
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# The psutil 2.0 include psutil.cpu_count() and psutil.cpu_count(logical=False)
# Return a dict with:
# - phys: physical cores only (hyper thread CPUs are excluded)
# - log: logical CPUs in the system
# Return None if undefined
try:
stats["phys"] = psutil.cpu_count(logical=False)
stats["log"] = psutil.cpu_count()
except NameError:
self.reset()
elif self.input_method == 'snmp':
# Update stats using SNMP
# http://stackoverflow.com/questions/5662467/how-to-find-out-the-number-of-cpus-using-snmp
pass
# Update the stats
self.stats = stats
return self.stats

View File

@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU plugin."""
from glances.timer import getTimeSinceLastUpdate
from glances.globals import LINUX, WINDOWS, SUNOS, iterkeys
from glances.cpu_percent import cpu_percent
from glances.plugins.core import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
# description: human readable description
# short_name: shortname to use un UI
# unit: unit type
# rate: is it a rate ? If yes, // by time_since_update when displayed,
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
fields_description = {
'total': {'description': 'Sum of all CPU percentages (except idle).', 'unit': 'percent'},
'system': {
'description': 'percent time spent in kernel space. System CPU time is the \
time spent running code in the Operating System kernel.',
'unit': 'percent',
},
'user': {
'description': 'CPU percent time spent in user space. \
User CPU time is the time spent on the processor running your program\'s code (or code in libraries).',
'unit': 'percent',
},
'iowait': {
'description': '*(Linux)*: percent time spent by the CPU waiting for I/O \
operations to complete.',
'unit': 'percent',
},
'dpc': {
'description': '*(Windows)*: time spent servicing deferred procedure calls (DPCs)',
'unit': 'percent',
},
'idle': {
'description': 'percent of CPU used by any program. Every program or task \
that runs on a computer system occupies a certain amount of processing \
time on the CPU. If the CPU has completed all tasks it is idle.',
'unit': 'percent',
},
'irq': {
'description': '*(Linux and BSD)*: percent time spent servicing/handling \
hardware/software interrupts. Time servicing interrupts (hardware + \
software).',
'unit': 'percent',
},
'nice': {
'description': '*(Unix)*: percent time occupied by user level processes with \
a positive nice value. The time the CPU has spent running users\' \
processes that have been *niced*.',
'unit': 'percent',
},
'steal': {
'description': '*(Linux)*: percentage of time a virtual CPU waits for a real \
CPU while the hypervisor is servicing another virtual processor.',
'unit': 'percent',
},
'ctx_switches': {
'description': 'number of context switches (voluntary + involuntary) per \
second. A context switch is a procedure that a computer\'s CPU (central \
processing unit) follows to change from one task (or process) to \
another while ensuring that the tasks do not conflict.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'ctx_sw',
},
'interrupts': {
'description': 'number of interrupts per second.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'inter',
},
'soft_interrupts': {
'description': 'number of software interrupts per second. Always set to \
0 on Windows and SunOS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sw_int',
},
'syscalls': {
'description': 'number of system calls per second. Always 0 on Linux OS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sys_call',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# 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 = {
'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'},
'netapp': {
'system': '1.3.6.1.4.1.789.1.2.1.3.0',
'idle': '1.3.6.1.4.1.789.1.2.1.5.0',
'cpucore': '1.3.6.1.4.1.789.1.2.1.6.0',
},
}
# Define the history items list
# - 'name' define the stat identifier
# - 'y_unit' define the Y label
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances CPU plugin.
'stats' is a dictionary that contains the system-wide CPU utilization as a
percentage.
"""
def __init__(self, args=None, config=None):
"""Init the CPU plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception:
self.nb_log_core = 1
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update CPU stats using the input method."""
# Grab stats into self.stats
if self.input_method == 'local':
stats = self.update_local()
elif self.input_method == 'snmp':
stats = self.update_snmp()
else:
stats = self.get_init_value()
# Update the stats
self.stats = stats
return self.stats
def update_local(self):
"""Update CPU stats using psutil."""
# Grab CPU stats using psutil's cpu_percent and cpu_times_percent
# Get all possible values for CPU stats: user, system, idle,
# nice (UNIX), iowait (Linux), irq (Linux, FreeBSD), steal (Linux 2.6.11+)
# The following stats are returned by the API but not displayed in the UI:
# softirq (Linux), guest (Linux 2.6.24+), guest_nice (Linux 3.2.0+)
# Init new stats
stats = self.get_init_value()
stats['total'] = cpu_percent.get()
# Standards stats
# - user: time spent by normal processes executing in user mode; on Linux this also includes guest time
# - system: time spent by processes executing in kernel mode
# - idle: time spent doing nothing
# - nice (UNIX): time spent by niced (prioritized) processes executing in user mode
# on Linux this also includes guest_nice time
# - iowait (Linux): time spent waiting for I/O to complete.
# This is not accounted in idle time counter.
# - irq (Linux, BSD): time spent for servicing hardware interrupts
# - softirq (Linux): time spent for servicing software interrupts
# - steal (Linux 2.6.11+): time spent by other operating systems running in a virtualized environment
# - guest (Linux 2.6.24+): time spent running a virtual CPU for guest operating systems under
# the control of the Linux kernel
# - guest_nice (Linux 3.2.0+): time spent running a niced guest (virtual CPU for guest operating systems
# under the control of the Linux kernel)
# - interrupt (Windows): time spent for servicing hardware interrupts ( similar to “irq” on UNIX)
# - dpc (Windows): time spent servicing deferred procedure calls (DPCs)
cpu_times_percent = psutil.cpu_times_percent(interval=0.0)
for stat in cpu_times_percent._fields:
stats[stat] = getattr(cpu_times_percent, stat)
# Additional CPU stats (number of events not as a %; psutil>=4.1.0)
# - ctx_switches: number of context switches (voluntary + involuntary) since boot.
# - interrupts: number of interrupts since boot.
# - soft_interrupts: number of software interrupts since boot. Always set to 0 on Windows and SunOS.
# - syscalls: number of system calls since boot. Always set to 0 on Linux.
cpu_stats = psutil.cpu_stats()
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('cpu')
# Core number is needed to compute the CTX switch limit
stats['cpucore'] = self.nb_log_core
# Previous CPU stats are stored in the cpu_stats_old variable
if not hasattr(self, 'cpu_stats_old'):
# Init the stats (needed to have the key name for export)
for stat in cpu_stats._fields:
# @TODO: better to set it to None but should refactor views and UI...
stats[stat] = 0
else:
# Others calls...
for stat in cpu_stats._fields:
if getattr(cpu_stats, stat) is not None:
stats[stat] = getattr(cpu_stats, stat) - getattr(self.cpu_stats_old, stat)
# Save stats to compute next step
self.cpu_stats_old = cpu_stats
return stats
def update_snmp(self):
"""Update CPU stats using SNMP."""
# Init new stats
stats = self.get_init_value()
# Update stats using SNMP
if self.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.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
# Iter through CPU and compute the idle CPU stats
stats['nb_log_core'] = 0
stats['idle'] = 0
for c in cpu_stats:
if c.startswith('percent'):
stats['idle'] += float(cpu_stats['percent.3'])
stats['nb_log_core'] += 1
if stats['nb_log_core'] > 0:
stats['idle'] = stats['idle'] / stats['nb_log_core']
stats['idle'] = 100 - stats['idle']
stats['total'] = 100 - stats['idle']
else:
# Default behavior
try:
stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name])
except KeyError:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['idle'] == '':
self.reset()
return self.stats
# Convert SNMP stats to float
for key in iterkeys(stats):
stats[key] = float(stats[key])
stats['total'] = 100 - stats['idle']
return stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
for key in ['user', 'system', 'iowait', 'dpc', 'total']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert_log(self.stats[key], header=key)
# Alert only
for key in ['steal']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
# Alert only but depend on Core number
for key in ['ctx_switches']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(
self.stats[key], maximum=100 * self.stats['cpucore'], header=key
)
# Optional
for key in ['nice', 'irq', 'idle', 'steal', 'ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the UI."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disable
if not self.stats or self.args.percpu or self.is_disabled():
return ret
# Some tag to enable/disable stats (example: idle_tag triggered on Windows OS)
idle_tag = 'user' not in self.stats
# First line
# Total + (idle) + ctx_sw
msg = '{}'.format('CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
trend_user = self.get_trend('user')
trend_system = self.get_trend('system')
if trend_user is None or trend_user is None:
trend_cpu = None
else:
trend_cpu = trend_user + trend_system
msg = ' {:4}'.format(self.trend_msg(trend_cpu))
ret.append(self.curse_add_line(msg))
# Total CPU usage
msg = '{:5.1f}%'.format(self.stats['total'])
ret.append(self.curse_add_line(msg, self.get_views(key='total', option='decoration')))
# Idle CPU
if 'idle' in self.stats and not idle_tag:
msg = ' {:8}'.format('idle')
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
msg = '{:4.1f}%'.format(self.stats['idle'])
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
# ctx_switches
# On WINDOWS/SUNOS the ctx_switches is displayed in the third line
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Second line
# user|idle + irq + interrupts
ret.append(self.curse_new_line())
# User CPU
if not idle_tag:
ret.extend(self.curse_add_stat('user', width=15))
elif 'idle' in self.stats:
ret.extend(self.curse_add_stat('idle', width=15))
# IRQ CPU
ret.extend(self.curse_add_stat('irq', width=14, header=' '))
# interrupts
ret.extend(self.curse_add_stat('interrupts', width=15, header=' '))
# Third line
# system|core + nice + sw_int
ret.append(self.curse_new_line())
# System CPU
if not idle_tag:
ret.extend(self.curse_add_stat('system', width=15))
else:
ret.extend(self.curse_add_stat('core', width=15))
# Nice CPU
ret.extend(self.curse_add_stat('nice', width=14, header=' '))
# soft_interrupts
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('soft_interrupts', width=15, header=' '))
else:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Fourth line
# iowait + steal + syscalls
ret.append(self.curse_new_line())
if 'iowait' in self.stats:
# IOWait CPU
ret.extend(self.curse_add_stat('iowait', width=15))
elif 'dpc' in self.stats:
# DPC CPU
ret.extend(self.curse_add_stat('dpc', width=15))
# Steal CPU usage
ret.extend(self.curse_add_stat('steal', width=14, header=' '))
# syscalls: number of system calls since boot. Always set to 0 on Linux. (do not display)
if not LINUX:
ret.extend(self.curse_add_stat('syscalls', width=15, header=' '))
# Return the message with decoration
return ret

View File

@ -1,392 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU plugin."""
from glances.timer import getTimeSinceLastUpdate
from glances.globals import LINUX, WINDOWS, SUNOS, iterkeys
from glances.cpu_percent import cpu_percent
from glances.plugins.core.model import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
# description: human readable description
# short_name: shortname to use un UI
# unit: unit type
# rate: is it a rate ? If yes, // by time_since_update when displayed,
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
fields_description = {
'total': {'description': 'Sum of all CPU percentages (except idle).', 'unit': 'percent'},
'system': {
'description': 'percent time spent in kernel space. System CPU time is the \
time spent running code in the Operating System kernel.',
'unit': 'percent',
},
'user': {
'description': 'CPU percent time spent in user space. \
User CPU time is the time spent on the processor running your program\'s code (or code in libraries).',
'unit': 'percent',
},
'iowait': {
'description': '*(Linux)*: percent time spent by the CPU waiting for I/O \
operations to complete.',
'unit': 'percent',
},
'dpc': {
'description': '*(Windows)*: time spent servicing deferred procedure calls (DPCs)',
'unit': 'percent',
},
'idle': {
'description': 'percent of CPU used by any program. Every program or task \
that runs on a computer system occupies a certain amount of processing \
time on the CPU. If the CPU has completed all tasks it is idle.',
'unit': 'percent',
},
'irq': {
'description': '*(Linux and BSD)*: percent time spent servicing/handling \
hardware/software interrupts. Time servicing interrupts (hardware + \
software).',
'unit': 'percent',
},
'nice': {
'description': '*(Unix)*: percent time occupied by user level processes with \
a positive nice value. The time the CPU has spent running users\' \
processes that have been *niced*.',
'unit': 'percent',
},
'steal': {
'description': '*(Linux)*: percentage of time a virtual CPU waits for a real \
CPU while the hypervisor is servicing another virtual processor.',
'unit': 'percent',
},
'ctx_switches': {
'description': 'number of context switches (voluntary + involuntary) per \
second. A context switch is a procedure that a computer\'s CPU (central \
processing unit) follows to change from one task (or process) to \
another while ensuring that the tasks do not conflict.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'ctx_sw',
},
'interrupts': {
'description': 'number of interrupts per second.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'inter',
},
'soft_interrupts': {
'description': 'number of software interrupts per second. Always set to \
0 on Windows and SunOS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sw_int',
},
'syscalls': {
'description': 'number of system calls per second. Always 0 on Linux OS.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
'short_name': 'sys_call',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# 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 = {
'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'},
'netapp': {
'system': '1.3.6.1.4.1.789.1.2.1.3.0',
'idle': '1.3.6.1.4.1.789.1.2.1.5.0',
'cpucore': '1.3.6.1.4.1.789.1.2.1.6.0',
},
}
# Define the history items list
# - 'name' define the stat identifier
# - 'y_unit' define the Y label
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances CPU plugin.
'stats' is a dictionary that contains the system-wide CPU utilization as a
percentage.
"""
def __init__(self, args=None, config=None):
"""Init the CPU plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception:
self.nb_log_core = 1
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update CPU stats using the input method."""
# Grab stats into self.stats
if self.input_method == 'local':
stats = self.update_local()
elif self.input_method == 'snmp':
stats = self.update_snmp()
else:
stats = self.get_init_value()
# Update the stats
self.stats = stats
return self.stats
def update_local(self):
"""Update CPU stats using psutil."""
# Grab CPU stats using psutil's cpu_percent and cpu_times_percent
# Get all possible values for CPU stats: user, system, idle,
# nice (UNIX), iowait (Linux), irq (Linux, FreeBSD), steal (Linux 2.6.11+)
# The following stats are returned by the API but not displayed in the UI:
# softirq (Linux), guest (Linux 2.6.24+), guest_nice (Linux 3.2.0+)
# Init new stats
stats = self.get_init_value()
stats['total'] = cpu_percent.get()
# Standards stats
# - user: time spent by normal processes executing in user mode; on Linux this also includes guest time
# - system: time spent by processes executing in kernel mode
# - idle: time spent doing nothing
# - nice (UNIX): time spent by niced (prioritized) processes executing in user mode
# on Linux this also includes guest_nice time
# - iowait (Linux): time spent waiting for I/O to complete.
# This is not accounted in idle time counter.
# - irq (Linux, BSD): time spent for servicing hardware interrupts
# - softirq (Linux): time spent for servicing software interrupts
# - steal (Linux 2.6.11+): time spent by other operating systems running in a virtualized environment
# - guest (Linux 2.6.24+): time spent running a virtual CPU for guest operating systems under
# the control of the Linux kernel
# - guest_nice (Linux 3.2.0+): time spent running a niced guest (virtual CPU for guest operating systems
# under the control of the Linux kernel)
# - interrupt (Windows): time spent for servicing hardware interrupts ( similar to “irq” on UNIX)
# - dpc (Windows): time spent servicing deferred procedure calls (DPCs)
cpu_times_percent = psutil.cpu_times_percent(interval=0.0)
for stat in cpu_times_percent._fields:
stats[stat] = getattr(cpu_times_percent, stat)
# Additional CPU stats (number of events not as a %; psutil>=4.1.0)
# - ctx_switches: number of context switches (voluntary + involuntary) since boot.
# - interrupts: number of interrupts since boot.
# - soft_interrupts: number of software interrupts since boot. Always set to 0 on Windows and SunOS.
# - syscalls: number of system calls since boot. Always set to 0 on Linux.
cpu_stats = psutil.cpu_stats()
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('cpu')
# Core number is needed to compute the CTX switch limit
stats['cpucore'] = self.nb_log_core
# Previous CPU stats are stored in the cpu_stats_old variable
if not hasattr(self, 'cpu_stats_old'):
# Init the stats (needed to have the key name for export)
for stat in cpu_stats._fields:
# @TODO: better to set it to None but should refactor views and UI...
stats[stat] = 0
else:
# Others calls...
for stat in cpu_stats._fields:
if getattr(cpu_stats, stat) is not None:
stats[stat] = getattr(cpu_stats, stat) - getattr(self.cpu_stats_old, stat)
# Save stats to compute next step
self.cpu_stats_old = cpu_stats
return stats
def update_snmp(self):
"""Update CPU stats using SNMP."""
# Init new stats
stats = self.get_init_value()
# Update stats using SNMP
if self.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.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
# Iter through CPU and compute the idle CPU stats
stats['nb_log_core'] = 0
stats['idle'] = 0
for c in cpu_stats:
if c.startswith('percent'):
stats['idle'] += float(cpu_stats['percent.3'])
stats['nb_log_core'] += 1
if stats['nb_log_core'] > 0:
stats['idle'] = stats['idle'] / stats['nb_log_core']
stats['idle'] = 100 - stats['idle']
stats['total'] = 100 - stats['idle']
else:
# Default behavior
try:
stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name])
except KeyError:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['idle'] == '':
self.reset()
return self.stats
# Convert SNMP stats to float
for key in iterkeys(stats):
stats[key] = float(stats[key])
stats['total'] = 100 - stats['idle']
return stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
for key in ['user', 'system', 'iowait', 'dpc', 'total']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert_log(self.stats[key], header=key)
# Alert only
for key in ['steal']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
# Alert only but depend on Core number
for key in ['ctx_switches']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(
self.stats[key], maximum=100 * self.stats['cpucore'], header=key
)
# Optional
for key in ['nice', 'irq', 'idle', 'steal', 'ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the UI."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disable
if not self.stats or self.args.percpu or self.is_disabled():
return ret
# Some tag to enable/disable stats (example: idle_tag triggered on Windows OS)
idle_tag = 'user' not in self.stats
# First line
# Total + (idle) + ctx_sw
msg = '{}'.format('CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
trend_user = self.get_trend('user')
trend_system = self.get_trend('system')
if trend_user is None or trend_user is None:
trend_cpu = None
else:
trend_cpu = trend_user + trend_system
msg = ' {:4}'.format(self.trend_msg(trend_cpu))
ret.append(self.curse_add_line(msg))
# Total CPU usage
msg = '{:5.1f}%'.format(self.stats['total'])
ret.append(self.curse_add_line(msg, self.get_views(key='total', option='decoration')))
# Idle CPU
if 'idle' in self.stats and not idle_tag:
msg = ' {:8}'.format('idle')
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
msg = '{:4.1f}%'.format(self.stats['idle'])
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
# ctx_switches
# On WINDOWS/SUNOS the ctx_switches is displayed in the third line
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Second line
# user|idle + irq + interrupts
ret.append(self.curse_new_line())
# User CPU
if not idle_tag:
ret.extend(self.curse_add_stat('user', width=15))
elif 'idle' in self.stats:
ret.extend(self.curse_add_stat('idle', width=15))
# IRQ CPU
ret.extend(self.curse_add_stat('irq', width=14, header=' '))
# interrupts
ret.extend(self.curse_add_stat('interrupts', width=15, header=' '))
# Third line
# system|core + nice + sw_int
ret.append(self.curse_new_line())
# System CPU
if not idle_tag:
ret.extend(self.curse_add_stat('system', width=15))
else:
ret.extend(self.curse_add_stat('core', width=15))
# Nice CPU
ret.extend(self.curse_add_stat('nice', width=14, header=' '))
# soft_interrupts
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('soft_interrupts', width=15, header=' '))
else:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Fourth line
# iowait + steal + syscalls
ret.append(self.curse_new_line())
if 'iowait' in self.stats:
# IOWait CPU
ret.extend(self.curse_add_stat('iowait', width=15))
elif 'dpc' in self.stats:
# DPC CPU
ret.extend(self.curse_add_stat('dpc', width=15))
# Steal CPU usage
ret.extend(self.curse_add_stat('steal', width=14, header=' '))
# syscalls: number of system calls since boot. Always set to 0 on Linux. (do not display)
if not LINUX:
ret.extend(self.curse_add_stat('syscalls', width=15, header=' '))
# Return the message with decoration
return ret

View File

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Disk I/O plugin."""
from __future__ import unicode_literals
from glances.logger import logger
from glances.globals import nativestr
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Define the history items list
items_history_list = [
{'name': 'read_bytes', 'description': 'Bytes read per second', 'y_unit': 'B/s'},
{'name': 'write_bytes', 'description': 'Bytes write per second', 'y_unit': 'B/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances disks I/O plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['read_bytes', 'write_bytes']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'disk_name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update disk I/O stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stat using the psutil disk_io_counters method
# read_count: number of reads
# write_count: number of writes
# read_bytes: number of bytes read
# write_bytes: number of bytes written
# read_time: time spent reading from disk (in milliseconds)
# write_time: time spent writing to disk (in milliseconds)
try:
diskio = psutil.disk_io_counters(perdisk=True)
except Exception:
return stats
# Previous disk IO stats are stored in the diskio_old variable
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('disk')
diskio = diskio
for disk in diskio:
# By default, RamFS is not displayed (issue #714)
if self.args is not None and not self.args.diskio_show_ramfs and disk.startswith('ram'):
continue
# Shall we display the stats ?
if not self.is_display(disk):
continue
# Compute count and bit rate
try:
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': diskio[disk].read_count - self.diskio_old[disk].read_count,
'write_count': diskio[disk].write_count - self.diskio_old[disk].write_count,
'read_bytes': diskio[disk].read_bytes - self.diskio_old[disk].read_bytes,
'write_bytes': diskio[disk].write_bytes - self.diskio_old[disk].write_bytes,
}
except (KeyError, AttributeError):
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': 0,
'write_count': 0,
'read_bytes': 0,
'write_bytes': 0,
}
# Add alias if exist (define in the configuration file)
if self.has_alias(disk) is not None:
diskstat['alias'] = self.has_alias(disk)
# Add the dict key
diskstat['key'] = self.get_key()
# Add the current disk stat to the list
stats.append(diskstat)
# Save stats to compute next bitrate
try:
self.diskio_old = diskio
except (IOError, UnboundLocalError):
pass
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
disk_real_name = i['disk_name']
self.views[i[self.get_key()]]['read_bytes']['decoration'] = self.get_alert(
int(i['read_bytes'] // i['time_since_update']), header=disk_real_name + '_rx'
)
self.views[i[self.get_key()]]['write_bytes']['decoration'] = self.get_alert(
int(i['write_bytes'] // i['time_since_update']), header=disk_real_name + '_tx'
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 13
# Header
msg = '{:{width}}'.format('DISK I/O', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.diskio_iops:
msg = '{:>8}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>8}'.format('R/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('W/s')
ret.append(self.curse_add_line(msg))
# Disk list (sorted by name)
for i in self.sorted_stats():
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Is there an alias for the disk name ?
disk_name = i['alias'] if 'alias' in i else i['disk_name']
# New line
ret.append(self.curse_new_line())
if len(disk_name) > name_max_width:
# Cut disk name if it is too long
disk_name = disk_name[:name_max_width] + '_'
msg = '{:{width}}'.format(nativestr(disk_name), width=name_max_width + 1)
ret.append(self.curse_add_line(msg))
if args.diskio_iops:
# count
txps = self.auto_unit(int(i['read_count'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_count'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_count', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_count', option='decoration')
)
)
else:
# Bitrate
txps = self.auto_unit(int(i['read_bytes'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_bytes'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_bytes', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_bytes', option='decoration')
)
)
return ret

View File

@ -1,230 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Disk I/O plugin."""
from __future__ import unicode_literals
from glances.globals import nativestr
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Define the history items list
items_history_list = [
{'name': 'read_bytes', 'description': 'Bytes read per second', 'y_unit': 'B/s'},
{'name': 'write_bytes', 'description': 'Bytes write per second', 'y_unit': 'B/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances disks I/O plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['read_bytes', 'write_bytes']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'disk_name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update disk I/O stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stat using the psutil disk_io_counters method
# read_count: number of reads
# write_count: number of writes
# read_bytes: number of bytes read
# write_bytes: number of bytes written
# read_time: time spent reading from disk (in milliseconds)
# write_time: time spent writing to disk (in milliseconds)
try:
diskio = psutil.disk_io_counters(perdisk=True)
except Exception:
return stats
# Previous disk IO stats are stored in the diskio_old variable
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('disk')
diskio = diskio
for disk in diskio:
# By default, RamFS is not displayed (issue #714)
if self.args is not None and not self.args.diskio_show_ramfs and disk.startswith('ram'):
continue
# Shall we display the stats ?
if not self.is_display(disk):
continue
# Compute count and bit rate
try:
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': diskio[disk].read_count - self.diskio_old[disk].read_count,
'write_count': diskio[disk].write_count - self.diskio_old[disk].write_count,
'read_bytes': diskio[disk].read_bytes - self.diskio_old[disk].read_bytes,
'write_bytes': diskio[disk].write_bytes - self.diskio_old[disk].write_bytes,
}
except (KeyError, AttributeError):
diskstat = {
'time_since_update': time_since_update,
'disk_name': disk,
'read_count': 0,
'write_count': 0,
'read_bytes': 0,
'write_bytes': 0,
}
# Add alias if exist (define in the configuration file)
if self.has_alias(disk) is not None:
diskstat['alias'] = self.has_alias(disk)
# Add the dict key
diskstat['key'] = self.get_key()
# Add the current disk stat to the list
stats.append(diskstat)
# Save stats to compute next bitrate
try:
self.diskio_old = diskio
except (IOError, UnboundLocalError):
pass
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
disk_real_name = i['disk_name']
self.views[i[self.get_key()]]['read_bytes']['decoration'] = self.get_alert(
int(i['read_bytes'] // i['time_since_update']), header=disk_real_name + '_rx'
)
self.views[i[self.get_key()]]['write_bytes']['decoration'] = self.get_alert(
int(i['write_bytes'] // i['time_since_update']), header=disk_real_name + '_tx'
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 13
# Header
msg = '{:{width}}'.format('DISK I/O', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.diskio_iops:
msg = '{:>8}'.format('IOR/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('IOW/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>8}'.format('R/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('W/s')
ret.append(self.curse_add_line(msg))
# Disk list (sorted by name)
for i in self.sorted_stats():
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Is there an alias for the disk name ?
disk_name = self.has_alias(i['disk_name']) if self.has_alias(i['disk_name']) else i['disk_name']
# New line
ret.append(self.curse_new_line())
if len(disk_name) > name_max_width:
# Cut disk name if it is too long
disk_name = '_' + disk_name[-name_max_width + 1 :]
msg = '{:{width}}'.format(nativestr(disk_name), width=name_max_width + 1)
ret.append(self.curse_add_line(msg))
if args.diskio_iops:
# count
txps = self.auto_unit(int(i['read_count'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_count'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_count', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_count', option='decoration')
)
)
else:
# Bitrate
txps = self.auto_unit(int(i['read_bytes'] // i['time_since_update']))
rxps = self.auto_unit(int(i['write_bytes'] // i['time_since_update']))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='read_bytes', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()], key='write_bytes', option='decoration')
)
)
return ret

View File

@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Folder plugin."""
from __future__ import unicode_literals
from glances.globals import nativestr
from glances.folder_list import FolderList as glancesFolderList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances folder plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init stats
self.glances_folders = glancesFolderList(config)
def get_key(self):
"""Return the key of the list."""
return 'path'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the folders list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Folder list only available in a full Glances environment
# Check if the glances_folder instance is init
if self.glances_folders is None:
return self.stats
# Update the folders list (result of command)
self.glances_folders.update(key=self.get_key())
# Put it on the stats var
stats = self.glances_folders.get()
else:
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, stat, header=""):
"""Manage limits of the folder list."""
if stat['errno'] != 0:
ret = 'ERROR'
else:
ret = 'OK'
if stat['critical'] is not None and stat['size'] > int(stat['critical']) * 1000000:
ret = 'CRITICAL'
elif stat['warning'] is not None and stat['size'] > int(stat['warning']) * 1000000:
ret = 'WARNING'
elif stat['careful'] is not None and stat['size'] > int(stat['careful']) * 1000000:
ret = 'CAREFUL'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, stat[self.get_key()])
return ret
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Header
msg = '{:{width}}'.format('FOLDERS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Data
for i in self.stats:
ret.append(self.curse_new_line())
if len(i['path']) > name_max_width:
# Cut path if it is too long
path = '_' + i['path'][-name_max_width + 1 :]
else:
path = i['path']
msg = '{:{width}}'.format(nativestr(path), width=name_max_width)
ret.append(self.curse_add_line(msg))
if i['errno'] != 0:
msg = '?{:>8}'.format(self.auto_unit(i['size']))
else:
msg = '{:>9}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg, self.get_alert(i, header='folder_' + i['indice'])))
return ret

View File

@ -1,122 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Folder plugin."""
from __future__ import unicode_literals
import numbers
from glances.globals import nativestr
from glances.folder_list import FolderList as glancesFolderList
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances folder plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init stats
self.glances_folders = glancesFolderList(config)
def get_key(self):
"""Return the key of the list."""
return 'path'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the folders list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Folder list only available in a full Glances environment
# Check if the glances_folder instance is init
if self.glances_folders is None:
return self.stats
# Update the folders list (result of command)
self.glances_folders.update(key=self.get_key())
# Put it on the stats var
stats = self.glances_folders.get()
else:
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, stat, header=""):
"""Manage limits of the folder list."""
if stat['errno'] != 0:
ret = 'ERROR'
else:
ret = 'OK'
if stat['critical'] is not None and stat['size'] > int(stat['critical']) * 1000000:
ret = 'CRITICAL'
elif stat['warning'] is not None and stat['size'] > int(stat['warning']) * 1000000:
ret = 'WARNING'
elif stat['careful'] is not None and stat['size'] > int(stat['careful']) * 1000000:
ret = 'CAREFUL'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, stat[self.get_key()])
return ret
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Header
msg = '{:{width}}'.format('FOLDERS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
# Data
for i in self.stats:
ret.append(self.curse_new_line())
if len(i['path']) > name_max_width:
# Cut path if it is too long
path = '_' + i['path'][-name_max_width + 1:]
else:
path = i['path']
msg = '{:{width}}'.format(nativestr(path), width=name_max_width)
ret.append(self.curse_add_line(msg))
if i['errno'] != 0:
msg = '?{:>8}'.format(self.auto_unit(i['size']))
else:
msg = '{:>9}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg, self.get_alert(i, header='folder_' + i['indice'])))
return ret

View File

@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""File system plugin."""
from __future__ import unicode_literals
import operator
from glances.globals import u, nativestr, PermissionError
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# SNMP OID
# The snmpd.conf needs to be edited.
# Add the following to enable it on all disk
# ...
# includeAllDisks 10%
# ...
# The OIDs are as follows (for the first disk)
# Path where the disk is mounted: .1.3.6.1.4.1.2021.9.1.2.1
# Path of the device for the partition: .1.3.6.1.4.1.2021.9.1.3.1
# Total size of the disk/partition (kBytes): .1.3.6.1.4.1.2021.9.1.6.1
# Available space on the disk: .1.3.6.1.4.1.2021.9.1.7.1
# 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 = {
'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',
},
'netapp': {
'mnt_point': '1.3.6.1.4.1.789.1.5.4.1.2',
'device_name': '1.3.6.1.4.1.789.1.5.4.1.10',
'size': '1.3.6.1.4.1.789.1.5.4.1.3',
'used': '1.3.6.1.4.1.789.1.5.4.1.4',
'percent': '1.3.6.1.4.1.789.1.5.4.1.6',
},
}
snmp_oid['esxi'] = snmp_oid['windows']
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'File system usage in percent', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances file system plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'mnt_point'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the FS stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stats using the psutil disk_partitions
# If 'all'=False return physical devices only (e.g. hard disks, cd-rom drives, USB keys)
# and ignore all others (e.g. memory partitions such as /dev/shm)
try:
fs_stat = psutil.disk_partitions(all=False)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil fetch failed")
return self.stats
# Optional hack to allow logical mounts points (issue #448)
allowed_fs_types = self.get_conf_value('allow')
if allowed_fs_types:
# Avoid Psutil call unless mounts need to be allowed
try:
all_mounted_fs = psutil.disk_partitions(all=True)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil extended fetch failed")
else:
# Discard duplicates (#2299) and add entries matching allowed fs types
tracked_mnt_points = set(f.mountpoint for f in fs_stat)
for f in all_mounted_fs:
if (
any(f.fstype.find(fs_type) >= 0 for fs_type in allowed_fs_types)
and f.mountpoint not in tracked_mnt_points
):
fs_stat.append(f)
# Loop over fs
for fs in fs_stat:
# Hide the stats if the mount point is in the exclude list
if not self.is_display(fs.mountpoint):
continue
# Grab the disk usage
try:
fs_usage = psutil.disk_usage(fs.mountpoint)
except OSError:
# Correct issue #346
# Disk is ejected during the command
continue
fs_current = {
'device_name': fs.device,
'fs_type': fs.fstype,
# Manage non breaking space (see issue #1065)
'mnt_point': u(fs.mountpoint).replace(u'\u00A0', ' '),
'size': fs_usage.total,
'used': fs_usage.used,
'free': fs_usage.free,
'percent': fs_usage.percent,
'key': self.get_key(),
}
# Hide the stats if the device name is in the exclude list
# Correct issue: glances.conf FS hide not applying #1666
if not self.is_display(fs_current['device_name']):
continue
# Add alias if exist (define in the configuration file)
if self.has_alias(fs_current['mnt_point']) is not None:
fs_current['alias'] = self.has_alias(fs_current['mnt_point'])
stats.append(fs_current)
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all file system in one shot
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True)
# Loop over fs
if self.short_system_name in ('windows', 'esxi'):
# Windows or ESXi tips
for fs in fs_stat:
# Memory stats are grabbed in the same OID table (ignore it)
if fs == 'Virtual Memory' or fs == 'Physical Memory' or fs == 'Real Memory':
continue
size = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
used = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
percent = float(used * 100 / size)
fs_current = {
'device_name': '',
'mnt_point': fs.partition(' ')[0],
'size': size,
'used': used,
'percent': percent,
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']):
continue
else:
stats.append(fs_current)
else:
# Default behavior
for fs in fs_stat:
fs_current = {
'device_name': fs_stat[fs]['device_name'],
'mnt_point': fs,
'size': int(fs_stat[fs]['size']) * 1024,
'used': int(fs_stat[fs]['used']) * 1024,
'percent': float(fs_stat[fs]['percent']),
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']) or self.is_hide(fs_current['device_name']):
continue
else:
stats.append(fs_current)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
self.views[i[self.get_key()]]['used']['decoration'] = self.get_alert(
current=i['size'] - i['free'], maximum=i['size'], header=i['mnt_point']
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 13
# Build the string message
# Header
msg = '{:{width}}'.format('FILE SYS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.fs_free_space:
msg = '{:>8}'.format('Free')
else:
msg = '{:>8}'.format('Used')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Total')
ret.append(self.curse_add_line(msg))
# Filesystem list (sorted by name)
for i in sorted(self.stats, key=operator.itemgetter(self.get_key())):
# New line
ret.append(self.curse_new_line())
mnt_point = i['alias'] if 'alias' in i else i['mnt_point']
if len(mnt_point) + len(i['device_name'].split('/')[-1]) <= name_max_width - 3:
# If possible concatenate mode info... Glances touch inside :)
mnt_point = i['mnt_point'] + ' (' + i['device_name'].split('/')[-1] + ')'
elif len(mnt_point) > name_max_width:
mnt_point = mnt_point[:name_max_width] + '_'
msg = '{:{width}}'.format(nativestr(mnt_point), width=name_max_width + 1)
ret.append(self.curse_add_line(msg))
if args.fs_free_space:
msg = '{:>7}'.format(self.auto_unit(i['free']))
else:
msg = '{:>7}'.format(self.auto_unit(i['used']))
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='used', option='decoration'))
)
msg = '{:>7}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg))
return ret

View File

@ -1,268 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""File system plugin."""
from __future__ import unicode_literals
import operator
from glances.globals import u, nativestr, PermissionError
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# SNMP OID
# The snmpd.conf needs to be edited.
# Add the following to enable it on all disk
# ...
# includeAllDisks 10%
# ...
# The OIDs are as follows (for the first disk)
# Path where the disk is mounted: .1.3.6.1.4.1.2021.9.1.2.1
# Path of the device for the partition: .1.3.6.1.4.1.2021.9.1.3.1
# Total size of the disk/partition (kBytes): .1.3.6.1.4.1.2021.9.1.6.1
# Available space on the disk: .1.3.6.1.4.1.2021.9.1.7.1
# 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 = {
'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',
},
'netapp': {
'mnt_point': '1.3.6.1.4.1.789.1.5.4.1.2',
'device_name': '1.3.6.1.4.1.789.1.5.4.1.10',
'size': '1.3.6.1.4.1.789.1.5.4.1.3',
'used': '1.3.6.1.4.1.789.1.5.4.1.4',
'percent': '1.3.6.1.4.1.789.1.5.4.1.6',
},
}
snmp_oid['esxi'] = snmp_oid['windows']
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'File system usage in percent', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances file system plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'mnt_point'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the FS stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab the stats using the psutil disk_partitions
# If 'all'=False return physical devices only (e.g. hard disks, cd-rom drives, USB keys)
# and ignore all others (e.g. memory partitions such as /dev/shm)
try:
fs_stat = psutil.disk_partitions(all=False)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil fetch failed")
return self.stats
# Optional hack to allow logical mounts points (issue #448)
allowed_fs_types = self.get_conf_value('allow')
if allowed_fs_types:
# Avoid Psutil call unless mounts need to be allowed
try:
all_mounted_fs = psutil.disk_partitions(all=True)
except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil extended fetch failed")
else:
# Discard duplicates (#2299) and add entries matching allowed fs types
tracked_mnt_points = set(f.mountpoint for f in fs_stat)
for f in all_mounted_fs:
if (
any(f.fstype.find(fs_type) >= 0 for fs_type in allowed_fs_types)
and f.mountpoint not in tracked_mnt_points
):
fs_stat.append(f)
# Loop over fs
for fs in fs_stat:
# Hide the stats if the mount point is in the exclude list
if not self.is_display(fs.mountpoint):
continue
# Grab the disk usage
try:
fs_usage = psutil.disk_usage(fs.mountpoint)
except OSError:
# Correct issue #346
# Disk is ejected during the command
continue
fs_current = {
'device_name': fs.device,
'fs_type': fs.fstype,
# Manage non breaking space (see issue #1065)
'mnt_point': u(fs.mountpoint).replace(u'\u00A0', ' '),
'size': fs_usage.total,
'used': fs_usage.used,
'free': fs_usage.free,
'percent': fs_usage.percent,
'key': self.get_key(),
}
# Hide the stats if the device name is in the exclude list
# Correct issue: glances.conf FS hide not applying #1666
if not self.is_display(fs_current['device_name']):
continue
stats.append(fs_current)
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all file system in one shot
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True)
# Loop over fs
if self.short_system_name in ('windows', 'esxi'):
# Windows or ESXi tips
for fs in fs_stat:
# Memory stats are grabbed in the same OID table (ignore it)
if fs == 'Virtual Memory' or fs == 'Physical Memory' or fs == 'Real Memory':
continue
size = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
used = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
percent = float(used * 100 / size)
fs_current = {
'device_name': '',
'mnt_point': fs.partition(' ')[0],
'size': size,
'used': used,
'percent': percent,
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']):
continue
else:
stats.append(fs_current)
else:
# Default behavior
for fs in fs_stat:
fs_current = {
'device_name': fs_stat[fs]['device_name'],
'mnt_point': fs,
'size': int(fs_stat[fs]['size']) * 1024,
'used': int(fs_stat[fs]['used']) * 1024,
'percent': float(fs_stat[fs]['percent']),
'key': self.get_key(),
}
# Do not take hidden file system into account
if self.is_hide(fs_current['mnt_point']) or self.is_hide(fs_current['device_name']):
continue
else:
stats.append(fs_current)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
self.views[i[self.get_key()]]['used']['decoration'] = self.get_alert(
current=i['size'] - i['free'], maximum=i['size'], header=i['mnt_point']
)
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Build the string message
# Header
msg = '{:{width}}'.format('FILE SYS', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.fs_free_space:
msg = '{:>7}'.format('Free')
else:
msg = '{:>7}'.format('Used')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Total')
ret.append(self.curse_add_line(msg))
# Filesystem list (sorted by name)
for i in sorted(self.stats, key=operator.itemgetter(self.get_key())):
# New line
ret.append(self.curse_new_line())
if i['device_name'] == '' or i['device_name'] == 'none':
mnt_point = i['mnt_point'][-name_max_width + 1 :]
elif len(i['mnt_point']) + len(i['device_name'].split('/')[-1]) <= name_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']) > name_max_width:
# Cut mount point name if it is too long
mnt_point = '_' + i['mnt_point'][-name_max_width + 1 :]
else:
mnt_point = i['mnt_point']
msg = '{:{width}}'.format(nativestr(mnt_point), width=name_max_width)
ret.append(self.curse_add_line(msg))
if args.fs_free_space:
msg = '{:>7}'.format(self.auto_unit(i['free']))
else:
msg = '{:>7}'.format(self.auto_unit(i['used']))
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='used', option='decoration'))
)
msg = '{:>7}'.format(self.auto_unit(i['size']))
ret.append(self.curse_add_line(msg))
return ret

View File

@ -0,0 +1,346 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2020 Kirby Banman <kirby.banman@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""GPU plugin (limited to NVIDIA chipsets)."""
from glances.globals import nativestr, to_fahrenheit
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# In Glances 3.1.4 or higher, we use the py3nvml lib (see issue #1523)
try:
import py3nvml.py3nvml as pynvml
except Exception as e:
import_error_tag = True
# Display debug message if import KeyError
logger.warning("Missing Python Lib ({}), Nvidia GPU plugin is disabled".format(e))
else:
import_error_tag = False
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'proc', 'description': 'GPU processor', 'y_unit': '%'},
{'name': 'mem', 'description': 'Memory consumption', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances GPU plugin (limited to NVIDIA chipsets).
stats is a list of dictionaries with one entry per GPU
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# Init the Nvidia API
self.init_nvidia()
# We want to display the stat in the curse interface
self.display_curse = True
def init_nvidia(self):
"""Init the NVIDIA API."""
if import_error_tag:
self.nvml_ready = False
try:
pynvml.nvmlInit()
self.device_handles = get_device_handles()
self.nvml_ready = True
except Exception:
logger.debug("pynvml could not be initialized.")
self.nvml_ready = False
return self.nvml_ready
def get_key(self):
"""Return the key of the list."""
return 'gpu_id'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the GPU stats."""
# Init new stats
stats = self.get_init_value()
if not self.nvml_ready:
# !!!
# Uncomment to test on computer without GPU
# One GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# }
# ]
# Two GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX1",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# },
# {
# "key": "gpu_id",
# "gpu_id": 1,
# "name": "Fake GeForce GTX2",
# "mem": 15,
# "proc": 8,
# "temperature": 65,
# "fan_speed": 75
# }
# ]
return self.stats
if self.input_method == 'local':
stats = self.get_device_stats()
elif self.input_method == 'snmp':
# not available
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
# Init the views for the current GPU
self.views[i[self.get_key()]] = {'proc': {}, 'mem': {}, 'temperature': {}}
# Processor alert
if 'proc' in i:
alert = self.get_alert(i['proc'], header='proc')
self.views[i[self.get_key()]]['proc']['decoration'] = alert
# Memory alert
if 'mem' in i:
alert = self.get_alert(i['mem'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
# Temperature alert
if 'temperature' in i:
alert = self.get_alert(i['temperature'], header='temperature')
self.views[i[self.get_key()]]['temperature']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == []) or self.is_disabled():
return ret
# Check if all GPU have the same name
same_name = all(s['name'] == self.stats[0]['name'] for s in self.stats)
# gpu_stats contain the first GPU in the list
gpu_stats = self.stats[0]
# Header
header = ''
if len(self.stats) > 1:
header += '{} '.format(len(self.stats))
if same_name:
header += '{} {}'.format('GPU', gpu_stats['name'])
else:
header += '{}'.format('GPU')
msg = header[:17]
ret.append(self.curse_add_line(msg, "TITLE"))
# Build the string message
if len(self.stats) == 1 or args.meangpu:
# GPU stat summary or mono GPU
# New line
ret.append(self.curse_new_line())
# GPU PROC
try:
mean_proc = sum(s['proc'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_proc_msg = '{:>4}'.format('N/A')
else:
mean_proc_msg = '{:>3.0f}%'.format(mean_proc)
if len(self.stats) > 1:
msg = '{:13}'.format('proc mean:')
else:
msg = '{:13}'.format('proc:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_proc_msg, self.get_views(item=gpu_stats[self.get_key()], key='proc', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU MEM
try:
mean_mem = sum(s['mem'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_mem_msg = '{:>4}'.format('N/A')
else:
mean_mem_msg = '{:>3.0f}%'.format(mean_mem)
if len(self.stats) > 1:
msg = '{:13}'.format('mem mean:')
else:
msg = '{:13}'.format('mem:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_mem_msg, self.get_views(item=gpu_stats[self.get_key()], key='mem', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU TEMPERATURE
try:
mean_temperature = sum(s['temperature'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_temperature_msg = '{:>4}'.format('N/A')
else:
unit = 'C'
if args.fahrenheit:
mean_temperature = to_fahrenheit(mean_temperature)
unit = 'F'
mean_temperature_msg = '{:>3.0f}{}'.format(mean_temperature, unit)
if len(self.stats) > 1:
msg = '{:13}'.format('temp mean:')
else:
msg = '{:13}'.format('temperature:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_temperature_msg,
self.get_views(item=gpu_stats[self.get_key()], key='temperature', option='decoration'),
)
)
else:
# Multi GPU
# Temperature is not displayed in this mode...
for gpu_stats in self.stats:
# New line
ret.append(self.curse_new_line())
# GPU ID + PROC + MEM + TEMPERATURE
id_msg = '{}'.format(gpu_stats['gpu_id'])
try:
proc_msg = '{:>3.0f}%'.format(gpu_stats['proc'])
except (ValueError, TypeError):
proc_msg = '{:>4}'.format('N/A')
try:
mem_msg = '{:>3.0f}%'.format(gpu_stats['mem'])
except (ValueError, TypeError):
mem_msg = '{:>4}'.format('N/A')
msg = '{}: {} mem: {}'.format(id_msg, proc_msg, mem_msg)
ret.append(self.curse_add_line(msg))
return ret
def get_device_stats(self):
"""Get GPU stats."""
stats = []
for index, device_handle in enumerate(self.device_handles):
device_stats = dict()
# Dictionary key is the GPU_ID
device_stats['key'] = self.get_key()
# GPU id (for multiple GPU, start at 0)
device_stats['gpu_id'] = index
# GPU name
device_stats['name'] = get_device_name(device_handle)
# Memory consumption in % (not available on all GPU)
device_stats['mem'] = get_mem(device_handle)
# Processor consumption in %
device_stats['proc'] = get_proc(device_handle)
# Processor temperature in °C
device_stats['temperature'] = get_temperature(device_handle)
# Fan speed in %
device_stats['fan_speed'] = get_fan_speed(device_handle)
stats.append(device_stats)
return stats
def exit(self):
"""Overwrite the exit method to close the GPU API."""
if self.nvml_ready:
try:
pynvml.nvmlShutdown()
except Exception as e:
logger.debug("pynvml failed to shutdown correctly ({})".format(e))
# Call the father exit method
super(PluginModel, self).exit()
def get_device_handles():
"""Get a list of NVML device handles, one per device.
Can throw NVMLError.
"""
return [pynvml.nvmlDeviceGetHandleByIndex(i) for i in range(pynvml.nvmlDeviceGetCount())]
def get_device_name(device_handle):
"""Get GPU device name."""
try:
return nativestr(pynvml.nvmlDeviceGetName(device_handle))
except pynvml.NVMLError:
return "NVIDIA"
def get_mem(device_handle):
"""Get GPU device memory consumption in percent."""
try:
memory_info = pynvml.nvmlDeviceGetMemoryInfo(device_handle)
return memory_info.used * 100.0 / memory_info.total
except pynvml.NVMLError:
return None
def get_proc(device_handle):
"""Get GPU device CPU consumption in percent."""
try:
return pynvml.nvmlDeviceGetUtilizationRates(device_handle).gpu
except pynvml.NVMLError:
return None
def get_temperature(device_handle):
"""Get GPU device CPU temperature in Celsius."""
try:
return pynvml.nvmlDeviceGetTemperature(device_handle, pynvml.NVML_TEMPERATURE_GPU)
except pynvml.NVMLError:
return None
def get_fan_speed(device_handle):
"""Get GPU device fan speed in percent."""
try:
return pynvml.nvmlDeviceGetFanSpeed(device_handle)
except pynvml.NVMLError:
return None

View File

@ -1,347 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2020 Kirby Banman <kirby.banman@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""GPU plugin (limited to NVIDIA chipsets)."""
from glances.globals import nativestr, to_fahrenheit
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
# In Glances 3.1.4 or higher, we use the py3nvml lib (see issue #1523)
try:
import py3nvml.py3nvml as pynvml
except Exception as e:
import_error_tag = True
# Display debug message if import KeyError
logger.warning("Missing Python Lib ({}), Nvidia GPU plugin is disabled".format(e))
else:
import_error_tag = False
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [
{'name': 'proc', 'description': 'GPU processor', 'y_unit': '%'},
{'name': 'mem', 'description': 'Memory consumption', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances GPU plugin (limited to NVIDIA chipsets).
stats is a list of dictionaries with one entry per GPU
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args,
config=config,
items_history_list=items_history_list,
stats_init_value=[])
# Init the Nvidia API
self.init_nvidia()
# We want to display the stat in the curse interface
self.display_curse = True
def init_nvidia(self):
"""Init the NVIDIA API."""
if import_error_tag:
self.nvml_ready = False
try:
pynvml.nvmlInit()
self.device_handles = get_device_handles()
self.nvml_ready = True
except Exception:
logger.debug("pynvml could not be initialized.")
self.nvml_ready = False
return self.nvml_ready
def get_key(self):
"""Return the key of the list."""
return 'gpu_id'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the GPU stats."""
# Init new stats
stats = self.get_init_value()
if not self.nvml_ready:
# !!!
# Uncomment to test on computer without GPU
# One GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# }
# ]
# Two GPU sample:
# self.stats = [
# {
# "key": "gpu_id",
# "gpu_id": 0,
# "name": "Fake GeForce GTX1",
# "mem": 5.792331695556641,
# "proc": 4,
# "temperature": 26,
# "fan_speed": 30
# },
# {
# "key": "gpu_id",
# "gpu_id": 1,
# "name": "Fake GeForce GTX2",
# "mem": 15,
# "proc": 8,
# "temperature": 65,
# "fan_speed": 75
# }
# ]
return self.stats
if self.input_method == 'local':
stats = self.get_device_stats()
elif self.input_method == 'snmp':
# not available
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert
for i in self.stats:
# Init the views for the current GPU
self.views[i[self.get_key()]] = {'proc': {}, 'mem': {}, 'temperature': {}}
# Processor alert
if 'proc' in i:
alert = self.get_alert(i['proc'], header='proc')
self.views[i[self.get_key()]]['proc']['decoration'] = alert
# Memory alert
if 'mem' in i:
alert = self.get_alert(i['mem'], header='mem')
self.views[i[self.get_key()]]['mem']['decoration'] = alert
# Temperature alert
if 'temperature' in i:
alert = self.get_alert(i['temperature'], header='temperature')
self.views[i[self.get_key()]]['temperature']['decoration'] = alert
return True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == []) or self.is_disabled():
return ret
# Check if all GPU have the same name
same_name = all(s['name'] == self.stats[0]['name'] for s in self.stats)
# gpu_stats contain the first GPU in the list
gpu_stats = self.stats[0]
# Header
header = ''
if len(self.stats) > 1:
header += '{} '.format(len(self.stats))
if same_name:
header += '{} {}'.format('GPU', gpu_stats['name'])
else:
header += '{}'.format('GPU')
msg = header[:17]
ret.append(self.curse_add_line(msg, "TITLE"))
# Build the string message
if len(self.stats) == 1 or args.meangpu:
# GPU stat summary or mono GPU
# New line
ret.append(self.curse_new_line())
# GPU PROC
try:
mean_proc = sum(s['proc'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_proc_msg = '{:>4}'.format('N/A')
else:
mean_proc_msg = '{:>3.0f}%'.format(mean_proc)
if len(self.stats) > 1:
msg = '{:13}'.format('proc mean:')
else:
msg = '{:13}'.format('proc:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_proc_msg, self.get_views(item=gpu_stats[self.get_key()], key='proc', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU MEM
try:
mean_mem = sum(s['mem'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_mem_msg = '{:>4}'.format('N/A')
else:
mean_mem_msg = '{:>3.0f}%'.format(mean_mem)
if len(self.stats) > 1:
msg = '{:13}'.format('mem mean:')
else:
msg = '{:13}'.format('mem:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_mem_msg, self.get_views(item=gpu_stats[self.get_key()], key='mem', option='decoration')
)
)
# New line
ret.append(self.curse_new_line())
# GPU TEMPERATURE
try:
mean_temperature = sum(s['temperature'] for s in self.stats if s is not None) / len(self.stats)
except TypeError:
mean_temperature_msg = '{:>4}'.format('N/A')
else:
unit = 'C'
if args.fahrenheit:
mean_temperature = to_fahrenheit(mean_temperature)
unit = 'F'
mean_temperature_msg = '{:>3.0f}{}'.format(mean_temperature, unit)
if len(self.stats) > 1:
msg = '{:13}'.format('temp mean:')
else:
msg = '{:13}'.format('temperature:')
ret.append(self.curse_add_line(msg))
ret.append(
self.curse_add_line(
mean_temperature_msg,
self.get_views(item=gpu_stats[self.get_key()], key='temperature', option='decoration'),
)
)
else:
# Multi GPU
# Temperature is not displayed in this mode...
for gpu_stats in self.stats:
# New line
ret.append(self.curse_new_line())
# GPU ID + PROC + MEM + TEMPERATURE
id_msg = '{}'.format(gpu_stats['gpu_id'])
try:
proc_msg = '{:>3.0f}%'.format(gpu_stats['proc'])
except (ValueError, TypeError):
proc_msg = '{:>4}'.format('N/A')
try:
mem_msg = '{:>3.0f}%'.format(gpu_stats['mem'])
except (ValueError, TypeError):
mem_msg = '{:>4}'.format('N/A')
msg = '{}: {} mem: {}'.format(id_msg, proc_msg, mem_msg)
ret.append(self.curse_add_line(msg))
return ret
def get_device_stats(self):
"""Get GPU stats."""
stats = []
for index, device_handle in enumerate(self.device_handles):
device_stats = dict()
# Dictionary key is the GPU_ID
device_stats['key'] = self.get_key()
# GPU id (for multiple GPU, start at 0)
device_stats['gpu_id'] = index
# GPU name
device_stats['name'] = get_device_name(device_handle)
# Memory consumption in % (not available on all GPU)
device_stats['mem'] = get_mem(device_handle)
# Processor consumption in %
device_stats['proc'] = get_proc(device_handle)
# Processor temperature in °C
device_stats['temperature'] = get_temperature(device_handle)
# Fan speed in %
device_stats['fan_speed'] = get_fan_speed(device_handle)
stats.append(device_stats)
return stats
def exit(self):
"""Overwrite the exit method to close the GPU API."""
if self.nvml_ready:
try:
pynvml.nvmlShutdown()
except Exception as e:
logger.debug("pynvml failed to shutdown correctly ({})".format(e))
# Call the father exit method
super(PluginModel, self).exit()
def get_device_handles():
"""Get a list of NVML device handles, one per device.
Can throw NVMLError.
"""
return [pynvml.nvmlDeviceGetHandleByIndex(i) for i in range(pynvml.nvmlDeviceGetCount())]
def get_device_name(device_handle):
"""Get GPU device name."""
try:
return nativestr(pynvml.nvmlDeviceGetName(device_handle))
except pynvml.NVMLError:
return "NVIDIA"
def get_mem(device_handle):
"""Get GPU device memory consumption in percent."""
try:
memory_info = pynvml.nvmlDeviceGetMemoryInfo(device_handle)
return memory_info.used * 100.0 / memory_info.total
except pynvml.NVMLError:
return None
def get_proc(device_handle):
"""Get GPU device CPU consumption in percent."""
try:
return pynvml.nvmlDeviceGetUtilizationRates(device_handle).gpu
except pynvml.NVMLError:
return None
def get_temperature(device_handle):
"""Get GPU device CPU temperature in Celsius."""
try:
return pynvml.nvmlDeviceGetTemperature(device_handle, pynvml.NVML_TEMPERATURE_GPU)
except pynvml.NVMLError:
return None
def get_fan_speed(device_handle):
"""Get GPU device fan speed in percent."""
try:
return pynvml.nvmlDeviceGetFanSpeed(device_handle)
except pynvml.NVMLError:
return None

View File

@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""
Help plugin.
Just a stupid plugin to display the help screen.
"""
import sys
from glances.globals import iteritems
from glances import __version__, psutil_version
from glances.plugins.plugin.model import GlancesPluginModel
from itertools import chain
class PluginModel(GlancesPluginModel):
"""Glances help plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# Set the config instance
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# init data dictionary, to preserve insertion order
if sys.version_info < (3, 6):
from collections import OrderedDict
self.view_data = OrderedDict()
else:
self.view_data = {}
self.generate_view_data()
def reset(self):
"""No stats. It is just a plugin to display the help."""
def update(self):
"""No stats. It is just a plugin to display the help."""
def generate_view_data(self):
"""Generate the views."""
self.view_data['version'] = '{} {}'.format('Glances', __version__)
self.view_data['psutil_version'] = ' with psutil {}'.format(psutil_version)
try:
self.view_data['configuration_file'] = 'Configuration file: {}'.format(self.config.loaded_config_file)
except AttributeError:
pass
msg_col = ' {0:1} {1:34}'
msg_header = '{0:39}'
self.view_data.update(
[
# First column
#
('header_sort', msg_header.format('SORT PROCESSES:')),
('sort_auto', msg_col.format('a', 'Automatically')),
('sort_cpu', msg_col.format('c', 'CPU%')),
('sort_io_rate', msg_col.format('i', 'I/O rate')),
('sort_mem', msg_col.format('m', 'MEM%')),
('sort_process_name', msg_col.format('p', 'Process name')),
('sort_cpu_times', msg_col.format('t', 'TIME')),
('sort_user', msg_col.format('u', 'USER')),
('header_show_hide', msg_header.format('SHOW/HIDE SECTION:')),
('show_hide_application_monitoring', msg_col.format('A', 'Application monitoring')),
('show_hide_diskio', msg_col.format('d', 'Disk I/O')),
('show_hide_docker', msg_col.format('D', 'Docker')),
('show_hide_top_extended_stats', msg_col.format('e', 'Top extended stats')),
('show_hide_filesystem', msg_col.format('f', 'Filesystem')),
('show_hide_gpu', msg_col.format('G', 'GPU')),
('show_hide_ip', msg_col.format('I', 'IP')),
('show_hide_tcp_connection', msg_col.format('K', 'TCP')),
('show_hide_alert', msg_col.format('l', 'Alert logs')),
('show_hide_network', msg_col.format('n', 'Network')),
('show_hide_current_time', msg_col.format('N', 'Time')),
('show_hide_irq', msg_col.format('Q', 'IRQ')),
('show_hide_raid_plugin', msg_col.format('R', 'RAID')),
('show_hide_sensors', msg_col.format('s', 'Sensors')),
('show_hide_wifi_module', msg_col.format('W', 'Wifi')),
('show_hide_processes', msg_col.format('z', 'Processes')),
('show_hide_left_sidebar', msg_col.format('2', 'Left sidebar')),
# Second column
#
('show_hide_quick_look', msg_col.format('3', 'Quick Look')),
('show_hide_cpu_mem_swap', msg_col.format('4', 'CPU, MEM, and SWAP')),
('show_hide_all', msg_col.format('5', 'ALL')),
('header_toggle', msg_header.format('TOGGLE DATA TYPE:')),
('toggle_bits_bytes', msg_col.format('b', 'Network I/O, bits/bytes')),
('toggle_count_rate', msg_col.format('B', 'Disk I/O, count/rate')),
('toggle_used_free', msg_col.format('F', 'Filesystem space, used/free')),
('toggle_bar_sparkline', msg_col.format('S', 'Quick Look, bar/sparkline')),
('toggle_separate_combined', msg_col.format('T', 'Network I/O, separate/combined')),
('toggle_live_cumulative', msg_col.format('U', 'Network I/O, live/cumulative')),
('toggle_linux_percentage', msg_col.format('0', 'Load, Linux/percentage')),
('toggle_cpu_individual_combined', msg_col.format('1', 'CPU, individual/combined')),
('toggle_gpu_individual_combined', msg_col.format('6', 'GPU, individual/combined')),
('toggle_short_full', msg_col.format('/', 'Process names, short/full')),
('header_miscellaneous', msg_header.format('MISCELLANEOUS:')),
('misc_erase_process_filter', msg_col.format('E', 'Erase process filter')),
('misc_generate_history_graphs', msg_col.format('g', 'Generate history graphs')),
('misc_help', msg_col.format('h', 'HELP')),
('misc_accumulate_processes_by_program', msg_col.format('j', 'Display threads or programs')),
('misc_increase_nice_process', msg_col.format('+', 'Increase nice process')),
('misc_decrease_nice_process', msg_col.format('-', 'Decrease nice process (need admin rights)')),
('misc_kill_process', msg_col.format('k', 'Kill process')),
('misc_reset_processes_summary_min_max', msg_col.format('M', 'Reset processes summary min/max')),
('misc_quit', msg_col.format('q', 'QUIT (or Esc or Ctrl-C)')),
('misc_reset_history', msg_col.format('r', 'Reset history')),
('misc_delete_warning_alerts', msg_col.format('w', 'Delete warning alerts')),
('misc_delete_warning_and_critical_alerts', msg_col.format('x', 'Delete warning & critical alerts')),
('misc_theme_white', msg_col.format('9', 'Optimize colors for white background')),
('misc_edit_process_filter_pattern', ' ENTER: Edit process filter pattern'),
]
)
def get_view_data(self, args=None):
"""Return the view."""
return self.view_data
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the curse interface."""
# Init the return message
ret = []
# Build the header message
ret.append(self.curse_add_line(self.view_data['version'], 'TITLE'))
ret.append(self.curse_add_line(self.view_data['psutil_version']))
ret.append(self.curse_new_line())
# Build the configuration file path
if 'configuration_file' in self.view_data:
ret.append(self.curse_add_line(self.view_data['configuration_file']))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
# key-shortcuts
#
# Collect all values after the 1st key-msg
# in a list of curse-lines.
#
shortcuts = []
collecting = False
for k, v in iteritems(self.view_data):
if collecting:
pass
elif k == 'header_sort':
collecting = True
else:
continue
shortcuts.append(self.curse_add_line(v))
# Divide shortcuts into 2 columns
# and if number of schortcuts is even,
# make the 1st column taller (len+1).
#
nlines = (len(shortcuts) + 1) // 2
ret.extend(
msg
for triplet in zip(
iter(shortcuts[:nlines]),
chain(shortcuts[nlines:], iter(lambda: self.curse_add_line(''), None)),
iter(self.curse_new_line, None),
)
for msg in triplet
)
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('For an exhaustive list of key bindings:'))
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('https://glances.readthedocs.io/en/latest/cmds.html#interactive-commands'))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('Colors binding:'))
ret.append(self.curse_new_line())
for c in [
'DEFAULT',
'UNDERLINE',
'BOLD',
'SORT',
'OK',
'MAX',
'FILTER',
'TITLE',
'PROCESS',
'PROCESS_SELECTED',
'STATUS',
'NICE',
'CPU_TIME',
'CAREFUL',
'WARNING',
'CRITICAL',
'OK_LOG',
'CAREFUL_LOG',
'WARNING_LOG',
'CRITICAL_LOG',
'PASSWORD',
'SELECTED',
'INFO',
'ERROR',
'SEPARATOR',
]:
ret.append(self.curse_add_line(c, decoration=c))
if c == 'CPU_TIME':
ret.append(self.curse_new_line())
else:
ret.append(self.curse_add_line(' '))
# Return the message with decoration
return ret

View File

@ -1,183 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""
Help plugin.
Just a stupid plugin to display the help screen.
"""
import sys
from glances.globals import iteritems
from glances import __version__, psutil_version
from glances.plugins.plugin.model import GlancesPluginModel
from itertools import chain
class PluginModel(GlancesPluginModel):
"""Glances help plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# Set the config instance
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# init data dictionary, to preserve insertion order
if sys.version_info < (3, 6):
from collections import OrderedDict
self.view_data = OrderedDict()
else:
self.view_data = {}
self.generate_view_data()
def reset(self):
"""No stats. It is just a plugin to display the help."""
def update(self):
"""No stats. It is just a plugin to display the help."""
def generate_view_data(self):
"""Generate the views."""
self.view_data['version'] = '{} {}'.format('Glances', __version__)
self.view_data['psutil_version'] = ' with psutil {}'.format(psutil_version)
try:
self.view_data['configuration_file'] = 'Configuration file: {}'.format(self.config.loaded_config_file)
except AttributeError:
pass
msg_col = ' {0:1} {1:34}'
msg_header = '{0:39}'
self.view_data.update(
[
# First column
#
('header_sort', msg_header.format('SORT PROCESSES:')),
('sort_auto', msg_col.format('a', 'Automatically')),
('sort_cpu', msg_col.format('c', 'CPU%')),
('sort_io_rate', msg_col.format('i', 'I/O rate')),
('sort_mem', msg_col.format('m', 'MEM%')),
('sort_process_name', msg_col.format('p', 'Process name')),
('sort_cpu_times', msg_col.format('t', 'TIME')),
('sort_user', msg_col.format('u', 'USER')),
('header_show_hide', msg_header.format('SHOW/HIDE SECTION:')),
('show_hide_application_monitoring', msg_col.format('A', 'Application monitoring')),
('show_hide_diskio', msg_col.format('d', 'Disk I/O')),
('show_hide_docker', msg_col.format('D', 'Docker')),
('show_hide_top_extended_stats', msg_col.format('e', 'Top extended stats')),
('show_hide_filesystem', msg_col.format('f', 'Filesystem')),
('show_hide_gpu', msg_col.format('G', 'GPU')),
('show_hide_ip', msg_col.format('I', 'IP')),
('show_hide_tcp_connection', msg_col.format('K', 'TCP')),
('show_hide_alert', msg_col.format('l', 'Alert logs')),
('show_hide_network', msg_col.format('n', 'Network')),
('show_hide_current_time', msg_col.format('N', 'Time')),
('show_hide_irq', msg_col.format('Q', 'IRQ')),
('show_hide_raid_plugin', msg_col.format('R', 'RAID')),
('show_hide_sensors', msg_col.format('s', 'Sensors')),
('show_hide_wifi_module', msg_col.format('W', 'Wifi')),
('show_hide_processes', msg_col.format('z', 'Processes')),
('show_hide_left_sidebar', msg_col.format('2', 'Left sidebar')),
# Second column
#
('show_hide_quick_look', msg_col.format('3', 'Quick Look')),
('show_hide_cpu_mem_swap', msg_col.format('4', 'CPU, MEM, and SWAP')),
('show_hide_all', msg_col.format('5', 'ALL')),
('header_toggle', msg_header.format('TOGGLE DATA TYPE:')),
('toggle_bits_bytes', msg_col.format('b', 'Network I/O, bits/bytes')),
('toggle_count_rate', msg_col.format('B', 'Disk I/O, count/rate')),
('toggle_used_free', msg_col.format('F', 'Filesystem space, used/free')),
('toggle_bar_sparkline', msg_col.format('S', 'Quick Look, bar/sparkline')),
('toggle_separate_combined', msg_col.format('T', 'Network I/O, separate/combined')),
('toggle_live_cumulative', msg_col.format('U', 'Network I/O, live/cumulative')),
('toggle_linux_percentage', msg_col.format('0', 'Load, Linux/percentage')),
('toggle_cpu_individual_combined', msg_col.format('1', 'CPU, individual/combined')),
('toggle_gpu_individual_combined', msg_col.format('6', 'GPU, individual/combined')),
('toggle_short_full', msg_col.format('/', 'Process names, short/full')),
('header_miscellaneous', msg_header.format('MISCELLANEOUS:')),
('misc_erase_process_filter', msg_col.format('E', 'Erase process filter')),
('misc_generate_history_graphs', msg_col.format('g', 'Generate history graphs')),
('misc_help', msg_col.format('h', 'HELP')),
('misc_accumulate_processes_by_program', msg_col.format('j', 'Display threads or programs')),
('misc_increase_nice_process', msg_col.format('+', 'Increase nice process')),
('misc_decrease_nice_process', msg_col.format('-', 'Decrease nice process (need admin rights)')),
('misc_kill_process', msg_col.format('k', 'Kill process')),
('misc_reset_processes_summary_min_max', msg_col.format('M', 'Reset processes summary min/max')),
('misc_quit', msg_col.format('q', 'QUIT (or Esc or Ctrl-C)')),
('misc_reset_history', msg_col.format('r', 'Reset history')),
('misc_delete_warning_alerts', msg_col.format('w', 'Delete warning alerts')),
('misc_delete_warning_and_critical_alerts', msg_col.format('x', 'Delete warning & critical alerts')),
('misc_theme_white', msg_col.format('9', 'Optimize colors for white background')),
('misc_edit_process_filter_pattern', ' ENTER: Edit process filter pattern'),
]
)
def get_view_data(self, args=None):
"""Return the view."""
return self.view_data
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the curse interface."""
# Init the return message
ret = []
# Build the header message
ret.append(self.curse_add_line(self.view_data['version'], 'TITLE'))
ret.append(self.curse_add_line(self.view_data['psutil_version']))
ret.append(self.curse_new_line())
# Build the configuration file path
if 'configuration_file' in self.view_data:
ret.append(self.curse_add_line(self.view_data['configuration_file']))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
# key-shortcuts
#
# Collect all values after the 1st key-msg
# in a list of curse-lines.
#
shortcuts = []
collecting = False
for k, v in iteritems(self.view_data):
if collecting:
pass
elif k == 'header_sort':
collecting = True
else:
continue
shortcuts.append(self.curse_add_line(v))
# Divide shortcuts into 2 columns
# and if number of schortcuts is even,
# make the 1st column taller (len+1).
#
nlines = (len(shortcuts) + 1) // 2
ret.extend(
msg
for triplet in zip(
iter(shortcuts[:nlines]),
chain(shortcuts[nlines:], iter(lambda: self.curse_add_line(''), None)),
iter(self.curse_new_line, None),
)
for msg in triplet
)
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('For an exhaustive list of key bindings:'))
ret.append(self.curse_new_line())
ret.append(self.curse_add_line('https://glances.readthedocs.io/en/latest/cmds.html#interactive-commands'))
# Return the message with decoration
return ret

View File

@ -0,0 +1,300 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IP plugin."""
import threading
from ujson import loads
from glances.globals import urlopen, queue, urlopen_auth
from glances.logger import logger
from glances.timer import Timer
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
# Import plugin specific dependency
try:
import netifaces
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), IP plugin is disabled".format(e))
else:
import_error_tag = False
# List of online services to retrieve public IP address
# List of tuple (url, json, key)
# - url: URL of the Web site
# - json: service return a JSON (True) or string (False)
# - key: key of the IP address in the JSON structure
urls = [
('https://httpbin.org/ip', True, 'origin'),
('https://api.ipify.org/?format=json', True, 'ip'),
('https://ipv4.jsonip.com', True, 'ip'),
]
class PluginModel(GlancesPluginModel):
"""Glances IP Plugin.
stats is a dict
"""
_default_public_refresh_interval = 300
_default_public_ip_disabled = ["False"]
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# For public IP address
self.public_address = ""
self.public_address_refresh_interval = self.get_conf_value(
"public_refresh_interval", default=self._default_public_refresh_interval
)
public_ip_disabled = self.get_conf_value("public_ip_disabled", default=self._default_public_ip_disabled)
self.public_ip_disabled = True if public_ip_disabled == ["True"] else False
# For the Censys options (see issue #2105)
self.public_info = ""
self.censys_url = self.get_conf_value("censys_url", default=[None])[0]
self.censys_username = self.get_conf_value("censys_username", default=[None])[0]
self.censys_password = self.get_conf_value("censys_password", default=[None])[0]
self.censys_fields = self.get_conf_value("censys_fields", default=[None])
self.public_info_disabled = (
self.censys_url is None
or self.censys_username is None
or self.censys_password is None
or self.censys_fields is None
)
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update IP stats using the input method.
:return: the stats dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local' and not import_error_tag:
# Update stats using the netifaces lib
# Start with the default IP gateway
try:
default_gw = netifaces.gateways()['default'][netifaces.AF_INET]
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab default gateway IP address ({})".format(e))
return {}
else:
stats['gateway'] = default_gw[0]
# Then the private IP address
# If multiple IP addresses are available, only the one with the default gateway is returned
try:
address = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['addr']
mask = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['netmask']
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab private IP address ({})".format(e))
return {}
else:
stats['address'] = address
stats['mask'] = mask
stats['mask_cidr'] = self.ip_to_cidr(stats['mask'])
# Finally with the public IP address
time_since_update = getTimeSinceLastUpdate('public-ip')
try:
if not self.public_ip_disabled and (
self.stats.get('address') != address or time_since_update > self.public_address_refresh_interval
):
self.public_address = PublicIpAddress().get()
if not self.public_info_disabled:
self.public_info = PublicIpInfo(
self.public_address, self.censys_url, self.censys_username, self.censys_password
).get()
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab public IP information ({})".format(e))
else:
stats['public_address'] = self.public_address
# Too much information provided in the public_info
# Limit it to public_info_for_human
# stats['public_info'] = self.public_info
stats['public_info_human'] = self.public_info_for_human(self.public_info)
elif self.input_method == 'snmp':
# Not implemented yet
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled() or import_error_tag:
return ret
# Build the string message
msg = ' - '
ret.append(self.curse_add_line(msg, optional=True))
# Start with the private IP information
msg = 'IP '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
if 'address' in self.stats:
msg = '{}'.format(self.stats['address'])
ret.append(self.curse_add_line(msg, optional=True))
if 'mask_cidr' in self.stats:
# VPN with no internet access (issue #842)
msg = '/{}'.format(self.stats['mask_cidr'])
ret.append(self.curse_add_line(msg, optional=True))
# Then with the public IP information
try:
msg_pub = '{}'.format(self.stats['public_address'])
except (UnicodeEncodeError, KeyError):
# Add KeyError exception (see https://github.com/nicolargo/glances/issues/1469)
pass
else:
if self.stats['public_address']:
msg = ' Pub '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
ret.append(self.curse_add_line(msg_pub, optional=True))
if self.stats['public_info_human']:
ret.append(self.curse_add_line(' {}'.format(self.stats['public_info_human']), optional=True))
return ret
def public_info_for_human(self, public_info):
"""Return the data to pack to the client."""
if not public_info:
return ''
field_result = []
for f in self.censys_fields:
field = f.split(':')
if len(field) == 1 and field[0] in public_info:
field_result.append('{}'.format(public_info[field[0]]))
elif len(field) == 2 and field[0] in public_info and field[1] in public_info[field[0]]:
field_result.append('{}'.format(public_info[field[0]][field[1]]))
return '/'.join(field_result)
@staticmethod
def ip_to_cidr(ip):
"""Convert IP address to CIDR.
Example: '255.255.255.0' will return 24
"""
# Thanks to @Atticfire
# See https://github.com/nicolargo/glances/issues/1417#issuecomment-469894399
if ip is None:
# Correct issue #1528
return 0
return sum(bin(int(x)).count('1') for x in ip.split('.'))
class PublicIpAddress(object):
"""Get public IP address from online services."""
def __init__(self, timeout=2):
"""Init the class."""
self.timeout = timeout
def get(self):
"""Get the first public IP address returned by one of the online services."""
q = queue.Queue()
for u, j, k in urls:
t = threading.Thread(target=self._get_ip_public, args=(q, u, j, k))
t.daemon = True
t.start()
timer = Timer(self.timeout)
ip = None
while not timer.finished() and ip is None:
if q.qsize() > 0:
ip = q.get()
if ip is None:
return None
return ', '.join(set([x.strip() for x in ip.split(',')]))
def _get_ip_public(self, queue_target, url, json=False, key=None):
"""Request the url service and put the result in the queue_target."""
try:
response = urlopen(url, timeout=self.timeout).read().decode('utf-8')
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(url, e))
queue_target.put(None)
else:
# Request depend on service
try:
if not json:
queue_target.put(response)
else:
queue_target.put(loads(response)[key])
except ValueError:
queue_target.put(None)
class PublicIpInfo(object):
"""Get public IP information from Censys online service."""
def __init__(self, ip, url, username, password, timeout=2):
"""Init the class."""
self.ip = ip
self.url = url
self.username = username
self.password = password
self.timeout = timeout
def get(self):
"""Return the public IP information returned by one of the online service."""
q = queue.Queue()
t = threading.Thread(target=self._get_ip_public_info, args=(q, self.ip, self.url, self.username, self.password))
t.daemon = True
t.start()
timer = Timer(self.timeout)
info = None
while not timer.finished() and info is None:
if q.qsize() > 0:
info = q.get()
if info is None:
return None
return info
def _get_ip_public_info(self, queue_target, ip, url, username, password):
"""Request the url service and put the result in the queue_target."""
request_url = "{}/v2/hosts/{}".format(url, ip)
try:
response = urlopen_auth(request_url, username, password).read()
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(request_url, e))
queue_target.put(None)
else:
try:
queue_target.put(loads(response)['result'])
except (ValueError, KeyError) as e:
logger.debug("IP plugin - Cannot get result field from {} ({})".format(request_url, e))
queue_target.put(None)

View File

@ -1,300 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IP plugin."""
import threading
from ujson import loads
from glances.globals import urlopen, queue, urlopen_auth
from glances.logger import logger
from glances.timer import Timer
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
# Import plugin specific dependency
try:
import netifaces
except ImportError as e:
import_error_tag = True
logger.warning("Missing Python Lib ({}), IP plugin is disabled".format(e))
else:
import_error_tag = False
# List of online services to retrieve public IP address
# List of tuple (url, json, key)
# - url: URL of the Web site
# - json: service return a JSON (True) or string (False)
# - key: key of the IP address in the JSON structure
urls = [
('https://httpbin.org/ip', True, 'origin'),
('https://api.ipify.org/?format=json', True, 'ip'),
('https://ipv4.jsonip.com', True, 'ip'),
]
class PluginModel(GlancesPluginModel):
"""Glances IP Plugin.
stats is a dict
"""
_default_public_refresh_interval = 300
_default_public_ip_disabled = ["False"]
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# For public IP address
self.public_address = ""
self.public_address_refresh_interval = self.get_conf_value(
"public_refresh_interval", default=self._default_public_refresh_interval
)
public_ip_disabled = self.get_conf_value("public_ip_disabled", default=self._default_public_ip_disabled)
self.public_ip_disabled = True if public_ip_disabled == ["True"] else False
# For the Censys options (see issue #2105)
self.public_info = ""
self.censys_url = self.get_conf_value("censys_url", default=[None])[0]
self.censys_username = self.get_conf_value("censys_username", default=[None])[0]
self.censys_password = self.get_conf_value("censys_password", default=[None])[0]
self.censys_fields = self.get_conf_value("censys_fields", default=[None])
self.public_info_disabled = (
self.censys_url is None
or self.censys_username is None
or self.censys_password is None
or self.censys_fields is None
)
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update IP stats using the input method.
:return: the stats dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local' and not import_error_tag:
# Update stats using the netifaces lib
# Start with the default IP gateway
try:
default_gw = netifaces.gateways()['default'][netifaces.AF_INET]
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab default gateway IP address ({})".format(e))
return {}
else:
stats['gateway'] = default_gw[0]
# Then the private IP address
# If multiple IP addresses are available, only the one with the default gateway is returned
try:
address = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['addr']
mask = netifaces.ifaddresses(default_gw[1])[netifaces.AF_INET][0]['netmask']
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab private IP address ({})".format(e))
return {}
else:
stats['address'] = address
stats['mask'] = mask
stats['mask_cidr'] = self.ip_to_cidr(stats['mask'])
# Finally with the public IP address
time_since_update = getTimeSinceLastUpdate('public-ip')
try:
if not self.public_ip_disabled and (
self.stats.get('address') != address or time_since_update > self.public_address_refresh_interval
):
self.public_address = PublicIpAddress().get()
if not self.public_info_disabled:
self.public_info = PublicIpInfo(
self.public_address, self.censys_url, self.censys_username, self.censys_password
).get()
except (KeyError, AttributeError) as e:
logger.debug("Cannot grab public IP information ({})".format(e))
else:
stats['public_address'] = self.public_address
# Too much information provided in the public_info
# Limit it to public_info_for_human
# stats['public_info'] = self.public_info
stats['public_info_human'] = self.public_info_for_human(self.public_info)
elif self.input_method == 'snmp':
# Not implemented yet
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled() or import_error_tag:
return ret
# Build the string message
msg = ' - '
ret.append(self.curse_add_line(msg, optional=True))
# Start with the private IP information
msg = 'IP '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
if 'address' in self.stats:
msg = '{}'.format(self.stats['address'])
ret.append(self.curse_add_line(msg, optional=True))
if 'mask_cidr' in self.stats:
# VPN with no internet access (issue #842)
msg = '/{}'.format(self.stats['mask_cidr'])
ret.append(self.curse_add_line(msg, optional=True))
# Then with the public IP information
try:
msg_pub = '{}'.format(self.stats['public_address'])
except (UnicodeEncodeError, KeyError):
# Add KeyError exception (see https://github.com/nicolargo/glances/issues/1469)
pass
else:
if self.stats['public_address']:
msg = ' Pub '
ret.append(self.curse_add_line(msg, 'TITLE', optional=True))
ret.append(self.curse_add_line(msg_pub, optional=True))
if self.stats['public_info_human']:
ret.append(self.curse_add_line(' {}'.format(self.stats['public_info_human']), optional=True))
return ret
def public_info_for_human(self, public_info):
"""Return the data to pack to the client."""
if not public_info:
return ''
field_result = []
for f in self.censys_fields:
field = f.split(':')
if len(field) == 1 and field[0] in public_info:
field_result.append('{}'.format(public_info[field[0]]))
elif len(field) == 2 and field[0] in public_info and field[1] in public_info[field[0]]:
field_result.append('{}'.format(public_info[field[0]][field[1]]))
return '/'.join(field_result)
@staticmethod
def ip_to_cidr(ip):
"""Convert IP address to CIDR.
Example: '255.255.255.0' will return 24
"""
# Thanks to @Atticfire
# See https://github.com/nicolargo/glances/issues/1417#issuecomment-469894399
if ip is None:
# Correct issue #1528
return 0
return sum(bin(int(x)).count('1') for x in ip.split('.'))
class PublicIpAddress(object):
"""Get public IP address from online services."""
def __init__(self, timeout=2):
"""Init the class."""
self.timeout = timeout
def get(self):
"""Get the first public IP address returned by one of the online services."""
q = queue.Queue()
for u, j, k in urls:
t = threading.Thread(target=self._get_ip_public, args=(q, u, j, k))
t.daemon = True
t.start()
timer = Timer(self.timeout)
ip = None
while not timer.finished() and ip is None:
if q.qsize() > 0:
ip = q.get()
if ip is None:
return None
return ', '.join(set([x.strip() for x in ip.split(',')]))
def _get_ip_public(self, queue_target, url, json=False, key=None):
"""Request the url service and put the result in the queue_target."""
try:
response = urlopen(url, timeout=self.timeout).read().decode('utf-8')
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(url, e))
queue_target.put(None)
else:
# Request depend on service
try:
if not json:
queue_target.put(response)
else:
queue_target.put(loads(response)[key])
except ValueError:
queue_target.put(None)
class PublicIpInfo(object):
"""Get public IP information from Censys online service."""
def __init__(self, ip, url, username, password, timeout=2):
"""Init the class."""
self.ip = ip
self.url = url
self.username = username
self.password = password
self.timeout = timeout
def get(self):
"""Return the public IP information returned by one of the online service."""
q = queue.Queue()
t = threading.Thread(target=self._get_ip_public_info, args=(q, self.ip, self.url, self.username, self.password))
t.daemon = True
t.start()
timer = Timer(self.timeout)
info = None
while not timer.finished() and info is None:
if q.qsize() > 0:
info = q.get()
if info is None:
return None
return info
def _get_ip_public_info(self, queue_target, ip, url, username, password):
"""Request the url service and put the result in the queue_target."""
request_url = "{}/v2/hosts/{}".format(url, ip)
try:
response = urlopen_auth(request_url, username, password).read()
except Exception as e:
logger.debug("IP plugin - Cannot open URL {} ({})".format(request_url, e))
queue_target.put(None)
else:
try:
queue_target.put(loads(response)['result'])
except (ValueError, KeyError) as e:
logger.debug("IP plugin - Cannot get result field from {} ({})".format(request_url, e))
queue_target.put(None)

View File

@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2018 Angelo Poerio <angelo.poerio@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IRQ plugin."""
import os
import operator
from glances.globals import LINUX
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances IRQ plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.irq = GlancesIRQ()
def get_key(self):
"""Return the key of the list."""
return self.irq.get_key()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the IRQ stats."""
# Init new stats
stats = self.get_init_value()
# IRQ plugin only available on GNU/Linux
if not LINUX:
return self.stats
if self.input_method == 'local':
# Grab the stats
stats = self.irq.get()
elif self.input_method == 'snmp':
# not available
pass
# Get the TOP 5 (by rate/s)
stats = sorted(stats, key=operator.itemgetter('irq_rate'), reverse=True)[:5]
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only available on GNU/Linux
# Only process if stats exist and display plugin enable...
if not LINUX or not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Build the string message
# Header
msg = '{:{width}}'.format('IRQ', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>9}'.format('Rate/s')
ret.append(self.curse_add_line(msg))
for i in self.stats:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(i['irq_line'][:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
msg = '{:>9}'.format(str(i['irq_rate']))
ret.append(self.curse_add_line(msg))
return ret
class GlancesIRQ(object):
"""This class manages the IRQ file."""
IRQ_FILE = '/proc/interrupts'
def __init__(self):
"""Init the class.
The stat are stored in a internal list of dict
"""
self.lasts = {}
self.reset()
def reset(self):
"""Reset the stats."""
self.stats = []
self.cpu_number = 0
def get(self):
"""Return the current IRQ stats."""
return self.__update()
def get_key(self):
"""Return the key of the dict."""
return 'irq_line'
def __header(self, line):
"""Build the header (contain the number of CPU).
CPU0 CPU1 CPU2 CPU3
0: 21 0 0 0 IO-APIC 2-edge timer
"""
self.cpu_number = len(line.split())
return self.cpu_number
def __humanname(self, line):
"""Return the IRQ name, alias or number (choose the best for human).
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
"""
splitted_line = line.split()
irq_line = splitted_line[0].replace(':', '')
if irq_line.isdigit():
# If the first column is a digit, use the alias (last column)
irq_line += '_{}'.format(splitted_line[-1])
return irq_line
def __sum(self, line):
"""Return the IRQ sum number.
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
FIQ: usb_fiq
"""
splitted_line = line.split()
try:
ret = sum(map(int, splitted_line[1 : (self.cpu_number + 1)]))
except ValueError:
# Correct issue #1007 on some conf (Raspberry Pi with Raspbian)
ret = 0
return ret
def __update(self):
"""Load the IRQ file and update the internal dict."""
self.reset()
if not os.path.exists(self.IRQ_FILE):
# Correct issue #947: IRQ file do not exist on OpenVZ container
return self.stats
try:
with open(self.IRQ_FILE) as irq_proc:
time_since_update = getTimeSinceLastUpdate('irq')
# Read the header
self.__header(irq_proc.readline())
# Read the rest of the lines (one line per IRQ)
for line in irq_proc.readlines():
irq_line = self.__humanname(line)
current_irqs = self.__sum(line)
irq_rate = int(
current_irqs - self.lasts.get(irq_line) if self.lasts.get(irq_line) else 0 // time_since_update
)
irq_current = {
'irq_line': irq_line,
'irq_rate': irq_rate,
'key': self.get_key(),
'time_since_update': time_since_update,
}
self.stats.append(irq_current)
self.lasts[irq_line] = current_irqs
except (OSError, IOError):
pass
return self.stats

View File

@ -1,198 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2018 Angelo Poerio <angelo.poerio@gmail.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""IRQ plugin."""
import os
import operator
from glances.globals import LINUX
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Glances IRQ plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
# We want to display the stat in the curse interface
self.display_curse = True
# Init the stats
self.irq = GlancesIRQ()
def get_key(self):
"""Return the key of the list."""
return self.irq.get_key()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the IRQ stats."""
# Init new stats
stats = self.get_init_value()
# IRQ plugin only available on GNU/Linux
if not LINUX:
return self.stats
if self.input_method == 'local':
# Grab the stats
stats = self.irq.get()
elif self.input_method == 'snmp':
# not available
pass
# Get the TOP 5 (by rate/s)
stats = sorted(stats, key=operator.itemgetter('irq_rate'), reverse=True)[:5]
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only available on GNU/Linux
# Only process if stats exist and display plugin enable...
if not LINUX or not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Build the string message
# Header
msg = '{:{width}}'.format('IRQ', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
msg = '{:>9}'.format('Rate/s')
ret.append(self.curse_add_line(msg))
for i in self.stats:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(i['irq_line'][:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
msg = '{:>9}'.format(str(i['irq_rate']))
ret.append(self.curse_add_line(msg))
return ret
class GlancesIRQ(object):
"""This class manages the IRQ file."""
IRQ_FILE = '/proc/interrupts'
def __init__(self):
"""Init the class.
The stat are stored in a internal list of dict
"""
self.lasts = {}
self.reset()
def reset(self):
"""Reset the stats."""
self.stats = []
self.cpu_number = 0
def get(self):
"""Return the current IRQ stats."""
return self.__update()
def get_key(self):
"""Return the key of the dict."""
return 'irq_line'
def __header(self, line):
"""Build the header (contain the number of CPU).
CPU0 CPU1 CPU2 CPU3
0: 21 0 0 0 IO-APIC 2-edge timer
"""
self.cpu_number = len(line.split())
return self.cpu_number
def __humanname(self, line):
"""Return the IRQ name, alias or number (choose the best for human).
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
"""
splitted_line = line.split()
irq_line = splitted_line[0].replace(':', '')
if irq_line.isdigit():
# If the first column is a digit, use the alias (last column)
irq_line += '_{}'.format(splitted_line[-1])
return irq_line
def __sum(self, line):
"""Return the IRQ sum number.
IRQ line samples:
1: 44487 341 44 72 IO-APIC 1-edge i8042
LOC: 33549868 22394684 32474570 21855077 Local timer interrupts
FIQ: usb_fiq
"""
splitted_line = line.split()
try:
ret = sum(map(int, splitted_line[1 : (self.cpu_number + 1)]))
except ValueError:
# Correct issue #1007 on some conf (Raspberry Pi with Raspbian)
ret = 0
return ret
def __update(self):
"""Load the IRQ file and update the internal dict."""
self.reset()
if not os.path.exists(self.IRQ_FILE):
# Correct issue #947: IRQ file do not exist on OpenVZ container
return self.stats
try:
with open(self.IRQ_FILE) as irq_proc:
time_since_update = getTimeSinceLastUpdate('irq')
# Read the header
self.__header(irq_proc.readline())
# Read the rest of the lines (one line per IRQ)
for line in irq_proc.readlines():
irq_line = self.__humanname(line)
current_irqs = self.__sum(line)
irq_rate = int(
current_irqs - self.lasts.get(irq_line) if self.lasts.get(irq_line) else 0 // time_since_update
)
irq_current = {
'irq_line': irq_line,
'irq_rate': irq_rate,
'key': self.get_key(),
'time_since_update': time_since_update,
}
self.stats.append(irq_current)
self.lasts[irq_line] = current_irqs
except (OSError, IOError):
pass
return self.stats

View File

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Load plugin."""
import os
import psutil
from glances.globals import iteritems
from glances.plugins.core import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Fields description
fields_description = {
'min1': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 1 minute.',
'unit': 'float',
},
'min5': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 5 minutes.',
'unit': 'float',
},
'min15': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 15 minutes.',
'unit': 'float',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
}
# SNMP OID
# 1 minute Load: .1.3.6.1.4.1.2021.10.1.3.1
# 5 minute Load: .1.3.6.1.4.1.2021.10.1.3.2
# 15 minute Load: .1.3.6.1.4.1.2021.10.1.3.3
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
items_history_list = [
{'name': 'min1', 'description': '1 minute load'},
{'name': 'min5', 'description': '5 minutes load'},
{'name': 'min15', 'description': '15 minutes load'},
]
class PluginModel(GlancesPluginModel):
"""Glances load plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception as e:
logger.warning('Error: Can not retrieve the CPU core number (set it to 1) ({})'.format(e))
self.nb_log_core = 1
def _getloadavg(self):
"""Get load average. On both Linux and Windows thanks to PsUtil"""
try:
return psutil.getloadavg()
except (AttributeError, OSError):
pass
try:
return os.getloadavg()
except (AttributeError, OSError):
return None
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update load stats."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Get the load using the os standard lib
load = self._getloadavg()
if load is None:
stats = self.get_init_value()
else:
stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], 'cpucore': self.nb_log_core}
elif self.input_method == 'snmp':
# Update stats using SNMP
stats = self.get_stats_snmp(snmp_oid=snmp_oid)
if stats['min1'] == '':
stats = self.get_init_value()
return stats
# Python 3 return a dict like:
# {'min1': "b'0.08'", 'min5': "b'0.12'", 'min15': "b'0.15'"}
for k, v in iteritems(stats):
stats[k] = float(v)
stats['cpucore'] = self.nb_log_core
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
try:
# Alert and log
self.views['min15']['decoration'] = self.get_alert_log(
self.stats['min15'], maximum=100 * self.stats['cpucore']
)
# Alert only
self.views['min5']['decoration'] = self.get_alert(self.stats['min5'], maximum=100 * self.stats['cpucore'])
except KeyError:
# try/except mandatory for Windows compatibility (no load stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == {}) or self.is_disabled():
return ret
# Build the string message
# Header
msg = '{:4}'.format('LOAD')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:1}'.format(self.trend_msg(self.get_trend('min1')))
ret.append(self.curse_add_line(msg))
# Core number
if 'cpucore' in self.stats and self.stats['cpucore'] > 0:
msg = '{:3}core'.format(int(self.stats['cpucore']))
ret.append(self.curse_add_line(msg))
# Loop over 1min, 5min and 15min load
for load_time in ['1', '5', '15']:
ret.append(self.curse_new_line())
msg = '{:7}'.format('{} min'.format(load_time))
ret.append(self.curse_add_line(msg))
if args.disable_irix and self.nb_log_core != 0:
# Enable Irix mode for load (see issue #1554)
load_stat = self.stats['min{}'.format(load_time)] / self.nb_log_core * 100
msg = '{:>5.1f}%'.format(load_stat)
else:
# Default mode for load
load_stat = self.stats['min{}'.format(load_time)]
msg = '{:>6.2f}'.format(load_stat)
ret.append(self.curse_add_line(msg, self.get_views(key='min{}'.format(load_time), option='decoration')))
return ret

View File

@ -1,183 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Load plugin."""
import os
import psutil
from glances.globals import iteritems
from glances.plugins.core.model import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
# Fields description
fields_description = {
'min1': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 1 minute.',
'unit': 'float',
},
'min5': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 5 minutes.',
'unit': 'float',
},
'min15': {
'description': 'Average sum of the number of processes \
waiting in the run-queue plus the number currently executing \
over 15 minutes.',
'unit': 'float',
},
'cpucore': {'description': 'Total number of CPU core.', 'unit': 'number'},
}
# SNMP OID
# 1 minute Load: .1.3.6.1.4.1.2021.10.1.3.1
# 5 minute Load: .1.3.6.1.4.1.2021.10.1.3.2
# 15 minute Load: .1.3.6.1.4.1.2021.10.1.3.3
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
items_history_list = [
{'name': 'min1', 'description': '1 minute load'},
{'name': 'min5', 'description': '5 minutes load'},
{'name': 'min15', 'description': '15 minutes load'},
]
class PluginModel(GlancesPluginModel):
"""Glances load plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
# Call CorePluginModel in order to display the core number
try:
self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
except Exception as e:
logger.warning('Error: Can not retrieve the CPU core number (set it to 1) ({})'.format(e))
self.nb_log_core = 1
def _getloadavg(self):
"""Get load average. On both Linux and Windows thanks to PsUtil"""
try:
return psutil.getloadavg()
except (AttributeError, OSError):
pass
try:
return os.getloadavg()
except (AttributeError, OSError):
return None
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update load stats."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Get the load using the os standard lib
load = self._getloadavg()
if load is None:
stats = self.get_init_value()
else:
stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], 'cpucore': self.nb_log_core}
elif self.input_method == 'snmp':
# Update stats using SNMP
stats = self.get_stats_snmp(snmp_oid=snmp_oid)
if stats['min1'] == '':
stats = self.get_init_value()
return stats
# Python 3 return a dict like:
# {'min1': "b'0.08'", 'min5': "b'0.12'", 'min15': "b'0.15'"}
for k, v in iteritems(stats):
stats[k] = float(v)
stats['cpucore'] = self.nb_log_core
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
try:
# Alert and log
self.views['min15']['decoration'] = self.get_alert_log(
self.stats['min15'], maximum=100 * self.stats['cpucore']
)
# Alert only
self.views['min5']['decoration'] = self.get_alert(self.stats['min5'], maximum=100 * self.stats['cpucore'])
except KeyError:
# try/except mandatory for Windows compatibility (no load stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist, not empty (issue #871) and plugin not disabled
if not self.stats or (self.stats == {}) or self.is_disabled():
return ret
# Build the string message
# Header
msg = '{:4}'.format('LOAD')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:1}'.format(self.trend_msg(self.get_trend('min1')))
ret.append(self.curse_add_line(msg))
# Core number
if 'cpucore' in self.stats and self.stats['cpucore'] > 0:
msg = '{:3}core'.format(int(self.stats['cpucore']))
ret.append(self.curse_add_line(msg))
# Loop over 1min, 5min and 15min load
for load_time in ['1', '5', '15']:
ret.append(self.curse_new_line())
msg = '{:7}'.format('{} min'.format(load_time))
ret.append(self.curse_add_line(msg))
if args.disable_irix and self.nb_log_core != 0:
# Enable Irix mode for load (see issue #1554)
load_stat = self.stats['min{}'.format(load_time)] / self.nb_log_core * 100
msg = '{:>5.1f}%'.format(load_stat)
else:
# Default mode for load
load_stat = self.stats['min{}'.format(load_time)]
msg = '{:>6.2f}'.format(load_stat)
ret.append(self.curse_add_line(msg, self.get_views(key='min{}'.format(load_time), option='decoration')))
return ret

View File

@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Virtual memory plugin."""
from glances.globals import iterkeys
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total physical memory available.', 'unit': 'bytes', 'min_symbol': 'K'},
'available': {
'description': 'The actual amount of available memory that can be given instantly \
to processes that request more memory in bytes; this is calculated by summing \
different memory values depending on the platform (e.g. free + buffers + cached on Linux) \
and it is supposed to be used to monitor actual memory usage in a cross platform fashion.',
'unit': 'bytes',
'min_symbol': 'K',
},
'percent': {
'description': 'The percentage usage calculated as (total - available) / total * 100.',
'unit': 'percent',
},
'used': {
'description': 'Memory used, calculated differently depending on the platform and \
designed for informational purposes only.',
'unit': 'bytes',
'min_symbol': 'K',
},
'free': {
'description': 'Memory not being used at all (zeroed) that is readily available; \
note that this doesn\'t reflect the actual memory available (use \'available\' instead).',
'unit': 'bytes',
'min_symbol': 'K',
},
'active': {
'description': '*(UNIX)*: memory currently in use or very recently used, and so it is in RAM.',
'unit': 'bytes',
'min_symbol': 'K',
},
'inactive': {
'description': '*(UNIX)*: memory that is marked as not used.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'inacti',
},
'buffers': {
'description': '*(Linux, BSD)*: cache for things like file system metadata.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'buffer',
},
'cached': {'description': '*(Linux, BSD)*: cache for various things.', 'unit': 'bytes', 'min_symbol': 'K'},
'wired': {
'description': '*(BSD, macOS)*: memory that is marked to always stay in RAM. It is never moved to disk.',
'unit': 'bytes',
'min_symbol': 'K',
},
'shared': {
'description': '*(BSD)*: memory that may be simultaneously accessed by multiple processes.',
'unit': 'bytes',
'min_symbol': 'K',
},
}
# SNMP OID
# Total RAM in machine: .1.3.6.1.4.1.2021.4.5.0
# Total RAM used: .1.3.6.1.4.1.2021.4.6.0
# Total RAM Free: .1.3.6.1.4.1.2021.4.11.0
# 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
# 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
items_history_list = [{'name': 'percent', 'description': 'RAM memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances' memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update RAM memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab MEM using the psutil virtual_memory method
vm_stats = psutil.virtual_memory()
# Get all the memory stats (copy/paste of the psutil documentation)
# total: total physical memory available.
# available: the actual amount of available memory that can be given instantly
# to processes that request more memory in bytes; this is calculated by summing
# different memory values depending on the platform (e.g. free + buffers + cached on Linux)
# and it is supposed to be used to monitor actual memory usage in a cross platform fashion.
# percent: the percentage usage calculated as (total - available) / total * 100.
# used: memory used, calculated differently depending on the platform and designed for informational
# purposes only.
# free: memory not being used at all (zeroed) that is readily available; note that this doesn't
# reflect the actual memory available (use available instead).
# Platform-specific fields:
# active: (UNIX): memory currently in use or very recently used, and so it is in RAM.
# inactive: (UNIX): memory that is marked as not used.
# buffers: (Linux, BSD): cache for things like file system metadata.
# cached: (Linux, BSD): cache for various things.
# wired: (BSD, macOS): 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.reset()
for mem in [
'total',
'available',
'percent',
'used',
'free',
'active',
'inactive',
'buffers',
'cached',
'wired',
'shared',
]:
if hasattr(vm_stats, mem):
stats[mem] = getattr(vm_stats, mem)
# Use the 'free'/htop calculation
# free=available+buffer+cached
stats['free'] = stats['available']
if hasattr(stats, 'buffers'):
stats['free'] += stats['buffers']
if hasattr(stats, 'cached'):
stats['free'] += stats['cached']
# used=total-free
stats['used'] = stats['total'] - stats['free']
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name in ('windows', 'esxi'):
# Mem stats for Windows|Vmware Esxi are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.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'):
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
# Default behavior for others OS
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return self.stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# Use the 'free'/htop calculation
stats['free'] = stats['free'] - stats['total'] + (stats['buffers'] + stats['cached'])
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total - available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
# Optional
for key in ['active', 'inactive', 'buffers', 'cached']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total% + active
msg = '{}'.format('MEM')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>7.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Active memory usage
ret.extend(self.curse_add_stat('active', width=16, header=' '))
# Second line
# total + inactive
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Inactive memory usage
ret.extend(self.curse_add_stat('inactive', width=16, header=' '))
# Third line
# used + buffers
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Buffers memory usage
ret.extend(self.curse_add_stat('buffers', width=16, header=' '))
# Fourth line
# free + cached
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
# Cached memory usage
ret.extend(self.curse_add_stat('cached', width=16, header=' '))
return ret

View File

@ -1,285 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Virtual memory plugin."""
from glances.globals import iterkeys
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total physical memory available.', 'unit': 'bytes', 'min_symbol': 'K'},
'available': {
'description': 'The actual amount of available memory that can be given instantly \
to processes that request more memory in bytes; this is calculated by summing \
different memory values depending on the platform (e.g. free + buffers + cached on Linux) \
and it is supposed to be used to monitor actual memory usage in a cross platform fashion.',
'unit': 'bytes',
'min_symbol': 'K',
},
'percent': {
'description': 'The percentage usage calculated as (total - available) / total * 100.',
'unit': 'percent',
},
'used': {
'description': 'Memory used, calculated differently depending on the platform and \
designed for informational purposes only.',
'unit': 'bytes',
'min_symbol': 'K',
},
'free': {
'description': 'Memory not being used at all (zeroed) that is readily available; \
note that this doesn\'t reflect the actual memory available (use \'available\' instead).',
'unit': 'bytes',
'min_symbol': 'K',
},
'active': {
'description': '*(UNIX)*: memory currently in use or very recently used, and so it is in RAM.',
'unit': 'bytes',
'min_symbol': 'K',
},
'inactive': {
'description': '*(UNIX)*: memory that is marked as not used.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'inacti',
},
'buffers': {
'description': '*(Linux, BSD)*: cache for things like file system metadata.',
'unit': 'bytes',
'min_symbol': 'K',
'short_name': 'buffer',
},
'cached': {'description': '*(Linux, BSD)*: cache for various things.', 'unit': 'bytes', 'min_symbol': 'K'},
'wired': {
'description': '*(BSD, macOS)*: memory that is marked to always stay in RAM. It is never moved to disk.',
'unit': 'bytes',
'min_symbol': 'K',
},
'shared': {
'description': '*(BSD)*: memory that may be simultaneously accessed by multiple processes.',
'unit': 'bytes',
'min_symbol': 'K',
},
}
# SNMP OID
# Total RAM in machine: .1.3.6.1.4.1.2021.4.5.0
# Total RAM used: .1.3.6.1.4.1.2021.4.6.0
# Total RAM Free: .1.3.6.1.4.1.2021.4.11.0
# 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
# 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
items_history_list = [{'name': 'percent', 'description': 'RAM memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances' memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
items_history_list=items_history_list,
fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update RAM memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab MEM using the psutil virtual_memory method
vm_stats = psutil.virtual_memory()
# Get all the memory stats (copy/paste of the psutil documentation)
# total: total physical memory available.
# available: the actual amount of available memory that can be given instantly
# to processes that request more memory in bytes; this is calculated by summing
# different memory values depending on the platform (e.g. free + buffers + cached on Linux)
# and it is supposed to be used to monitor actual memory usage in a cross platform fashion.
# percent: the percentage usage calculated as (total - available) / total * 100.
# used: memory used, calculated differently depending on the platform and designed for informational
# purposes only.
# free: memory not being used at all (zeroed) that is readily available; note that this doesn't
# reflect the actual memory available (use available instead).
# Platform-specific fields:
# active: (UNIX): memory currently in use or very recently used, and so it is in RAM.
# inactive: (UNIX): memory that is marked as not used.
# buffers: (Linux, BSD): cache for things like file system metadata.
# cached: (Linux, BSD): cache for various things.
# wired: (BSD, macOS): 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.reset()
for mem in [
'total',
'available',
'percent',
'used',
'free',
'active',
'inactive',
'buffers',
'cached',
'wired',
'shared',
]:
if hasattr(vm_stats, mem):
stats[mem] = getattr(vm_stats, mem)
# Use the 'free'/htop calculation
# free=available+buffer+cached
stats['free'] = stats['available']
if hasattr(stats, 'buffers'):
stats['free'] += stats['buffers']
if hasattr(stats, 'cached'):
stats['free'] += stats['cached']
# used=total-free
stats['used'] = stats['total'] - stats['free']
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name in ('windows', 'esxi'):
# Mem stats for Windows|Vmware Esxi are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.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'):
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
# Default behavior for others OS
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return self.stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# Use the 'free'/htop calculation
stats['free'] = stats['free'] - stats['total'] + (stats['buffers'] + stats['cached'])
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total - available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
# Optional
for key in ['active', 'inactive', 'buffers', 'cached']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total% + active
msg = '{}'.format('MEM')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>7.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Active memory usage
ret.extend(self.curse_add_stat('active', width=16, header=' '))
# Second line
# total + inactive
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Inactive memory usage
ret.extend(self.curse_add_stat('inactive', width=16, header=' '))
# Third line
# used + buffers
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Buffers memory usage
ret.extend(self.curse_add_stat('buffers', width=16, header=' '))
# Fourth line
# free + cached
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
# Cached memory usage
ret.extend(self.curse_add_stat('cached', width=16, header=' '))
return ret

View File

@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Swap memory plugin."""
from glances.globals import iterkeys
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'used': {'description': 'Used swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'free': {'description': 'Free swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'percent': {'description': 'Used swap memory in percentage.', 'unit': 'percent'},
'sin': {
'description': 'The number of bytes the system has swapped in from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'sout': {
'description': 'The number of bytes the system has swapped out from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# 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 = {
'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',
},
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'Swap memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances swap memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update swap memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab SWAP using the psutil swap_memory method
try:
sm_stats = psutil.swap_memory()
except RuntimeError:
# Crash on startup on Illumos when no swap is configured #1767
pass
else:
# Get all the swap stats (copy/paste of the psutil documentation)
# total: total swap memory in bytes
# used: used swap memory in bytes
# free: free swap memory in bytes
# percent: the percentage usage
# sin: the number of bytes the system has swapped in from disk (cumulative)
# sout: the number of bytes the system has swapped out from disk (cumulative)
for swap in ['total', 'used', 'free', 'percent', 'sin', 'sout']:
if hasattr(sm_stats, swap):
stats[swap] = getattr(sm_stats, swap)
# By storing time data we enable sin/s and sout/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('memswap')
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name == 'windows':
# Mem stats for Windows OS are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.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':
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total -
# available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
if 'used' in self.stats and 'total' in self.stats and 'percent' in self.stats:
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total%
msg = '{:4}'.format('SWAP')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>6.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Second line
# total
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Third line
# used
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Fourth line
# free
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
return ret

View File

@ -1,191 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Swap memory plugin."""
from glances.globals import iterkeys
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
import psutil
# Fields description
fields_description = {
'total': {'description': 'Total swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'used': {'description': 'Used swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'free': {'description': 'Free swap memory.', 'unit': 'bytes', 'min_symbol': 'K'},
'percent': {'description': 'Used swap memory in percentage.', 'unit': 'percent'},
'sin': {
'description': 'The number of bytes the system has swapped in from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'sout': {
'description': 'The number of bytes the system has swapped out from disk (cumulative).',
'unit': 'bytes',
'min_symbol': 'K',
},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# 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 = {
'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',
},
}
# Define the history items list
# All items in this list will be historised if the --enable-history tag is set
items_history_list = [{'name': 'percent', 'description': 'Swap memory usage', 'y_unit': '%'}]
class PluginModel(GlancesPluginModel):
"""Glances swap memory plugin.
stats is a dict
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, fields_description=fields_description
)
# We want to display the stat in the curse interface
self.display_curse = True
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update swap memory stats using the input method."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab SWAP using the psutil swap_memory method
try:
sm_stats = psutil.swap_memory()
except RuntimeError:
# Crash on startup on Illumos when no swap is configured #1767
pass
else:
# Get all the swap stats (copy/paste of the psutil documentation)
# total: total swap memory in bytes
# used: used swap memory in bytes
# free: free swap memory in bytes
# percent: the percentage usage
# sin: the number of bytes the system has swapped in from disk (cumulative)
# sout: the number of bytes the system has swapped out from disk (cumulative)
for swap in ['total', 'used', 'free', 'percent', 'sin', 'sout']:
if hasattr(sm_stats, swap):
stats[swap] = getattr(sm_stats, swap)
# By storing time data we enable sin/s and sout/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
stats['time_since_update'] = getTimeSinceLastUpdate('memswap')
elif self.input_method == 'snmp':
# Update stats using SNMP
if self.short_system_name == 'windows':
# Mem stats for Windows OS are stored in the FS table
try:
fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.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':
stats['total'] = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit'])
stats['used'] = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit'])
stats['percent'] = float(stats['used'] * 100 / stats['total'])
stats['free'] = stats['total'] - stats['used']
break
else:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['total'] == '':
self.reset()
return stats
for key in iterkeys(stats):
if stats[key] != '':
stats[key] = float(stats[key]) * 1024
# used=total-free
stats['used'] = stats['total'] - stats['free']
# percent: the percentage usage calculated as (total -
# available) / total * 100.
stats['percent'] = float((stats['total'] - stats['free']) / stats['total'] * 100)
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
if 'used' in self.stats and 'total' in self.stats and 'percent' in self.stats:
self.views['percent']['decoration'] = self.get_alert_log(self.stats['used'], maximum=self.stats['total'])
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disabled
if not self.stats or self.is_disabled():
return ret
# First line
# total%
msg = '{:4}'.format('SWAP')
ret.append(self.curse_add_line(msg, "TITLE"))
msg = ' {:2}'.format(self.trend_msg(self.get_trend('percent')))
ret.append(self.curse_add_line(msg))
# Percent memory usage
msg = '{:>6.1%}'.format(self.stats['percent'] / 100)
ret.append(self.curse_add_line(msg, self.get_views(key='percent', option='decoration')))
# Second line
# total
ret.append(self.curse_new_line())
# Total memory usage
ret.extend(self.curse_add_stat('total', width=15))
# Third line
# used
ret.append(self.curse_new_line())
# Used memory usage
ret.extend(self.curse_add_stat('used', width=15))
# Fourth line
# free
ret.append(self.curse_new_line())
# Free memory usage
ret.extend(self.curse_add_stat('free', width=15))
return ret

View File

@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Network plugin."""
from __future__ import unicode_literals
import base64
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
import psutil
# {'interface_name': 'mpqemubr0-dummy',
# 'alias': None,
# 'time_since_update': 2.081636428833008,
# 'cumulative_rx': 0,
# 'rx': 0, 'cumulative_tx': 0, 'tx': 0, 'cumulative_cx': 0, 'cx': 0,
# 'is_up': False,
# 'speed': 0,
# 'key': 'interface_name'}
# Fields description
fields_description = {
'interface_name': {'description': 'Interface name.', 'unit': 'string'},
'alias': {'description': 'Interface alias name (optional).', 'unit': 'string'},
'rx': {'description': 'The received/input rate (in bit per second).', 'unit': 'bps'},
'tx': {'description': 'The sent/output rate (in bit per second).', 'unit': 'bps'},
'cx': {'description': 'The cumulative received+sent rate (in bit per second).', 'unit': 'bps'},
'cumulative_rx': {
'description': 'The number of bytes received through the interface (cumulative).',
'unit': 'bytes',
},
'cumulative_tx': {'description': 'The number of bytes sent through the interface (cumulative).', 'unit': 'bytes'},
'cumulative_cx': {
'description': 'The cumulative number of bytes reveived and sent through the interface (cumulative).',
'unit': 'bytes',
},
'speed': {
'description': 'Maximum interface speed (in bit per second). Can return 0 on some operating-system.',
'unit': 'bps',
},
'is_up': {'description': 'Is the interface up ?', 'unit': 'bool'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# http://www.net-snmp.org/docs/mibs/interfaces.html
# Dict key = interface_name
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',
}
}
# Define the history items list
items_history_list = [
{'name': 'rx', 'description': 'Download rate per second', 'y_unit': 'bit/s'},
{'name': 'tx', 'description': 'Upload rate per second', 'y_unit': 'bit/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances network plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
items_history_list=items_history_list,
fields_description=fields_description,
stats_init_value=[],
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['rx', 'tx']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'interface_name'
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update network stats using the input method.
:return: list of stats dict (one dict per interface)
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab network interface stat using the psutil net_io_counter method
try:
net_io_counters = psutil.net_io_counters(pernic=True)
except UnicodeDecodeError as e:
logger.debug('Can not get network interface counters ({})'.format(e))
return self.stats
# Grab interface's status (issue #765)
# Grab interface's speed (issue #718)
net_status = {}
try:
net_status = psutil.net_if_stats()
except OSError as e:
# see psutil #797/glances #1106
logger.debug('Can not get network interface status ({})'.format(e))
# Previous network interface stats are stored in the network_old variable
if not hasattr(self, 'network_old'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
return self.stats
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
# or KeyError: 'eth0' when interface is not connected #1348
if not self.is_display(net) or net not in net_status:
continue
try:
cumulative_rx = network_new[net].bytes_recv
cumulative_tx = network_new[net].bytes_sent
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - self.network_old[net].bytes_recv
tx = cumulative_tx - self.network_old[net].bytes_sent
cx = rx + tx
netstat = {
'interface_name': net,
'alias': self.has_alias(net),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
# Interface status
'is_up': net_status[net].isup,
# Interface speed in Mbps, convert it to bps
# Can be always 0 on some OSes
'speed': net_status[net].speed * 1048576,
# Set the key for the dict
'key': self.get_key(),
}
except KeyError:
continue
else:
# Append the interface stats to the list
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all network interface in one shot
try:
net_io_counters = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
net_io_counters = self.get_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'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
else:
# See description in the 'local' block
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
if not self.is_display(net):
continue
try:
# 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.short_system_name == 'windows':
try:
interface_name = str(base64.b16decode(net[2:-2].upper()))
except TypeError:
interface_name = net
else:
interface_name = net
cumulative_rx = float(network_new[net]['cumulative_rx'])
cumulative_tx = float(network_new[net]['cumulative_tx'])
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - float(self.network_old[net]['cumulative_rx'])
tx = cumulative_tx - float(self.network_old[net]['cumulative_tx'])
cx = rx + tx
netstat = {
'interface_name': interface_name,
'alias': self.has_alias(interface_name),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
}
except KeyError:
continue
else:
netstat['key'] = self.get_key()
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
if i['time_since_update'] == 0:
# Skip alert if no timespan to measure
continue
if_real_name = i['interface_name'].split(':')[0]
# Convert rate in bps (to be able to compare to interface speed)
bps_rx = int(i['rx'] // i['time_since_update'] * 8)
bps_tx = int(i['tx'] // i['time_since_update'] * 8)
# Decorate the bitrate with the configuration file thresholds
alert_rx = self.get_alert(bps_rx, header=if_real_name + '_rx')
alert_tx = self.get_alert(bps_tx, header=if_real_name + '_tx')
# If nothing is define in the configuration file...
# ... then use the interface speed (not available on all systems)
if alert_rx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_rx = self.get_alert(current=bps_rx, maximum=i['speed'], header='rx')
if alert_tx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_tx = self.get_alert(current=bps_tx, maximum=i['speed'], header='tx')
# then decorates
self.views[i[self.get_key()]]['rx']['decoration'] = alert_rx
self.views[i[self.get_key()]]['tx']['decoration'] = alert_tx
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('NETWORK', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.network_cumul:
# Cumulative stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx')
ret.append(self.curse_add_line(msg))
else:
# Rx/Tx stats
msg = '{:>7}'.format('Rx')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx')
ret.append(self.curse_add_line(msg))
else:
# Bitrate stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
# Interface list (sorted by name)
for i in self.sorted_stats():
# Do not display interface in down state (issue #765)
if ('is_up' in i) and (i['is_up'] is False):
continue
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Format stats
# Is there an alias for the interface name ?
if i['alias'] is None:
if_name = i['interface_name'].split(':')[0]
else:
if_name = i['alias']
if len(if_name) > name_max_width:
# Cut interface name if it is too long
if_name = '_' + if_name[-name_max_width + 1 :]
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
if args.network_cumul:
rx = self.auto_unit(int(i['cumulative_rx'] * to_bit)) + unit
tx = self.auto_unit(int(i['cumulative_tx'] * to_bit)) + unit
sx = self.auto_unit(int(i['cumulative_rx'] * to_bit) + int(i['cumulative_tx'] * to_bit)) + unit
else:
rx = self.auto_unit(int(i['rx'] // i['time_since_update'] * to_bit)) + unit
tx = self.auto_unit(int(i['tx'] // i['time_since_update'] * to_bit)) + unit
sx = (
self.auto_unit(
int(i['rx'] // i['time_since_update'] * to_bit)
+ int(i['tx'] // i['time_since_update'] * to_bit)
)
+ unit
)
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(if_name, width=name_max_width)
ret.append(self.curse_add_line(msg))
if args.network_sum:
msg = '{:>14}'.format(sx)
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format(rx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='rx', option='decoration'))
)
msg = '{:>7}'.format(tx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='tx', option='decoration'))
)
return ret

View File

@ -1,389 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Network plugin."""
from __future__ import unicode_literals
import base64
from glances.timer import getTimeSinceLastUpdate
from glances.plugins.plugin.model import GlancesPluginModel
from glances.logger import logger
import psutil
# {'interface_name': 'mpqemubr0-dummy',
# 'alias': None,
# 'time_since_update': 2.081636428833008,
# 'cumulative_rx': 0,
# 'rx': 0, 'cumulative_tx': 0, 'tx': 0, 'cumulative_cx': 0, 'cx': 0,
# 'is_up': False,
# 'speed': 0,
# 'key': 'interface_name'}
# Fields description
fields_description = {
'interface_name': {'description': 'Interface name.', 'unit': 'string'},
'alias': {'description': 'Interface alias name (optional).', 'unit': 'string'},
'rx': {'description': 'The received/input rate (in bit per second).', 'unit': 'bps'},
'tx': {'description': 'The sent/output rate (in bit per second).', 'unit': 'bps'},
'cx': {'description': 'The cumulative received+sent rate (in bit per second).', 'unit': 'bps'},
'cumulative_rx': {
'description': 'The number of bytes received through the interface (cumulative).',
'unit': 'bytes',
},
'cumulative_tx': {'description': 'The number of bytes sent through the interface (cumulative).', 'unit': 'bytes'},
'cumulative_cx': {
'description': 'The cumulative number of bytes reveived and sent through the interface (cumulative).',
'unit': 'bytes',
},
'speed': {
'description': 'Maximum interface speed (in bit per second). Can return 0 on some operating-system.',
'unit': 'bps',
},
'is_up': {'description': 'Is the interface up ?', 'unit': 'bool'},
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
}
# SNMP OID
# http://www.net-snmp.org/docs/mibs/interfaces.html
# Dict key = interface_name
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',
}
}
# Define the history items list
items_history_list = [
{'name': 'rx', 'description': 'Download rate per second', 'y_unit': 'bit/s'},
{'name': 'tx', 'description': 'Upload rate per second', 'y_unit': 'bit/s'},
]
class PluginModel(GlancesPluginModel):
"""Glances network plugin.
stats is a list
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args,
config=config,
items_history_list=items_history_list,
fields_description=fields_description,
stats_init_value=[],
)
# We want to display the stat in the curse interface
self.display_curse = True
# Hide stats if it has never been != 0
if config is not None:
self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
else:
self.hide_zero = False
self.hide_zero_fields = ['rx', 'tx']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
def get_key(self):
"""Return the key of the list."""
return 'interface_name'
# @GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update network stats using the input method.
:return: list of stats dict (one dict per interface)
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the standard system lib
# Grab network interface stat using the psutil net_io_counter method
try:
net_io_counters = psutil.net_io_counters(pernic=True)
except UnicodeDecodeError as e:
logger.debug('Can not get network interface counters ({})'.format(e))
return self.stats
# Grab interface's status (issue #765)
# Grab interface's speed (issue #718)
net_status = {}
try:
net_status = psutil.net_if_stats()
except OSError as e:
# see psutil #797/glances #1106
logger.debug('Can not get network interface status ({})'.format(e))
# Previous network interface stats are stored in the network_old variable
if not hasattr(self, 'network_old'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
return self.stats
# By storing time data we enable Rx/s and Tx/s calculations in the
# XML/RPC API, which would otherwise be overly difficult work
# for users of the API
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
# or KeyError: 'eth0' when interface is not connected #1348
if not self.is_display(net) or net not in net_status:
continue
try:
cumulative_rx = network_new[net].bytes_recv
cumulative_tx = network_new[net].bytes_sent
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - self.network_old[net].bytes_recv
tx = cumulative_tx - self.network_old[net].bytes_sent
cx = rx + tx
netstat = {
'interface_name': net,
'alias': self.has_alias(net),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
# Interface status
'is_up': net_status[net].isup,
# Interface speed in Mbps, convert it to bps
# Can be always 0 on some OSes
'speed': net_status[net].speed * 1048576,
# Set the key for the dict
'key': self.get_key(),
}
except KeyError:
continue
else:
# Append the interface stats to the list
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
elif self.input_method == 'snmp':
# Update stats using SNMP
# SNMP bulk command to get all network interface in one shot
try:
net_io_counters = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
net_io_counters = self.get_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'):
# First call, we init the network_old var
try:
self.network_old = net_io_counters
except (IOError, UnboundLocalError):
pass
else:
# See description in the 'local' block
time_since_update = getTimeSinceLastUpdate('net')
# Loop over interfaces
network_new = net_io_counters
for net in network_new:
# Do not take hidden interface into account
if not self.is_display(net):
continue
try:
# 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.short_system_name == 'windows':
try:
interface_name = str(base64.b16decode(net[2:-2].upper()))
except TypeError:
interface_name = net
else:
interface_name = net
cumulative_rx = float(network_new[net]['cumulative_rx'])
cumulative_tx = float(network_new[net]['cumulative_tx'])
cumulative_cx = cumulative_rx + cumulative_tx
rx = cumulative_rx - float(self.network_old[net]['cumulative_rx'])
tx = cumulative_tx - float(self.network_old[net]['cumulative_tx'])
cx = rx + tx
netstat = {
'interface_name': interface_name,
'alias': self.has_alias(interface_name),
'time_since_update': time_since_update,
'cumulative_rx': cumulative_rx,
'rx': rx,
'cumulative_tx': cumulative_tx,
'tx': tx,
'cumulative_cx': cumulative_cx,
'cx': cx,
}
except KeyError:
continue
else:
netstat['key'] = self.get_key()
stats.append(netstat)
# Save stats to compute next bitrate
self.network_old = network_new
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Check if the stats should be hidden
self.update_views_hidden()
# Add specifics information
# Alert
for i in self.get_raw():
if i['time_since_update'] == 0:
# Skip alert if no timespan to measure
continue
if_real_name = i['interface_name'].split(':')[0]
# Convert rate in bps (to be able to compare to interface speed)
bps_rx = int(i['rx'] // i['time_since_update'] * 8)
bps_tx = int(i['tx'] // i['time_since_update'] * 8)
# Decorate the bitrate with the configuration file thresholds
alert_rx = self.get_alert(bps_rx, header=if_real_name + '_rx')
alert_tx = self.get_alert(bps_tx, header=if_real_name + '_tx')
# If nothing is define in the configuration file...
# ... then use the interface speed (not available on all systems)
if alert_rx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_rx = self.get_alert(current=bps_rx, maximum=i['speed'], header='rx')
if alert_tx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
alert_tx = self.get_alert(current=bps_tx, maximum=i['speed'], header='tx')
# then decorates
self.views[i[self.get_key()]]['rx']['decoration'] = alert_rx
self.views[i[self.get_key()]]['tx']['decoration'] = alert_tx
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Max size for the interface name
name_max_width = max_width - 12
# Header
msg = '{:{width}}'.format('NETWORK', width=name_max_width)
ret.append(self.curse_add_line(msg, "TITLE"))
if args.network_cumul:
# Cumulative stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx')
ret.append(self.curse_add_line(msg))
else:
# Rx/Tx stats
msg = '{:>7}'.format('Rx')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx')
ret.append(self.curse_add_line(msg))
else:
# Bitrate stats
if args.network_sum:
# Sum stats
msg = '{:>14}'.format('Rx+Tx/s')
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format('Rx/s')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('Tx/s')
ret.append(self.curse_add_line(msg))
# Interface list (sorted by name)
for i in self.sorted_stats():
# Do not display interface in down state (issue #765)
if ('is_up' in i) and (i['is_up'] is False):
continue
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields]):
continue
# Format stats
# Is there an alias for the interface name ?
if i['alias'] is None:
if_name = i['interface_name'].split(':')[0]
else:
if_name = i['alias']
if len(if_name) > name_max_width:
# Cut interface name if it is too long
if_name = '_' + if_name[-name_max_width + 1 :]
if args.byte:
# Bytes per second (for dummy)
to_bit = 1
unit = ''
else:
# Bits per second (for real network administrator | Default)
to_bit = 8
unit = 'b'
if args.network_cumul:
rx = self.auto_unit(int(i['cumulative_rx'] * to_bit)) + unit
tx = self.auto_unit(int(i['cumulative_tx'] * to_bit)) + unit
sx = self.auto_unit(int(i['cumulative_rx'] * to_bit) + int(i['cumulative_tx'] * to_bit)) + unit
else:
rx = self.auto_unit(int(i['rx'] // i['time_since_update'] * to_bit)) + unit
tx = self.auto_unit(int(i['tx'] // i['time_since_update'] * to_bit)) + unit
sx = (
self.auto_unit(
int(i['rx'] // i['time_since_update'] * to_bit)
+ int(i['tx'] // i['time_since_update'] * to_bit)
)
+ unit
)
# New line
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(if_name, width=name_max_width)
ret.append(self.curse_add_line(msg))
if args.network_sum:
msg = '{:>14}'.format(sx)
ret.append(self.curse_add_line(msg))
else:
msg = '{:>7}'.format(rx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='rx', option='decoration'))
)
msg = '{:>7}'.format(tx)
ret.append(
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='tx', option='decoration'))
)
return ret

View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Now (current date) plugin."""
from time import tzname, strftime
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Plugin to get the current date/time.
stats is (string)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
if args.strftime_format:
self.strftime = args.strftime_format
elif config is not None:
if 'global' in config.as_dict():
self.strftime = config.as_dict()['global']['strftime_format']
def reset(self):
"""Reset/init the stats."""
self.stats = ''
def update(self):
"""Update current date/time."""
# Had to convert it to string because datetime is not JSON serializable
# Add the time zone (issue #1249 / #1337 / #1598)
if self.strftime:
self.stats = strftime(self.strftime)
else:
if len(tzname[1]) > 6:
self.stats = strftime('%Y-%m-%d %H:%M:%S %z')
else:
self.stats = strftime('%Y-%m-%d %H:%M:%S %Z')
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.is_disabled():
return ret
# Build the string message
# 23 is the padding for the process list
msg = '{:23}'.format(self.stats)
ret.append(self.curse_add_line(msg))
return ret

View File

@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Now (current date) plugin."""
from time import tzname, strftime
from glances.plugins.plugin.model import GlancesPluginModel
class PluginModel(GlancesPluginModel):
"""Plugin to get the current date/time.
stats is (string)
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config)
# We want to display the stat in the curse interface
self.display_curse = True
# Set the message position
self.align = 'bottom'
if args.strftime_format:
self.strftime = args.strftime_format
elif config is not None:
if 'global' in config.as_dict():
self.strftime = config.as_dict()['global']['strftime_format']
def reset(self):
"""Reset/init the stats."""
self.stats = ''
def update(self):
"""Update current date/time."""
# Had to convert it to string because datetime is not JSON serializable
# Add the time zone (issue #1249 / #1337 / #1598)
if self.strftime:
self.stats = strftime(self.strftime)
else:
if len(tzname[1]) > 6:
self.stats = strftime('%Y-%m-%d %H:%M:%S %z')
else:
self.stats = strftime('%Y-%m-%d %H:%M:%S %Z')
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the string to display in the curse interface."""
# Init the return message
ret = []
if not self.stats or self.is_disabled():
return ret
# Build the string message
# 23 is the padding for the process list
msg = '{:23}'.format(self.stats)
ret.append(self.curse_add_line(msg))
return ret

View File

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Per-CPU plugin."""
from glances.cpu_percent import cpu_percent
from glances.plugins.plugin.model import GlancesPluginModel
# Define the history items list
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances per-CPU plugin.
'stats' is a list of dictionaries that contain the utilization percentages
for each CPU.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'cpu_number'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update per-CPU stats using the input method."""
# Init new stats
stats = self.get_init_value()
# Grab per-CPU stats using psutil's cpu_percent(percpu=True) and
# cpu_times_percent(percpu=True) methods
if self.input_method == 'local':
stats = cpu_percent.get(percpu=True)
else:
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or not self.args.percpu or self.is_disabled():
return ret
# Build the string message
if self.is_disabled('quicklook'):
msg = '{:7}'.format('PER CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
# Per CPU stats displayed per line
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
msg = '{:>7}'.format(stat)
ret.append(self.curse_add_line(msg))
# Per CPU stats displayed per column
for cpu in self.stats:
ret.append(self.curse_new_line())
if self.is_disabled('quicklook'):
try:
msg = '{:6.1f}%'.format(cpu['total'])
except TypeError:
# TypeError: string indices must be integers (issue #1027)
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg))
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
try:
msg = '{:6.1f}%'.format(cpu[stat])
except TypeError:
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg, self.get_alert(cpu[stat], header=stat)))
return ret

View File

@ -1,102 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Per-CPU plugin."""
from glances.cpu_percent import cpu_percent
from glances.plugins.plugin.model import GlancesPluginModel
# Define the history items list
items_history_list = [
{'name': 'user', 'description': 'User CPU usage', 'y_unit': '%'},
{'name': 'system', 'description': 'System CPU usage', 'y_unit': '%'},
]
class PluginModel(GlancesPluginModel):
"""Glances per-CPU plugin.
'stats' is a list of dictionaries that contain the utilization percentages
for each CPU.
"""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(
args=args, config=config, items_history_list=items_history_list, stats_init_value=[]
)
# We want to display the stat in the curse interface
self.display_curse = True
def get_key(self):
"""Return the key of the list."""
return 'cpu_number'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update per-CPU stats using the input method."""
# Init new stats
stats = self.get_init_value()
# Grab per-CPU stats using psutil's cpu_percent(percpu=True) and
# cpu_times_percent(percpu=True) methods
if self.input_method == 'local':
stats = cpu_percent.get(percpu=True)
else:
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist...
if not self.stats or not self.args.percpu or self.is_disabled():
return ret
# Build the string message
if self.is_disabled('quicklook'):
msg = '{:7}'.format('PER CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
# Per CPU stats displayed per line
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
msg = '{:>7}'.format(stat)
ret.append(self.curse_add_line(msg))
# Per CPU stats displayed per column
for cpu in self.stats:
ret.append(self.curse_new_line())
if self.is_disabled('quicklook'):
try:
msg = '{:6.1f}%'.format(cpu['total'])
except TypeError:
# TypeError: string indices must be integers (issue #1027)
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg))
for stat in ['user', 'system', 'idle', 'iowait', 'steal']:
if stat not in self.stats[0]:
continue
try:
msg = '{:6.1f}%'.format(cpu[stat])
except TypeError:
msg = '{:>6}%'.format('?')
ret.append(self.curse_add_line(msg, self.get_alert(cpu[stat], header=stat)))
return ret

View File

@ -16,7 +16,7 @@ I am your father...
import re
import copy
from glances.globals import iterkeys, itervalues, listkeys, mean, nativestr, json_dumps, json_dumps_dictlist
from glances.globals import iterkeys, itervalues, listkeys, mean, nativestr, json_dumps, json_dumps_dictlist, dictlist
from glances.actions import GlancesActions
from glances.history import GlancesHistory
from glances.logger import logger
@ -70,7 +70,10 @@ class GlancesPluginModel(object):
:stats_init_value: Default value for a stats item
"""
# Build the plugin name
self.plugin_name = self.__class__.__module__.split('.')[2]
# Internal or external module (former prefixed by 'glances.plugins')
_mod = self.__class__.__module__.replace('glances.plugins.', '')
self.plugin_name = _mod.split('.')[0]
if self.plugin_name.startswith('glances_'):
self.plugin_name = self.plugin_name.split('glances_')[1]
logger.debug("Init {} plugin".format(self.plugin_name))
@ -95,6 +98,9 @@ class GlancesPluginModel(object):
logger.debug('Load section {} in {}'.format(self.plugin_name, config.config_file_paths()))
self.load_limits(config=config)
# Init the alias (dictionnary)
self.alias = self.read_alias()
# Init the actions
self.actions = GlancesActions(args=args)
@ -392,6 +398,13 @@ class GlancesPluginModel(object):
"""Return the stats object in JSON format."""
return self.get_stats()
def get_raw_stats_item(self, item):
"""Return the stats object for a specific item in RAW format.
Stats should be a list of dict (processlist, network...)
"""
return dictlist(self.stats, item)
def get_stats_item(self, item):
"""Return the stats object for a specific item in JSON format.
@ -399,8 +412,8 @@ class GlancesPluginModel(object):
"""
return json_dumps_dictlist(self.stats, item)
def get_stats_value(self, item, value):
"""Return the stats object for a specific item=value in JSON format.
def get_raw_stats_value(self, item, value):
"""Return the stats object for a specific item=value.
Stats should be a list of dict (processlist, network...)
"""
@ -410,11 +423,22 @@ class GlancesPluginModel(object):
if not isinstance(value, int) and value.isdigit():
value = int(value)
try:
return json_dumps({value: [i for i in self.stats if i[item] == value]})
return {value: [i for i in self.stats if i[item] == value]}
except (KeyError, ValueError) as e:
logger.error("Cannot get item({})=value({}) ({})".format(item, value, 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...)
"""
rsv = self.get_raw_stats_value(item, value)
if rsv is None:
return None
else:
return json_dumps(rsv)
def update_views_hidden(self):
"""Update the hidden views
@ -612,7 +636,7 @@ class GlancesPluginModel(object):
return self.stats
def get_stat_name(self, header=""):
""" "Return the stat name with an optional header"""
"""Return the stat name with an optional header"""
ret = self.plugin_name
if header != "":
ret += '_' + header
@ -686,7 +710,7 @@ class GlancesPluginModel(object):
# Add _LOG to the return string
# So stats will be highlighted with a specific color
log_str = "_LOG"
# Add the log to the list
# Add the log to the events list
glances_events.add(ret, stat_name.upper(), value)
# Manage threshold
@ -825,7 +849,7 @@ class GlancesPluginModel(object):
If the show value is empty, return True (show by default)
The show configuration list is defined in the glances.conf file.
It is a comma separated list of regexp.
It is a comma-separated list of regexp.
Example for diskio:
show=sda.*
"""
@ -838,7 +862,7 @@ class GlancesPluginModel(object):
"""Return True if the value is in the hide configuration list.
The hide configuration list is defined in the glances.conf file.
It is a comma separated list of regexp.
It is a comma-separated list of regexp.
Example for diskio:
hide=sda2,sda5,loop.*
"""
@ -854,14 +878,15 @@ class GlancesPluginModel(object):
else:
return not self.is_hide(value, header=header)
def read_alias(self):
if self.plugin_name + '_' + 'alias' in self._limits:
return {i.split(':')[0]: i.split(':')[1] for i in self._limits[self.plugin_name + '_' + 'alias'][0].split(',')}
else:
return dict()
def has_alias(self, header):
"""Return the alias name for the relative header it it exists otherwise None."""
try:
# Force to lower case (issue #1126)
return self._limits[self.plugin_name + '_' + header.lower() + '_' + 'alias'][0]
except (KeyError, IndexError):
# logger.debug("No alias found for {}".format(header))
return None
return self.alias.get(header, None)
def msg_curse(self, args=None, max_width=None):
"""Return default string to display in the curse interface."""

View File

@ -0,0 +1,352 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""Ports scanner plugin."""
import os
import subprocess
import threading
import socket
import time
import numbers
from glances.globals import WINDOWS, MACOS, BSD, bool_type
from glances.ports_list import GlancesPortsList
from glances.web_list import GlancesWebList
from glances.timer import Counter
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
try:
import requests
requests_tag = True
except ImportError as e:
requests_tag = False
logger.warning("Missing Python Lib ({}), Ports plugin is limited to port scanning".format(e))
class PluginModel(GlancesPluginModel):
"""Glances ports scanner plugin."""
def __init__(self, args=None, config=None):
"""Init the plugin."""
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init stats
self.stats = (
GlancesPortsList(config=config, args=args).get_ports_list()
+ GlancesWebList(config=config, args=args).get_web_list()
)
# Global Thread running all the scans
self._thread = None
def exit(self):
"""Overwrite the exit method to close threads."""
if self._thread is not None:
self._thread.stop()
# Call the father class
super(PluginModel, self).exit()
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the ports list."""
if self.input_method == 'local':
# Only refresh:
# * if there is not other scanning thread
# * every refresh seconds (define in the configuration file)
if self._thread is None:
thread_is_running = False
else:
thread_is_running = self._thread.is_alive()
if not thread_is_running:
# Run ports scanner
self._thread = ThreadScanner(self.stats)
self._thread.start()
else:
# Not available in SNMP mode
pass
return self.stats
def get_key(self):
"""Return the key of the list."""
return 'indice'
def get_ports_alert(self, port, header="", log=False):
"""Return the alert status relative to the port scan return value."""
ret = 'OK'
if port['status'] is None:
ret = 'CAREFUL'
elif port['status'] == 0:
ret = 'CRITICAL'
elif (
isinstance(port['status'], (float, int))
and port['rtt_warning'] is not None
and port['status'] > port['rtt_warning']
):
ret = 'WARNING'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, port[self.get_key()])
return ret
def get_web_alert(self, web, header="", log=False):
"""Return the alert status relative to the web/url scan return value."""
ret = 'OK'
if web['status'] is None:
ret = 'CAREFUL'
elif web['status'] not in [200, 301, 302]:
ret = 'CRITICAL'
elif web['rtt_warning'] is not None and web['elapsed'] > web['rtt_warning']:
ret = 'WARNING'
# Get stat name
stat_name = self.get_stat_name(header=header)
# Manage threshold
self.manage_threshold(stat_name, ret)
# Manage action
self.manage_action(stat_name, ret.lower(), header, web[self.get_key()])
return ret
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_ports:
return ret
# Max size for the interface name
name_max_width = max_width - 7
# Build the string message
for p in self.stats:
if 'host' in p:
if p['host'] is None:
status = 'None'
elif p['status'] is None:
status = 'Scanning'
elif isinstance(p['status'], bool_type) and p['status'] is True:
status = 'Open'
elif p['status'] == 0:
status = 'Timeout'
else:
# Convert second to ms
status = '{0:.0f}ms'.format(p['status'] * 1000.0)
msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
msg = '{:>9}'.format(status)
ret.append(self.curse_add_line(msg, self.get_ports_alert(p, header=p['indice'] + '_rtt')))
ret.append(self.curse_new_line())
elif 'url' in p:
msg = '{:{width}}'.format(p['description'][0:name_max_width], width=name_max_width)
ret.append(self.curse_add_line(msg))
if isinstance(p['status'], numbers.Number):
status = 'Code {}'.format(p['status'])
elif p['status'] is None:
status = 'Scanning'
else:
status = p['status']
msg = '{:>9}'.format(status)
ret.append(self.curse_add_line(msg, self.get_web_alert(p, header=p['indice'] + '_rtt')))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret
class ThreadScanner(threading.Thread):
"""
Specific thread for the port/web scanner.
stats is a list of dict
"""
def __init__(self, stats):
"""Init the class."""
logger.debug("ports plugin - Create thread for scan list {}".format(stats))
super(ThreadScanner, self).__init__()
# Event needed to stop properly the thread
self._stopper = threading.Event()
# The class return the stats as a list of dict
self._stats = stats
# Is part of Ports plugin
self.plugin_name = "ports"
def run(self):
"""Grab the stats.
Infinite loop, should be stopped by calling the stop() method.
"""
for p in self._stats:
# End of the thread has been asked
if self.stopped():
break
# Scan a port (ICMP or TCP)
if 'port' in p:
self._port_scan(p)
# Had to wait between two scans
# If not, result are not ok
time.sleep(1)
# Scan an URL
elif 'url' in p and requests_tag:
self._web_scan(p)
@property
def stats(self):
"""Stats getter."""
return self._stats
@stats.setter
def stats(self, value):
"""Stats setter."""
self._stats = value
def stop(self, timeout=None):
"""Stop the thread."""
logger.debug("ports plugin - Close thread for scan list {}".format(self._stats))
self._stopper.set()
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.is_set()
def _web_scan(self, web):
"""Scan the Web/URL (dict) and update the status key."""
try:
req = requests.head(
web['url'],
allow_redirects=True,
verify=web['ssl_verify'],
proxies=web['proxies'],
timeout=web['timeout'],
)
except Exception as e:
logger.debug(e)
web['status'] = 'Error'
web['elapsed'] = 0
else:
web['status'] = req.status_code
web['elapsed'] = req.elapsed.total_seconds()
return web
def _port_scan(self, port):
"""Scan the port structure (dict) and update the status key."""
if int(port['port']) == 0:
return self._port_scan_icmp(port)
else:
return self._port_scan_tcp(port)
def _resolv_name(self, hostname):
"""Convert hostname to IP address."""
ip = hostname
try:
ip = socket.gethostbyname(hostname)
except Exception as e:
logger.debug("{}: Cannot convert {} to IP address ({})".format(self.plugin_name, hostname, e))
return ip
def _port_scan_icmp(self, port):
"""Scan the (ICMP) port structure (dict) and update the status key."""
ret = None
# Create the ping command
# Use the system ping command because it already have the sticky bit set
# Python can not create ICMP packet with non root right
if WINDOWS:
timeout_opt = '-w'
count_opt = '-n'
elif MACOS or BSD:
timeout_opt = '-t'
count_opt = '-c'
else:
# Linux and co...
timeout_opt = '-W'
count_opt = '-c'
# Build the command line
# Note: Only string are allowed
cmd = [
'ping',
count_opt,
'1',
timeout_opt,
str(self._resolv_name(port['timeout'])),
self._resolv_name(port['host']),
]
fnull = open(os.devnull, 'w')
try:
counter = Counter()
ret = subprocess.check_call(cmd, stdout=fnull, stderr=fnull, close_fds=True)
if ret == 0:
port['status'] = counter.get()
else:
port['status'] = False
except subprocess.CalledProcessError:
# Correct issue #1084: No Offline status for timed-out ports
port['status'] = False
except Exception as e:
logger.debug("{}: Error while pinging host {} ({})".format(self.plugin_name, port['host'], e))
fnull.close()
return ret
def _port_scan_tcp(self, port):
"""Scan the (TCP) port structure (dict) and update the status key."""
ret = None
# Create and configure the scanning socket
try:
socket.setdefaulttimeout(port['timeout'])
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except Exception as e:
logger.debug("{}: Error while creating scanning socket ({})".format(self.plugin_name, e))
# Scan port
ip = self._resolv_name(port['host'])
counter = Counter()
try:
ret = _socket.connect_ex((ip, int(port['port'])))
except Exception as e:
logger.debug("{}: Error while scanning port {} ({})".format(self.plugin_name, port, e))
else:
if ret == 0:
port['status'] = counter.get()
else:
port['status'] = False
finally:
_socket.close()
return ret

Some files were not shown because too many files have changed in this diff Show More