mirror of https://github.com/nicolargo/glances.git
Merge branch 'develop' into issue2183
This commit is contained in:
commit
70250981b9
|
|
@ -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 }}
|
||||
|
|
|
|||
11
Makefile
11
Makefile
|
|
@ -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
|
||||
# ===================================================================
|
||||
|
|
|
|||
13
NEWS.rst
13
NEWS.rst
|
|
@ -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
|
||||
|
|
|
|||
10
README.rst
10
README.rst
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
617
docs/api.rst
617
docs/api.rst
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 program’s version number and exit
|
||||
show the program’s 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
|
||||
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
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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):
|
||||
|
|
@ -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()))
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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('')
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ static
|
|||
|
|
||||
|--- public # path where builds are put
|
||||
|
|
||||
|--- templates (bottle)
|
||||
|--- templates
|
||||
```
|
||||
|
||||
## Data
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = '?';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue