Compute rate automaticaly

This commit is contained in:
nicolargo 2024-02-03 17:38:17 +01:00
commit bd5e297a0e
160 changed files with 11560 additions and 9594 deletions

View File

@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install pip install build tools
run: >-
@ -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 }}
@ -101,11 +101,11 @@ jobs:
tag: ${{ fromJson(needs.create_Docker_builds.outputs.tags) }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Retrieve Repository Docker metadata
id: docker_meta
uses: crazy-max/ghaction-docker-meta@v4.6.0
uses: crazy-max/ghaction-docker-meta@v5.0.0
with:
images: ${{ env.DEFAULT_DOCKER_IMAGE }}
labels: |
@ -119,25 +119,25 @@ jobs:
restore-keys: ${{ runner.os }}-buildx-${{ env.NODE_ENV }}-${{ matrix.os }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
if: ${{ env.PUSH_BRANCH == 'true' }}
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
push: ${{ env.PUSH_BRANCH == 'true' }}
tags: "${{ env.DEFAULT_DOCKER_IMAGE }}:${{ matrix.os != 'alpine' && format('{0}-', matrix.os) || '' }}${{ matrix.tag.tag }}"

View File

@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
@ -64,11 +64,12 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
# Python version "3.12" introduce this issue:
# https://github.com/nicolargo/glances/actions/runs/6439648370/job/17487567454
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4

View File

@ -80,8 +80,11 @@ test-min: ## Run unit tests in minimal environment
test-min-with-upgrade: venv-min-upgrade ## Upgrade deps and run unit tests in minimal environment
./venv-min/bin/python ./unitest.py
test-restful-api: ## Run unit tests of the RESTful API
./venv/bin/python ./unitest-restful.py
# ===================================================================
# Linters and profilers
# Linters, profilers and cyber security
# ===================================================================
format: ## Format the code
@ -99,7 +102,7 @@ codespell: ## Run codespell to fix common misspellings in text files
./venv-dev/bin/codespell -S .git,./docs/_build,./Glances.egg-info,./venv*,./glances/outputs,*.svg -L hart,bu,te,statics
semgrep: ## Run semgrep to find bugs and enforce code standards
./venv-dev/bin/semgrep --config=auto --lang python --use-git-ignore ./glances
./venv-dev/bin/semgrep scan --config=auto
profiling: ## How to start the profiling of the Glances software
@echo "Please complete and run: sudo ./venv-dev/bin/py-spy record -o ./docs/_static/glances-flame.svg -d 60 -s --pid <GLANCES PID>"
@ -123,6 +126,10 @@ memory-profiling: ## Profile memory usage
./venv-dev/bin/mprof plot --output ./docs/_static/glances-memory-profiling-without-history.png
rm -f mprofile_*.dat
# Trivy installation: https://aquasecurity.github.io/trivy/latest/getting-started/installation/
trivy: ## Run Trivy to find vulnerabilities in container images
trivy fs .
# ===================================================================
# Docs
# ===================================================================
@ -167,6 +174,12 @@ flatpak: venv-dev-upgrade ## Generate FlatPack JSON file
rm -rf ./flatpak-builder-tools
@echo "Now follow: https://github.com/flathub/flathub/wiki/App-Submission"
# Snap package is automaticaly build on the Snapcraft.io platform
# https://snapcraft.io/glances
# But you can try an offline build with the following command
snapcraft:
snapcraft
# ===================================================================
# Docker
# ===================================================================

View File

@ -8,6 +8,22 @@ 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
* Alert data model change from a list of list to a list of dict #2633
===============
Version 3.4.0.3
===============
@ -899,7 +915,7 @@ Processes list Nice value:
[processlist]
# Nice priorities range from -20 to 19.
# Configure nice levels using a comma separated list.
# Configure nice levels using a comma-separated list.
#
# Nice: Example 1, non-zero is warning (default behavior)
nice_warning=-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19

View File

@ -92,19 +92,21 @@ 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)
- ``couchdb`` (for the CouchDB export module)
- ``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)
- ``podman`` (for the Containers Podman monitoring support)
- ``potsdb`` (for the OpenTSDB export module)
@ -140,8 +142,8 @@ To install Glances, simply use ``pip``:
pip install --user glances
*Note*: Python headers are required to install `psutil`_, a Glances
dependency. For example, on Debian/Ubuntu you need to install first
the *python-dev* package (*python-devel* on Fedora/CentOS/RHEL).
dependency. For example, on Debian/Ubuntu **the simplest** is ``apt install python3-psutil`` or alternatively need to install first
the *python-dev* package and gcc (*python-devel* on Fedora/CentOS/RHEL).
For Windows, just install psutil from the binary installation file.
*Note 2 (for the Wifi plugin)*: If you want to use the Wifi plugin, you need
@ -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*:
@ -219,14 +221,16 @@ Run last version of Glances container in *console mode*:
docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro -v /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro --pid host --network host -it nicolargo/glances:latest-full
By default, the /etc/glances/glances.conf file is used (based on docker-compose/glances.conf).
Additionally, if you want to use your own glances.conf file, you can
create your own Dockerfile:
.. code-block:: console
FROM nicolargo/glances:latest
COPY glances.conf /etc/glances.conf
CMD python -m glances -C /etc/glances.conf $GLANCES_OPT
COPY glances.conf /root/.config/glances/glances.conf
CMD python -m glances -C /root/.config/glances/glances.conf $GLANCES_OPT
Alternatively, you can specify something along the same lines with
docker run options (notice the `GLANCES_OPT` environment
@ -234,7 +238,7 @@ variable setting parameters for the glances startup command):
.. code-block:: console
docker run -e TZ="${TZ}" -v `pwd`/glances.conf:/etc/glances.conf -v /var/run/docker.sock:/var/run/docker.sock:ro -v /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro --pid host -e GLANCES_OPT="-C /etc/glances.conf" -it nicolargo/glances:latest-full
docker run -e TZ="${TZ}" -v `pwd`/glances.conf:/root/.config/glances/glances.conf -v /var/run/docker.sock:/var/run/docker.sock:ro -v /run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro --pid host -e GLANCES_OPT="-C /root/.config/glances/glances.conf" -it nicolargo/glances:latest-full
Where \`pwd\`/glances.conf is a local directory containing your glances.conf file.
@ -252,7 +256,7 @@ GNU/Linux
`Glances` is available on many Linux distributions, so you should be
able to install it using your favorite package manager. Be aware that
when you use this method the operating system `package`_ for `Glances`
may not be the latest version.
may not be the latest version and only basics plugins are enabled.
Note: The Debian package (and all other Debian-based distributions) do
not include anymore the JS statics files used by the Web interface
@ -319,7 +323,7 @@ Start Termux on your device and enter:
$ apt update
$ apt upgrade
$ apt install clang python
$ pip install bottle
$ pip install fastapi uvicorn orjson jinja2
$ pip install glances
And start Glances:

View File

@ -14,16 +14,23 @@ check_update=true
history_size=1200
# Set the way Glances should display the date (default is %Y-%m-%d %H:%M:%S %Z)
#strftime_format="%Y-%m-%d %H:%M:%S %Z"
# Define external directory for loading additional plugins
# The layout follows the glances standard for plugin definitions
#plugin_dir=/home/user/dev/plugins
##############################################################################
# User interface
##############################################################################
[outputs]
# Theme name for the Curses interface: black or white
# Theme name (for the moment only for the Curses interface: black or white)
curse_theme=black
# Separator in the Curses and WebUI interface (between top and others plugins)
separator=True
# Set the the Curses and WebUI interface left menu plugin list (comma-separated)
#left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now
# 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
@ -38,8 +45,11 @@ max_processes_display=30
# Set to true to disable a plugin
# Note: you can also disable it from the command line (see --disable-plugin <plugin_name>)
disable=False
# Graphical percentage char used in the terminal user interface (default is |)
percentage_char=|
# Stats list (default is cpu,mem,load)
# Available stats are: cpu,mem,load,swap
list=cpu,mem,load
# Graphical bar char used in the terminal user interface (default is |)
bar_char=|
# Define CPU, MEM and SWAP thresholds in %
cpu_careful=50
cpu_warning=70
@ -50,6 +60,11 @@ mem_critical=90
swap_careful=50
swap_warning=70
swap_critical=90
# Source: http://blog.scoutapp.com/articles/2009/07/31/understanding-load-averages
# With 1 CPU core, the load should be lower than 1.00 ~ 100%
load_careful=70
load_warning=100
load_critical=500
[system]
# This plugin display the first line in the Glances UI with:
@ -167,8 +182,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 +192,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 +233,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 +251,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 +324,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 +347,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,37 +405,42 @@ 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)
; show=telegraf
# Hide some containers (comma-separated list of container name or regular expression)
# Comment this line to display all containers (default configuration)
#hide=telegraf
; hide=telegraf
# Define the maximum docker size name (default is 20 chars)
max_name_size=20
#cpu_careful=50
; cpu_careful=50
# Thresholds for CPU and MEM (in %)
#cpu_warning=70
#cpu_critical=90
#mem_careful=20
#mem_warning=50
#mem_critical=70
; cpu_warning=70
; cpu_critical=90
; mem_careful=20
; mem_warning=50
; mem_critical=70
#
# Per container thresholds
#containername_cpu_careful=10
#containername_cpu_warning=20
#containername_cpu_critical=30
; containername_cpu_careful=10
; containername_cpu_warning=20
; containername_cpu_critical=30
#
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=False
# Define Podman sock
#podman_sock=unix:///run/user/1000/podman/podman.sock
; podman_sock=unix:///run/user/1000/podman/podman.sock
[amps]
# AMPs configuration are defined in the bottom of this file
disable=False
[alert]
disable=False
# Maximum number of alerts to display (default is 10)
;max_events=10
##############################################################################
# Client/server
##############################################################################
@ -466,7 +482,7 @@ path=/tmp
# It is possible to generate the graphs automatically by setting the
# generate_every to a non zero value corresponding to the seconds between
# two generation. Set it to 0 to disable graph auto generation.
generate_every=60
generate_every=0
# See following configuration keys definitions in the Pygal lib documentation
# http://pygal.org/en/stable/documentation/index.html
width=800
@ -590,10 +606,8 @@ topic_structure=per-metric
host=localhost
port=5984
db=glances
# user and password are optional (comment if not configured on the server side)
# If they are used, then the https protocol will be used
#user=root
#password=root
user=admin
password=admin
[mongodb]
# Configuration for the --export mongodb option

View File

@ -11,4 +11,6 @@ memory-profiler
matplotlib
semgrep
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
numpy>=1.22.2 # not directly required, pinned by Snyk to avoid a vulnerability
numpy>=1.22.2 # not directly required, pinned by Snyk to avoid a vulnerability
pillow>=10.0.1 # not directly required, pinned by Snyk to avoid a vulnerability
fonttools>=4.43.0 # not directly required, pinned by Snyk to avoid a vulnerability

View File

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

View File

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

View File

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

View File

@ -22,8 +22,12 @@ history_size=1200
[outputs]
# Theme name for the Curses interface: black or white
curse_theme=black
# Limit the number of processes to display in the WebUI
max_processes_display=30
# Separator in the Curses and WebUI interface (between top and others plugins)
separator=True
# Set the the Curses and WebUI interface left menu plugin list (comma-separated)
#left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now
# Limit the number of processes to display (for the WebUI)
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
@ -93,6 +97,7 @@ steal_warning=70
steal_critical=90
#steal_log=True
#
# Context switch limit (core / second)
# Leave commented to just use the default config (critical is 50000*# (Logical CPU cores)
#ctx_switches_careful=10000
@ -340,7 +345,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,37 +403,42 @@ 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)
; show=telegraf
# Hide some containers (comma-separated list of container name or regular expression)
# Comment this line to display all containers (default configuration)
#hide=telegraf
; hide=telegraf
# Define the maximum docker size name (default is 20 chars)
max_name_size=20
#cpu_careful=50
; cpu_careful=50
# Thresholds for CPU and MEM (in %)
#cpu_warning=70
#cpu_critical=90
#mem_careful=20
#mem_warning=50
#mem_critical=70
; cpu_warning=70
; cpu_critical=90
; mem_careful=20
; mem_warning=50
; mem_critical=70
#
# Per container thresholds
#containername_cpu_careful=10
#containername_cpu_warning=20
#containername_cpu_critical=30
; containername_cpu_careful=10
; containername_cpu_warning=20
; containername_cpu_critical=30
#
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=False
# Define Podman sock
#podman_sock=unix:///run/user/1000/podman/podman.sock
; podman_sock=unix:///run/user/1000/podman/podman.sock
[amps]
# AMPs configuration are defined in the bottom of this file
disable=False
[alert]
disable=False
# Maximum number of alerts to display (default is 10)
; max_events=10
##############################################################################
# Client/server
##############################################################################
@ -470,7 +480,7 @@ path=/tmp
# It is possible to generate the graphs automatically by setting the
# generate_every to a non zero value corresponding to the seconds between
# two generation. Set it to 0 to disable graph auto generation.
generate_every=60
generate_every=0
# See following configuration keys definitions in the Pygal lib documentation
# http://pygal.org/en/stable/documentation/index.html
width=800

View File

@ -4,11 +4,13 @@
# https://github.com/nicolargo/glances
#
# WARNING: the versions should be set.
# Ex: Python 3.11 for Alpine 3.18
# Note: ENV is for future running containers. ARG for building your Docker image.
ARG IMAGE_VERSION=3.18.3
# WARNING: the Alpine image version and Python version should be set.
# Alpine 3.18 tag is a link to the latest 3.18.x version.
# Be aware that if you change the Alpine version, you may have to change the Python version.
ARG IMAGE_VERSION=3.19
ARG PYTHON_VERSION=3.11
##############################################################################
@ -83,7 +85,7 @@ RUN python${PYTHON_VERSION} -m pip install --target="/venv/lib/python${PYTHON_VE
FROM base as release
# Copy source code and config file
COPY ./docker-compose/glances.conf /etc/glances.conf
COPY ./docker-compose/glances.conf /etc/glances/glances.conf
COPY /glances /app/glances
# Copy binary and update PATH
@ -96,7 +98,7 @@ EXPOSE 61209 61208
# Define default command.
WORKDIR /app
CMD /venv/bin/python3 -m glances -C /etc/glances.conf $GLANCES_OPT
CMD /venv/bin/python3 -m glances $GLANCES_OPT
################################################################################
# RELEASE: minimal

View File

@ -77,7 +77,7 @@ RUN python${PYTHON_VERSION} -m pip install --target="/venv/lib/python${PYTHON_VE
FROM base as release
# Copy Glances source code and config file
COPY ./docker-compose/glances.conf /etc/glances.conf
COPY ./docker-compose/glances.conf /etc/glances/glances.conf
COPY /glances /app/glances
# Copy binary and update PATH
@ -90,7 +90,7 @@ EXPOSE 61209 61208
# Define default command.
WORKDIR /app
CMD /venv/bin/python3 -m glances -C /etc/glances.conf $GLANCES_OPT
CMD /venv/bin/python3 -m glances $GLANCES_OPT
################################################################################
# RELEASE: minimal

View File

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 145 KiB

View File

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

View File

@ -15,17 +15,6 @@ displayed.
.. image:: ../_static/cpu-wide.png
A character is also displayed just after the CPU header and shows the
trend value:
======== ==============================================================
Trend Status
======== ==============================================================
``-`` CPU value is equal to the mean of the six latests refreshes
``\`` CPU value is lower than the mean of the six latests refreshes
``/`` CPU value is higher than the mean of the six latests refreshes
======== ==============================================================
CPU stats description:
- **user**: percent time spent in user space. User CPU time is the time
@ -46,6 +35,8 @@ CPU stats description:
operations to complete.
- **steal** *(Linux)*: percentage of time a virtual CPU waits for a real
CPU while the hypervisor is servicing another virtual processor.
- **guest** *(Linux)*: percentage of time a virtual CPU spends
servicing another virtual CPU under the control of the Linux kernel.
- **ctx_sw**: 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

View File

@ -31,6 +31,17 @@ percentage.
.. image:: ../_static/loadpercent.png
A character is also displayed just after the LOAD header and shows the
trend value (for the 1 minute load stat):
======== ==============================================================
Trend Status
======== ==============================================================
``-`` Mean 15 lasts values equal mean 15 previous values
```` Mean 15 lasts values is lower mean 15 previous values
```` Mean 15 lasts values is higher mean 15 previous valuess
======== ==============================================================
Legend:
============= ============

View File

@ -47,9 +47,9 @@ trend value:
======== ==============================================================
Trend Status
======== ==============================================================
``-`` MEM value is equal to the mean of the six latests refreshes
``\`` MEM value is lower than the mean of the six latests refreshes
``/`` MEM value is higher than the mean of the six latests refreshes
``-`` Mean 15 lasts values equal mean 15 previous values
```` Mean 15 lasts values is lower mean 15 previous values
```` Mean 15 lasts values is higher mean 15 previous valuess
======== ==============================================================
Alerts are only set for used memory and used swap.

View File

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

View File

@ -4,7 +4,7 @@ Quick Look
==========
The ``quicklook`` plugin is only displayed on wide screen and proposes a
bar view for CPU and memory (virtual and swap).
bar view for cpu, memory, swap and load (this list is configurable).
In the terminal interface, click on ``3`` to enable/disable it.
@ -27,10 +27,14 @@ client/server mode (see issue ).
Limit values can be overwritten in the configuration file under
the ``[quicklook]`` section.
You can also configure the percentage char used in the terminal user interface.
You can also configure the stats list and the bat character used in the
user interface.
.. code-block:: ini
[quicklook]
# Stats list (default is cpu,mem,load)
# Available stats are: cpu,mem,load,swap
list=cpu,mem,load
# Graphical percentage char used in the terminal user interface (default is |)
percentage_char=@
bar_char=|

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -5,7 +5,7 @@ Configuration
No configuration file is mandatory to use Glances.
Furthermore a configuration file is needed to access more settings.
Furthermore, a configuration file is needed to access more settings.
Location
--------
@ -14,25 +14,25 @@ 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/
``*BSD`` ~/.config/glances/, /usr/local/etc/glances/, /usr/share/docs/glances/
``macOS`` ~/Library/Application Support/glances/, /usr/local/etc/glances/, /usr/share/docs/glances/
``macOS`` ~/.config/glances/, ~/Library/Application Support/glances/, /usr/local/etc/glances/, /usr/share/docs/glances/
``Windows`` %APPDATA%\\glances\\glances.conf
==================== =============================================================
- 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,41 @@ 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
# Set the way Glances should display the date (default is %Y-%m-%d %H:%M:%S %Z)
#strftime_format="%Y-%m-%d %H:%M:%S %Z"
# Define external directory for loading additional plugins
# The layout follows the glances standard for plugin definitions
#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:
than a second one concerning the user interface:
.. code-block:: ini
[outputs]
# Theme name (for the moment only for the Curses interface: black or white)
curse_theme=black
# Separator in the Curses and WebUI interface (between top and others plugins)
separator=True
# Set the the Curses and WebUI interface left menu plugin list (comma-separated)
#left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now
# Limit the number of processes to display (for the WebUI)
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
# Default is no prefix (/)
#url_prefix=/glances/
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 +114,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 +144,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 +226,7 @@ and start Glances using the following command line:
LOG_CFG=<path>/glances.json glances
.. note::
Replace ``<path>`` by the folder where your ``glances.json`` file
Replace ``<path>`` with the directory where your ``glances.json`` file
is hosted.
.. _GitHub: https://raw.githubusercontent.com/nicolargo/glances/master/conf/glances.conf

View File

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

View File

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

View File

@ -9,9 +9,9 @@ following:
.. code-block:: ini
[mongodb]
[couchdb]
host=localhost
port=27017
port=5984
db=glances
user=root
password=example
@ -20,24 +20,31 @@ and run Glances with:
.. code-block:: console
$ glances --export mongodb
$ glances --export couchdb
Documents are stored in native the configured database (glances by default)
with one collection per plugin.
Documents are stored in native ``JSON`` format. Glances adds ``"type"``
and ``"time"`` entries:
Example of MongoDB Document for the load stats:
- ``type``: plugin name
- ``time``: timestamp (format: "2016-09-24T16:39:08.524Z")
Example of Couch Document for the load stats:
.. code-block:: json
{
_id: ObjectId('63d78ffee5528e543ce5af3a'),
min1: 1.46337890625,
min5: 1.09619140625,
min15: 1.07275390625,
cpucore: 4,
history_size: 1200,
load_disable: 'False',
load_careful: 0.7,
load_warning: 1,
load_critical: 5
"_id": "36cbbad81453c53ef08804cb2612d5b6",
"_rev": "1-382400899bec5615cabb99aa34df49fb",
"min15": 0.33,
"time": "2016-09-24T16:39:08.524Z",
"min5": 0.4,
"cpucore": 4,
"load_warning": 1,
"min1": 0.5,
"history_size": 28800,
"load_critical": 5,
"type": "load",
"load_careful": 0.7
}
You can view the result using the CouchDB utils URL: http://127.0.0.1:5984/_utils/database.html?glances.

View File

@ -9,42 +9,35 @@ following:
.. code-block:: ini
[couchdb]
[mongodb]
host=localhost
port=
port=27017
db=glances
user=root
password=example
db=glances
and run Glances with:
.. code-block:: console
$ glances --export couchdb
$ glances --export mongodb
Documents are stored in native ``JSON`` format. Glances adds ``"type"``
and ``"time"`` entries:
Documents are stored in native the configured database (glances by default)
with one collection per plugin.
- ``type``: plugin name
- ``time``: timestamp (format: "2016-09-24T16:39:08.524828Z")
Example of Couch Document for the load stats:
Example of MongoDB Document for the load stats:
.. code-block:: json
{
"_id": "36cbbad81453c53ef08804cb2612d5b6",
"_rev": "1-382400899bec5615cabb99aa34df49fb",
"min15": 0.33,
"time": "2016-09-24T16:39:08.524828Z",
"min5": 0.4,
"cpucore": 4,
"load_warning": 1,
"min1": 0.5,
"history_size": 28800,
"load_critical": 5,
"type": "load",
"load_careful": 0.7
_id: ObjectId('63d78ffee5528e543ce5af3a'),
min1: 1.46337890625,
min5: 1.09619140625,
min15: 1.07275390625,
cpucore: 4,
history_size: 1200,
load_disable: 'False',
load_careful: 0.7,
load_warning: 1,
load_critical: 5
}
You can view the result using the CouchDB utils URL: http://127.0.0.1:5984/_utils/database.html?glances.

View File

@ -4,8 +4,8 @@ Prometheus
==========
You can export statistics to a ``Prometheus`` server through an exporter.
When the *--export-prometheus* is used, Glances creates a Prometheus exporter
listening on <host:port> (define in the Glances configuration file).
When the *--export prometheus* is used, Glances creates a Prometheus exporter
listening on <host:port> (defined in the Glances configuration file).
.. code-block:: ini

View File

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

View File

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

View File

@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "GLANCES" "1" "Jul 25, 2023" "4.0.0_beta01" "Glances"
.TH "GLANCES" "1" "Feb 03, 2024" "4.0.0_beta01" "Glances"
.SH NAME
glances \- An eye on your system
.SH SYNOPSIS
@ -35,12 +35,12 @@ glances \- An eye on your system
\fBglances\fP [OPTIONS]
.SH DESCRIPTION
.sp
\fBglances\fP is a cross\-platform curses\-based monitoring tool which aims
to present a maximum of information in a minimum of space, ideally to
fit in a classical 80x24 terminal or higher to have additional
information. It can adapt dynamically the displayed information
depending on the terminal size. It can also work in client/server mode.
Remote monitoring could be done via terminal or web interface.
\fBglances\fP is a cross\-platform curses\-based monitoring tool that aims
to present a maximum of information in a minimum of space, ideally fitting
in a classic 80x24 terminal or larger for more details. It can adapt
dynamically to the displayed information depending on the terminal size.
It can also work in client/server mode.
Remote monitoring can be performed via a terminal or web interface.
.sp
\fBglances\fP is written in Python and uses the \fIpsutil\fP library to get
information from your system.
@ -54,7 +54,7 @@ show this help message and exit
.INDENT 0.0
.TP
.B \-V, \-\-version
show programs version number and exit
show the programs version number and exit
.UNINDENT
.INDENT 0.0
.TP
@ -68,28 +68,33 @@ path to the configuration file
.UNINDENT
.INDENT 0.0
.TP
.B \-P PLUGIN_DIRECTORY, \-\-plugins PLUGIN_DIRECTORY
path to a directory containing additional plugins
.UNINDENT
.INDENT 0.0
.TP
.B \-\-modules\-list
display modules (plugins & exports) list and exit
.UNINDENT
.INDENT 0.0
.TP
.B \-\-disable\-plugin PLUGIN
disable PLUGIN (comma separated list)
disable PLUGIN (comma\-separated list)
.UNINDENT
.INDENT 0.0
.TP
.B \-\-enable\-plugin PLUGIN
enable PLUGIN (comma separated list)
enable PLUGIN (comma\-separated list)
.UNINDENT
.INDENT 0.0
.TP
.B \-\-stdout PLUGINS_STATS
display stats to stdout (comma separated list of plugins/plugins.attribute)
display stats to stdout (comma\-separated list of plugins/plugins.attribute)
.UNINDENT
.INDENT 0.0
.TP
.B \-\-export EXPORT
enable EXPORT module (comma separated list)
enable EXPORT module (comma\-separated list)
.UNINDENT
.INDENT 0.0
.TP
@ -114,7 +119,7 @@ disable the Web UI (only the RESTful API will respond)
.INDENT 0.0
.TP
.B \-\-light, \-\-enable\-light
light mode for Curses UI (disable all but top menu)
light mode for Curses UI (disable all but the top menu)
.UNINDENT
.INDENT 0.0
.TP
@ -144,7 +149,7 @@ disable all but quick look and load
.INDENT 0.0
.TP
.B \-5, \-\-disable\-top
disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
disable top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
.UNINDENT
.INDENT 0.0
.TP
@ -249,7 +254,7 @@ set refresh time in seconds [default: 3 sec]
.INDENT 0.0
.TP
.B \-w, \-\-webserver
run Glances in web server mode (bottle lib needed)
run Glances in web server mode (FastAPI lib needed)
.UNINDENT
.INDENT 0.0
.TP
@ -279,12 +284,12 @@ force short name for processes name
.INDENT 0.0
.TP
.B \-\-hide\-kernel\-threads
hide kernel threads in process list (not available on Windows)
hide kernel threads in the process list (not available on Windows)
.UNINDENT
.INDENT 0.0
.TP
.B \-b, \-\-byte
display network rate in byte per second
display network rate in bytes per second
.UNINDENT
.INDENT 0.0
.TP
@ -309,12 +314,12 @@ display FS free space instead of used
.INDENT 0.0
.TP
.B \-\-theme\-white
optimize display colors for white background
optimize display colors for a white background
.UNINDENT
.INDENT 0.0
.TP
.B \-\-disable\-check\-update
disable online Glances version ckeck
disable online Glances version check
.UNINDENT
.SH INTERACTIVE COMMANDS
.sp
@ -331,7 +336,7 @@ On macOS please use \fBCTRL\-H\fP to delete filter.
.UNINDENT
.UNINDENT
.sp
Filter is a regular expression pattern:
The filter is a regular expression pattern:
.INDENT 7.0
.IP \(bu 2
\fBgnome\fP: matches all processes starting with the \fBgnome\fP
@ -353,7 +358,7 @@ If CPU iowait \fB>60%\fP, sort processes by I/O read and write
.UNINDENT
.TP
.B \fBA\fP
Enable/disable Application Monitoring Process
Enable/disable the Application Monitoring Process
.TP
.B \fBb\fP
Switch between bit/s or Byte/s for network I/O
@ -377,7 +382,7 @@ Enable/disable Docker stats
Enable/disable top extended stats
.TP
.B \fBE\fP
Erase current process filter
Erase the current process filter
.TP
.B \fBf\fP
Show/hide file system and folder monitoring stats
@ -404,7 +409,7 @@ Show/hide IP module
Increase selected process nice level / Lower the priority (need right) \- Only in standalone mode.
.TP
.B \fB\-\fP
Decrease selected process nice level / Higher the priority (need right) \- Only in standalone mode.
Decrease selected process nice level / Higher the priority (need right) \- Only in standalone mode.
.TP
.B \fBk\fP
Kill selected process (need right) \- Only in standalone mode.
@ -455,7 +460,7 @@ Enable/disable spark lines
Sort process by CPU times (TIME+)
.TP
.B \fBT\fP
View network I/O as combination
View network I/O as a combination
.TP
.B \fBu\fP
Sort processes by USER
@ -478,13 +483,13 @@ Show/hide processes stats
.B \fB0\fP
Enable/disable Irix/Solaris mode
.sp
Tasks CPU usage will be divided by the total number of CPUs
The tasks CPU usage will be divided by the total number of CPUs
.TP
.B \fB1\fP
Switch between global CPU and per\-CPU stats
.TP
.B \fB2\fP
Enable/disable left sidebar
Enable/disable the left sidebar
.TP
.B \fB3\fP
Enable/disable the quick look module
@ -493,7 +498,7 @@ Enable/disable the quick look module
Enable/disable all but quick look and load module
.TP
.B \fB5\fP
Enable/disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
Enable/disable the top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
.TP
.B \fB6\fP
Enable/disable mean GPU mode
@ -508,10 +513,10 @@ Switch between process command line or command name
Refresh user interface
.TP
.B \fBLEFT\fP
Navigation left through process sort
Navigation left through the process sort
.TP
.B \fBRIGHT\fP
Navigation right through process sort
Navigation right through the process sort
.TP
.B \fBUP\fP
Up in the processes list
@ -540,7 +545,7 @@ Quit Glances
.sp
No configuration file is mandatory to use Glances.
.sp
Furthermore a configuration file is needed to access more settings.
Furthermore, a configuration file is needed to access more settings.
.SH LOCATION
.sp
\fBNOTE:\fP
@ -551,7 +556,7 @@ A template is available in the \fB/usr{,/local}/share/doc/glances\fP
.UNINDENT
.UNINDENT
.sp
You can put your own \fBglances.conf\fP file in the following locations:
You can place your \fBglances.conf\fP file in the following locations:
.TS
center;
|l|l|.
@ -571,7 +576,7 @@ _
T{
\fBmacOS\fP
T} T{
~/Library/Application Support/glances/, /usr/local/etc/glances/, /usr/share/docs/glances/
~/.config/glances/, ~/Library/Application Support/glances/, /usr/local/etc/glances/, /usr/share/docs/glances/
T}
_
T{
@ -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,51 @@ 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
# Set the way Glances should display the date (default is %Y\-%m\-%d %H:%M:%S %Z)
#strftime_format=\(dq%Y\-%m\-%d %H:%M:%S %Z\(dq
# Define external directory for loading additional plugins
# The layout follows the glances standard for plugin definitions
#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:
than a second one concerning the user interface:
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
[outputs]
# Theme name (for the moment only for the Curses interface: black or white)
curse_theme=black
# Separator in the Curses and WebUI interface (between top and others plugins)
separator=True
# Set the the Curses and WebUI interface left menu plugin list (comma\-separated)
#left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now
# Limit the number of processes to display (for the WebUI)
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
# Default is no prefix (/)
#url_prefix=/glances/
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
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 +705,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 +716,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 +749,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 +845,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 +858,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 +875,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
@ -885,6 +922,6 @@ $ glances browser
.sp
Nicolas Hennion aka Nicolargo <\fI\%contact@nicolargo.com\fP>
.SH COPYRIGHT
2023, Nicolas Hennion
2024, Nicolas Hennion
.\" Generated by docutils manpage writer.
.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,28 +20,26 @@ from glances.logger import logger
def user_config_dir():
r"""Return the per-user config dir (full path).
r"""Return a list of per-user config dir (full path).
- Linux, *BSD, SunOS: ~/.config/glances
- macOS: ~/Library/Application Support/glances
- Windows: %APPDATA%\glances
"""
paths = []
if WINDOWS:
path = os.environ.get('APPDATA')
paths.append(os.environ.get('APPDATA'))
elif MACOS:
path = os.path.expanduser('~/Library/Application Support')
paths.append(os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'))
paths.append(os.path.expanduser('~/Library/Application Support'))
else:
path = os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')
if path is None:
path = ''
else:
path = os.path.join(path, 'glances')
paths.append(os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'))
return path
return [os.path.join(path, 'glances') if path is not None else '' for path in paths]
def user_cache_dir():
r"""Return the per-user cache dir (full path).
r"""Return a list of per-user cache dir (full path).
- Linux, *BSD, SunOS: ~/.cache/glances
- macOS: ~/Library/Caches/glances
@ -54,11 +52,11 @@ def user_cache_dir():
else:
path = os.path.join(os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), 'glances')
return path
return [path]
def system_config_dir():
r"""Return the system-wide config dir (full path).
r"""Return a list of system-wide config dir (full path).
- Linux, SunOS: /etc/glances
- *BSD, macOS: /usr/local/etc/glances
@ -75,11 +73,11 @@ def system_config_dir():
else:
path = os.path.join(path, 'glances')
return path
return [path]
def default_config_dir():
r"""Return the system-wide config dir (full path).
r"""Return a list of system-wide config dir (full path).
- Linux, SunOS, *BSD, macOS: /usr/share/doc (as defined in the setup.py files)
- Windows: %APPDATA%\glances
@ -93,7 +91,7 @@ def default_config_dir():
else:
path = os.path.join(path, 'glances')
return path
return [path]
class Config(object):
@ -127,7 +125,7 @@ class Config(object):
* custom path: /path/to/glances
* Linux, SunOS: ~/.config/glances, /etc/glances
* *BSD: ~/.config/glances, /usr/local/etc/glances
* macOS: ~/Library/Application Support/glances, /usr/local/etc/glances
* macOS: ~/.config/glances, ~/Library/Application Support/glances, /usr/local/etc/glances
* Windows: %APPDATA%\glances
The config file will be searched in the following order of priority:
@ -138,12 +136,18 @@ class Config(object):
"""
paths = []
# self.config_dir is the path to the config file (via -C flag)
if self.config_dir:
paths.append(self.config_dir)
paths.append(os.path.join(user_config_dir(), self.config_filename))
paths.append(os.path.join(system_config_dir(), self.config_filename))
paths.append(os.path.join(default_config_dir(), self.config_filename))
# user_config_dir() returns a list of paths
paths.extend([os.path.join(path, self.config_filename) for path in user_config_dir()])
# system_config_dir() returns a list of paths
paths.extend([os.path.join(path, self.config_filename) for path in system_config_dir()])
# default_config_dir() returns a list of paths
paths.extend([os.path.join(path, self.config_filename) for path in default_config_dir()])
return paths
@ -321,6 +325,13 @@ class Config(object):
pass
return ret
def get_list_value(self, section, option, default=None, separator=','):
"""Get the list value of an option, if it exists."""
try:
return self.parser.get(section, option).split(separator)
except (NoOptionError, NoSectionError):
return default
def get_int_value(self, section, option, default=0):
"""Get the int value of an option, if it exists."""
try:

View File

@ -25,24 +25,34 @@ class GlancesEvents(object):
event_value = value
Item (or event) is defined by:
["begin",
"end",
"WARNING|CRITICAL",
"CPU|LOAD|MEM",
MAX, AVG, MIN, SUM, COUNT,
[top3 process list],
"Processes description",
"top sort key"]
{
"begin": "begin",
"end": "end",
"state": "WARNING|CRITICAL",
"type": "CPU|LOAD|MEM",
"max": MAX,
"avg": AVG,
"min": MIN,
"sum": SUM,
"count": COUNT,
"top": [top 3 process name],
"desc": "Processes description",
"sort": "top sort key"
}
"""
def __init__(self):
def __init__(self, max_events=10):
"""Init the events class."""
# Maximum size of the events list
self.events_max = 10
self.set_max_events(max_events)
# Init the logs list
self.events_list = []
def set_max_events(self, max_events):
"""Set the maximum size of the events list."""
self.max_events = max_events
def get(self):
"""Return the raw events list."""
return self.events_list
@ -56,11 +66,11 @@ class GlancesEvents(object):
An event exist if:
* end is < 0
* event_type is matching
* type is matching
Return -1 if the item is not found.
"""
for i in range(self.len()):
if self.events_list[i][1] < 0 and self.events_list[i][3] == event_type:
if self.events_list[i]['end'] < 0 and self.events_list[i]['type'] == event_type:
return i
return -1
@ -101,14 +111,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.
@ -120,28 +130,27 @@ class GlancesEvents(object):
# Create the new log item
# Time is stored in Epoch format
# Epoch -> DMYHMS = datetime.fromtimestamp(epoch)
item = [
time.mktime(datetime.now().timetuple()), # START DATE
-1, # END DATE
event_state, # STATE: WARNING|CRITICAL
event_type, # TYPE: CPU, LOAD, MEM...
event_value, # MAX
event_value, # AVG
event_value, # MIN
event_value, # SUM
1, # COUNT
[], # TOP 3 PROCESS LIST
proc_desc, # MONITORED PROCESSES DESC
glances_processes.sort_key,
] # TOP PROCESS SORT KEY
item = {
"begin": time.mktime(datetime.now().timetuple()),
"end": -1,
"state": event_state,
"type": event_type,
"max": event_value,
"avg": event_value,
"min": event_value,
"sum": event_value,
"count": 1,
"top": [],
"desc": proc_desc,
"sort": glances_processes.sort_key,
}
# Add the item to the list
self.events_list.insert(0, item)
# Limit the list to 'events_max' items
if self.len() > self.events_max:
# Limit the list to 'max_events' items
if self.len() > self.max_events:
self.events_list.pop()
return True
else:
return False
@ -154,9 +163,9 @@ class GlancesEvents(object):
# Set the end of the events
end_time = time.mktime(datetime.now().timetuple())
if end_time - self.events_list[event_index][0] > peak_time:
if end_time - self.events_list[event_index]['begin'] > peak_time:
# If event is > peak_time seconds
self.events_list[event_index][1] = end_time
self.events_list[event_index]['end'] = end_time
else:
# If event <= peak_time seconds, ignore
self.events_list.remove(self.events_list[event_index])
@ -166,25 +175,25 @@ class GlancesEvents(object):
# State
if event_state == "CRITICAL":
self.events_list[event_index][2] = event_state
self.events_list[event_index]['state'] = event_state
# Min value
self.events_list[event_index][6] = min(self.events_list[event_index][6], event_value)
self.events_list[event_index]['min'] = min(self.events_list[event_index]['min'], event_value)
# Max value
self.events_list[event_index][4] = max(self.events_list[event_index][4], event_value)
self.events_list[event_index]['max'] = max(self.events_list[event_index]['max'], event_value)
# Average value
self.events_list[event_index][7] += event_value
self.events_list[event_index][8] += 1
self.events_list[event_index][5] = self.events_list[event_index][7] / self.events_list[event_index][8]
self.events_list[event_index]['sum'] += event_value
self.events_list[event_index]['count'] += 1
self.events_list[event_index]['avg'] = self.events_list[event_index]['sum'] / self.events_list[event_index]['count']
# TOP PROCESS LIST (only for CRITICAL ALERT)
if event_state == "CRITICAL":
events_sort_key = self.get_event_sort_key(event_type)
# Sort the current process list to retrieve the TOP 3 processes
self.events_list[event_index][9] = sort_stats(proc_list, events_sort_key)[0:3]
self.events_list[event_index][11] = events_sort_key
self.events_list[event_index]['top'] = [p['name'] for p in sort_stats(proc_list, events_sort_key)[0:3]]
self.events_list[event_index]['sort'] = events_sort_key
# MONITORED PROCESSES DESC
self.events_list[event_index][10] = proc_desc
self.events_list[event_index]['desc'] = proc_desc
return True
@ -198,7 +207,7 @@ class GlancesEvents(object):
clean_events_list = []
while self.len() > 0:
item = self.events_list.pop()
if item[1] < 0 or (not critical and item[2].startswith("CRITICAL")):
if item['end'] < 0 or (not critical and item['state'].startswith("CRITICAL")):
clean_events_list.insert(0, item)
# The list is now the clean one
self.events_list = clean_events_list

View File

@ -9,14 +9,21 @@
"""CouchDB interface class."""
#
# How to test ?
#
# 1) docker run -d -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=admin -p 5984:5984 --name my-couchdb couchdb
# 2) ./venv/bin/python -m glances -C ./conf/glances.conf --export couchdb --quiet
# 3) Result can be seen at: http://127.0.0.1:5984/_utils
#
import sys
from datetime import datetime
from glances.logger import logger
from glances.exports.export import GlancesExport
import couchdb
import couchdb.mapping
import pycouchdb
class Export(GlancesExport):
@ -27,15 +34,9 @@ class Export(GlancesExport):
"""Init the CouchDB export IF."""
super(Export, self).__init__(config=config, args=args)
# Mandatory configuration keys (additional to host and port)
self.db = None
# Optional configuration keys
self.user = None
self.password = None
# Load the Cassandra configuration file section
self.export_enable = self.load_conf('couchdb', mandatories=['host', 'port', 'db'], options=['user', 'password'])
# 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'])
if not self.export_enable:
sys.exit(2)
@ -47,35 +48,28 @@ class Export(GlancesExport):
if not self.export_enable:
return None
if self.user is None:
server_uri = 'http://{}:{}/'.format(self.host, self.port)
else:
# Force https if a login/password is provided
# Related to https://github.com/nicolargo/glances/issues/2124
server_uri = 'https://{}:{}@{}:{}/'.format(self.user, self.password, self.host, self.port)
# @TODO: https
server_uri = 'http://{}:{}@{}:{}/'.format(self.user, self.password, self.host, self.port)
try:
s = couchdb.Server(server_uri)
s = pycouchdb.Server(server_uri)
except Exception as e:
logger.critical("Cannot connect to CouchDB server (%s)" % e)
sys.exit(2)
else:
logger.info("Connected to the CouchDB server")
logger.info("Connected to the CouchDB server version %s" % s.info()['version'])
try:
s[self.db]
s.database(self.db)
except Exception:
# Database did not exist
# Create it...
s.create(self.db)
logger.info("Create CouchDB database %s" % self.db)
else:
logger.info("There is already a %s database" % self.db)
logger.info("CouchDB database %s already exist" % self.db)
return s
def database(self):
"""Return the CouchDB database object"""
return self.client[self.db]
return s.database(self.db)
def export(self, name, columns, points):
"""Write the points to the CouchDB server."""
@ -84,13 +78,12 @@ class Export(GlancesExport):
# Create DB input
data = dict(zip(columns, points))
# Set the type to the current stat name
# Add the type and the timestamp in ISO format
data['type'] = name
data['time'] = couchdb.mapping.DateTimeField()._to_json(datetime.now())
data['time'] = datetime.now().isoformat()[:-3] + 'Z'
# Write data to the CouchDB database
# Result can be seen at: http://127.0.0.1:5984/_utils
try:
self.client[self.db].save(data)
self.client.save(data)
except Exception as e:
logger.error("Cannot export {} stats to CouchDB ({})".format(name, e))

View File

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

View File

@ -71,7 +71,7 @@ class Export(GlancesExport):
metric_name = self.prefix + self.METRIC_SEPARATOR + str(name) + self.METRIC_SEPARATOR + str(k)
# Prometheus is very sensible to the metric name
# See: https://prometheus.io/docs/practices/naming/
for c in ['.', '-', '/', ' ']:
for c in ' .-/:[]':
metric_name = metric_name.replace(c, self.METRIC_SEPARATOR)
# Get the labels
labels = self.parse_tags(self.labels)

View File

@ -10,25 +10,11 @@
"""Manage the folder list."""
from __future__ import unicode_literals
import os
from glances.timer import Timer
from glances.globals import nativestr
from glances.globals import nativestr, folder_size
from glances.logger import logger
# Use the built-in version of scandir/walk if possible, otherwise
# use the scandir module version
scandir_tag = True
try:
# For Python 3.5 or higher
from os import scandir
except ImportError:
# For others...
try:
from scandir import scandir
except ImportError:
scandir_tag = False
class FolderList(object):
@ -62,12 +48,9 @@ class FolderList(object):
self.first_grab = True
if self.config is not None and self.config.has_section('folders'):
if scandir_tag:
# Process monitoring list
logger.debug("Folder list configuration detected")
self.__set_folder_list('folders')
else:
logger.error('Scandir not found. Please use Python 3.5+ or install the scandir lib')
# Process monitoring list
logger.debug("Folder list configuration detected")
self.__set_folder_list('folders')
else:
self.__folder_list = []
@ -132,23 +115,6 @@ class FolderList(object):
else:
return None
def __folder_size(self, path):
"""Return the size of the directory given by path
path: <string>"""
ret = 0
for f in scandir(path):
if f.is_dir(follow_symlinks=False) and (f.name != '.' or f.name != '..'):
ret += self.__folder_size(os.path.join(path, f.name))
else:
try:
ret += f.stat().st_size
except OSError:
pass
return ret
def update(self, key='path'):
"""Update the command result attributed."""
# Only continue if monitor list is not empty
@ -163,15 +129,13 @@ class FolderList(object):
# Set the key (see issue #2327)
self.__folder_list[i]['key'] = key
# Get folder size
try:
self.__folder_list[i]['size'] = self.__folder_size(self.path(i))
except OSError as e:
logger.debug('Cannot get folder size ({}). Error: {}'.format(self.path(i), e))
if e.errno == 13:
# Permission denied
self.__folder_list[i]['size'] = '!'
else:
self.__folder_list[i]['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
self.timer_folders[i].reset()

View File

@ -25,6 +25,8 @@ import subprocess
from datetime import datetime
import re
import base64
import functools
import weakref
import queue
from configparser import ConfigParser, NoOptionError, NoSectionError
@ -258,7 +260,7 @@ def pretty_date(time=False):
Source: https://stackoverflow.com/questions/1551382/user-friendly-time-format-in-python
"""
now = datetime.now()
if type(time) is int:
if isinstance(time, int):
diff = now - datetime.fromtimestamp(time)
elif isinstance(time, datetime):
diff = now - time
@ -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:
@ -372,3 +382,57 @@ def string_value_to_float(s):
def file_exists(filename):
"""Return True if the file exists and is readable."""
return os.path.isfile(filename) and os.access(filename, os.R_OK)
def folder_size(path, errno=0):
"""Return a tuple with the size of the directory given by path and the errno.
If an error occurs (for example one file or subfolder is not accessible),
errno is set to the error number.
path: <string>
errno: <int> Should always be 0 when calling the function"""
ret_size = 0
ret_err = errno
try:
for f in os.scandir(path):
if f.is_dir(follow_symlinks=False) and (f.name != '.' or f.name != '..'):
ret = folder_size(os.path.join(path, f.name), ret_err)
ret_size += ret[0]
ret_err = ret[1]
else:
try:
ret_size += f.stat().st_size
except OSError as e:
ret_err = e.errno
except OSError as e:
return 0, e.errno
else:
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
def namedtuple_to_dict(data):
"""Convert a namedtuple to a dict, using the _asdict() method embeded in PsUtil stats."""
return {k: (v._asdict() if hasattr(v, '_asdict') else v) for k, v in data.items()}
def list_of_namedtuple_to_list_of_dict(data):
"""Convert a list of namedtuples to a dict, using the _asdict() method embeded in PsUtil stats."""
return [namedtuple_to_dict(d) for d in data]

View File

@ -12,8 +12,10 @@
import argparse
import sys
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
@ -81,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
"""
@ -97,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',
@ -121,12 +131,17 @@ Examples of use:
parser.add_argument(
'--disable-plugin',
'--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(
'--enable-plugin', '--enable-plugins', dest='enable_plugin', help='enable plugin (comma separated list)'
'--enable-plugin',
'--enable-plugins',
'--enable',
dest='enable_plugin',
help='enable plugin (comma-separated list)',
)
parser.add_argument(
'--disable-process',
@ -149,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',
@ -229,12 +244,11 @@ Examples of use:
help='enable extended stats on top process',
)
parser.add_argument(
'--separator',
'--enable-separator',
action='store_true',
default=False,
'--disable-separator',
action='store_false',
default=True,
dest='enable_separator',
help='enable separator in the UI',
help='disable separator in the UI (between top and others modules)',
),
parser.add_argument(
'--disable-cursor',
@ -260,7 +274,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'
)
@ -355,7 +369,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',
@ -413,19 +427,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',
@ -457,7 +471,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',
@ -465,7 +479,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',
@ -514,7 +528,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(
@ -533,36 +547,28 @@ Examples of use:
return parser
def parse_args(self):
"""Parse command line arguments."""
args = self.init_args().parse_args()
# Load the configuration file, if it exists
# This function should be called after the parse_args
# because the configuration file path can be defined
self.config = Config(args.conf_file)
# Debug mode
def init_debug(self, args):
"""Init Glances debug mode."""
if args.debug:
from logging import DEBUG
logger.setLevel(DEBUG)
else:
from warnings import simplefilter
simplefilter("ignore")
# Plugins refresh rate
def init_refresh_rate(self, args):
"""Init Glances refresh rate"""
if self.config.has_section('global'):
global_refresh = self.config.get_float_value('global', 'refresh', default=self.DEFAULT_REFRESH_TIME)
else:
global_refresh = self.DEFAULT_REFRESH_TIME
# The configuration key can be overwrite from the command line
# The configuration key can be overwrite from the command line (-t <time>)
if args.time == self.DEFAULT_REFRESH_TIME:
args.time = global_refresh
logger.debug('Global refresh rate is set to {} seconds'.format(args.time))
# Plugins disable/enable
def init_plugins(self, args):
"""Init Glances plugins"""
# Allow users to disable plugins from the glances.conf (issue #1378)
for s in self.config.sections():
if self.config.has_section(s) and (self.config.get_bool_value(s, 'disable', False)):
@ -589,6 +595,16 @@ Examples of use:
for p in args.export.split(','):
setattr(args, 'export_' + p, True)
# By default help is hidden
args.help_tag = False
# Display Rx and Tx, not the sum for the network
args.network_sum = False
args.network_cumul = False
def init_client_server(self, args):
"""Init Glances client/server mode."""
# Client/server Port
if args.port is None:
if args.webserver:
@ -601,14 +617,10 @@ Examples of use:
x if x else y for (x, y) in zip(args.client.partition(':')[::2], (args.client, args.port))
)
# Autodiscover
# Client autodiscover mode
if args.disable_autodiscover:
logger.info("Auto discover mode is disabled")
# In web server mode
if args.webserver:
args.process_short_name = True
# Server or client login/password
if args.username_prompt:
# Every username needs a password
@ -652,15 +664,9 @@ Examples of use:
# Default is no password
args.password = self.password
# By default help is hidden
args.help_tag = False
# Display Rx and Tx, not the sum for the network
args.network_sum = False
args.network_cumul = False
def init_ui_mode(self, args):
# Manage light mode
if args.enable_light:
if getattr(args, 'enable_light', False):
logger.info("Light mode is on")
args.disable_left_sidebar = True
disable(args, 'process')
@ -669,7 +675,7 @@ Examples of use:
disable(args, 'docker')
# Manage full quicklook option
if args.full_quicklook:
if getattr(args, 'full_quicklook', False):
logger.info("Full quicklook mode")
enable(args, 'quicklook')
disable(args, 'cpu')
@ -678,7 +684,7 @@ Examples of use:
enable(args, 'load')
# Manage disable_top option
if args.disable_top:
if getattr(args, 'disable_top', False):
logger.info("Disable top menu")
disable(args, 'quicklook')
disable(args, 'cpu')
@ -686,6 +692,39 @@ Examples of use:
disable(args, 'memswap')
disable(args, 'load')
# Memory leak
if getattr(args, 'memory_leak', False):
logger.info('Memory leak detection enabled')
args.quiet = True
if not args.stop_after:
args.stop_after = 60
args.time = 1
args.disable_history = True
def parse_args(self):
"""Parse command line arguments."""
args = self.init_args().parse_args()
# Load the configuration file, if it exists
# This function should be called after the parse_args
# because the configuration file path can be defined
self.config = Config(args.conf_file)
# Init Glances debug mode
self.init_debug(args)
# Plugins Glances refresh rate
self.init_refresh_rate(args)
# Manage Plugins disable/enable option
self.init_plugins(args)
# Init Glances client/server mode
self.init_client_server(args)
# Init UI mode
self.init_ui_mode(args)
# Init the generate_graph tag
# Should be set to True to generate graphs
args.generate_graph = False
@ -713,26 +752,6 @@ Examples of use:
if not args.disable_cursor and not self.is_standalone():
logger.debug("Cursor is only available in standalone mode")
# Disable HDDTemp if sensors are disabled
if getattr(self.args, 'disable_sensors', False):
disable(self.args, 'hddtemp')
logger.debug("Sensors and HDDTemp are disabled")
if getattr(self.args, 'trace_malloc', True) and not self.is_standalone():
logger.critical("Option --trace-malloc is only available in the terminal mode")
sys.exit(2)
if getattr(self.args, 'memory_leak', True) and not self.is_standalone():
logger.critical("Option --memory-leak is only available in the terminal mode")
sys.exit(2)
elif getattr(self.args, 'memory_leak', True) and self.is_standalone():
logger.info('Memory leak detection enabled')
self.args.quiet = True
if not self.args.stop_after:
self.args.stop_after = 60
self.args.time = 1
self.args.disable_history = True
# Let the plugins known the Glances mode
self.args.is_standalone = self.is_standalone()
self.args.is_client = self.is_client()
@ -747,8 +766,19 @@ Examples of use:
def check_mode_compatibility(self):
"""Check mode compatibility"""
# Server and Web server are not compatible
if self.args.is_server and self.args.is_webserver:
logger.critical("Server and Web server mode are incompatible")
logger.critical("Server and Web server modes should not be used together")
sys.exit(2)
# Trace malloc option
if getattr(self.args, 'trace_malloc', True) and not self.is_standalone():
logger.critical("Option --trace-malloc is only available in the terminal mode")
sys.exit(2)
# Memory leak option
if getattr(self.args, 'memory_leak', True) and not self.is_standalone():
logger.critical("Option --memory-leak is only available in the terminal mode")
sys.exit(2)
def is_standalone(self):

View File

@ -43,7 +43,7 @@ class Outdated(object):
"""Init the Outdated class"""
self.args = args
self.config = config
self.cache_dir = user_cache_dir()
self.cache_dir = user_cache_dir()[0]
self.cache_file = os.path.join(self.cache_dir, 'glances-version.db')
# Set default value...

View File

@ -15,7 +15,6 @@ from math import modf
class Bar(object):
"""Manage bar (progression or status).
import sys
@ -28,28 +27,49 @@ class Bar(object):
sys.stdout.flush()
"""
def __init__(self, size, percentage_char='|', empty_char=' ', pre_char='[', post_char=']', with_text=True):
def __init__(self, size,
bar_char='|',
empty_char=' ',
pre_char='[', post_char=']',
unit_char='%',
display_value=True,
min_value=0, max_value=100):
"""Init a bar (used in Quicllook plugin)
Args:
size (_type_): Bar size
bar_char (str, optional): Bar character. Defaults to '|'.
empty_char (str, optional): Empty character. Defaults to ' '.
pre_char (str, optional): Display this char before the bar. Defaults to '['.
post_char (str, optional): Display this char after the bar. Defaults to ']'.
unit_char (str, optional): Unit char to be displayed. Defaults to '%'.
display_value (bool, optional): Do i need to display the value. Defaults to True.
min_value (int, optional): Minimum value. Defaults to 0.
max_value (int, optional): Maximum value (percent can be higher). Defaults to 100.
"""
# Build curses_bars
self.__curses_bars = [empty_char] * 5 + [percentage_char] * 5
self.__curses_bars = [empty_char] * 5 + [bar_char] * 5
# Bar size
self.__size = size
# Bar current percent
self.__percent = 0
# Min and max value
self.min_value = 0
self.max_value = 100
self.min_value = min_value
self.max_value = max_value
# Char used for the decoration
self.__pre_char = pre_char
self.__post_char = post_char
self.__empty_char = empty_char
self.__with_text = with_text
self.__unit_char = unit_char
# Value should be displayed ?
self.__display_value = display_value
@property
def size(self, with_decoration=False):
# Return the bar size, with or without decoration
if with_decoration:
return self.__size
if self.__with_text:
if self.__display_value:
return self.__size - 6
@property
@ -58,10 +78,8 @@ class Bar(object):
@percent.setter
def percent(self, value):
if value <= self.min_value:
if value < self.min_value:
value = self.min_value
if value >= self.max_value:
value = self.max_value
self.__percent = value
@property
@ -72,16 +90,34 @@ class Bar(object):
def post_char(self):
return self.__post_char
def get(self):
def get(self, overlay: str = None):
"""Return the bars."""
frac, whole = modf(self.size * self.percent / 100.0)
value = self.max_value if self.percent > self.max_value else self.percent
# Build the bar
frac, whole = modf(self.size * value / 100.0)
ret = self.__curses_bars[8] * int(whole)
if frac > 0:
ret += self.__curses_bars[int(frac * 8)]
whole += 1
ret += self.__empty_char * int(self.size - whole)
if self.__with_text:
ret = '{}{:5.1f}%'.format(ret, self.percent)
# Add the value
if self.__display_value:
if self.percent >= self.max_value:
ret = '{} {}{:3.0f}{}'.format(ret,
'>' if self.percent > self.max_value else ' ',
self.max_value,
self.__unit_char)
else:
ret = '{}{:5.1f}{}'.format(ret,
self.percent,
self.__unit_char)
# Add overlay
if overlay and len(overlay) < len(ret) - 6:
ret = overlay + ret[len(overlay):]
return ret
def __str__(self):

View File

@ -1,637 +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
#
"""Web 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>/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_history(self, plugin, nb=0):
"""Glances API RESTful implementation.
Return the JSON representation of a given plugin history
Limit to the last nb items (all if nb=0)
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
self.__update__()
try:
# Get the JSON value of the stat ID
statval = self.stats.get_plugin(plugin).get_stats_history(nb=int(nb))
except Exception as e:
abort(404, "Cannot get plugin history %s (%s)" % (plugin, str(e)))
return statval
@compress
def _api_limits(self, plugin):
"""Glances API RESTful implementation.
Return the JSON limits of a given plugin
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
# self.__update__()
try:
# Get the JSON value of the stat limits
ret = self.stats.get_plugin(plugin).limits
except Exception as e:
abort(404, "Cannot get limits for plugin %s (%s)" % (plugin, str(e)))
return ret
@compress
def _api_views(self, plugin):
"""Glances API RESTful implementation.
Return the JSON views of a given plugin
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
# self.__update__()
try:
# Get the JSON value of the stat views
ret = self.stats.get_plugin(plugin).get_views()
except Exception as e:
abort(404, "Cannot get views for plugin %s (%s)" % (plugin, str(e)))
return ret
# No compression see issue #1228
# @compress
def _api_itemvalue(self, plugin, item, value=None, history=False, nb=0):
"""Father method for _api_item and _api_value."""
response.content_type = 'application/json; charset=utf-8'
if plugin not in self.plugins_list:
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
# Update the stat
self.__update__()
if value is None:
if history:
ret = self.stats.get_plugin(plugin).get_stats_history(item, nb=int(nb))
else:
ret = self.stats.get_plugin(plugin).get_stats_item(item)
if ret is None:
abort(404, "Cannot get item %s%s in plugin %s" % (item, 'history ' if history else '', plugin))
else:
if history:
# Not available
ret = None
else:
ret = self.stats.get_plugin(plugin).get_stats_value(item, value)
if ret is None:
abort(
404, "Cannot get item %s(%s=%s) in plugin %s" % ('history ' if history else '', item, value, plugin)
)
return ret
@compress
def _api_item(self, plugin, item):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/item
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
return self._api_itemvalue(plugin, item)
@compress
def _api_item_history(self, plugin, item, nb=0):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/history of item
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
return self._api_itemvalue(plugin, item, history=True, nb=int(nb))
@compress
def _api_value(self, plugin, item, value):
"""Glances API RESTful implementation.
Return the process stats (dict) for the given item=value
HTTP/200 if OK
HTTP/400 if plugin is not found
HTTP/404 if others error
"""
return self._api_itemvalue(plugin, item, value)
@compress
def _api_config(self):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration file
HTTP/200 if OK
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
try:
# Get the JSON value of the config' dict
args_json = json_dumps(self.config.as_dict())
except Exception as e:
abort(404, "Cannot get config (%s)" % str(e))
return args_json
@compress
def _api_config_item(self, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
config_dict = self.config.as_dict()
if item not in config_dict:
abort(400, "Unknown configuration item %s" % item)
try:
# Get the JSON value of the config' dict
args_json = json_dumps(config_dict[item])
except Exception as e:
abort(404, "Cannot get config item (%s)" % str(e))
return args_json
@compress
def _api_args(self):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments
HTTP/200 if OK
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
try:
# Get the JSON value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = json_dumps(vars(self.args))
except Exception as e:
abort(404, "Cannot get args (%s)" % str(e))
return args_json
@compress
def _api_args_item(self, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
response.content_type = 'application/json; charset=utf-8'
if item not in self.args:
abort(400, "Unknown argument item %s" % item)
try:
# Get the JSON value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = json_dumps(vars(self.args)[item])
except Exception as e:
abort(404, "Cannot get args item (%s)" % str(e))
return args_json
class EnableCors(object):
name = 'enable_cors'
api = 2
def apply(self, fn, context):
def _enable_cors(*args, **kwargs):
# set CORS headers
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
response.headers[
'Access-Control-Allow-Headers'
] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
if request.method != 'OPTIONS':
# actual request; reply with the actual response
return fn(*args, **kwargs)
return _enable_cors

View File

@ -39,15 +39,16 @@ class _GlancesCurses(object):
"""
_hotkeys = {
# 'ENTER' > Edit the process filter
'\n': {'handler': '_handle_enter'},
'0': {'switch': 'disable_irix'},
'1': {'switch': 'percpu'},
'2': {'switch': 'disable_left_sidebar'},
'3': {'switch': 'disable_quicklook'},
# '4' > Enable or disable quicklook
# '5' > Enable or disable top menu
'4': {'handler': '_handle_quicklook'},
'5': {'handler': '_handle_top_menu'},
'6': {'switch': 'meangpu'},
'9': {'switch': 'theme_white'},
'9': {'switch': 'theme_white',
'handler': '_handle_theme'},
'/': {'switch': 'process_short_name'},
'a': {'sort_key': 'auto'},
'A': {'switch': 'disable_amps'},
@ -58,8 +59,8 @@ class _GlancesCurses(object):
'd': {'switch': 'disable_diskio'},
'D': {'switch': 'disable_containers'},
# 'e' > Enable/Disable process extended
# 'E' > Erase the process filter
# 'f' > Show/hide fs / folder stats
'E': {'handler': '_handle_erase_filter'},
'f': {'handler': '_handle_fs_stats'},
'F': {'switch': 'fs_free_space'},
'g': {'switch': 'generate_graph'},
'G': {'switch': 'disable_gpu'},
@ -86,12 +87,12 @@ class _GlancesCurses(object):
'T': {'switch': 'network_sum'},
'u': {'sort_key': 'username'},
'U': {'switch': 'network_cumul'},
# 'w' > Delete finished warning logs
'w': {'handler': '_handle_clean_logs'},
'W': {'switch': 'disable_wifi'},
# 'x' > Delete finished warning and critical logs
# 'z' > Enable or disable processes
# '+' > Increase the process nice level
# '-' > Decrease the process nice level
'x': {'handler': '_handle_clean_critical_logs'},
'z': {'handler': '_handle_disable_process'},
'+': {'handler': '_handle_increase_nice'},
'-': {'handler': '_handle_decrease_nice'},
# "<" (left arrow) navigation through process sort
# ">" (right arrow) navigation through process sort
# 'UP' > Up in the server list
@ -105,6 +106,8 @@ class _GlancesCurses(object):
_quicklook_max_width = 68
# Define left sidebar
# This default list is also defined in the glances/outputs/static/js/uiconfig.json
# file for the web interface
_left_sidebar = [
'network',
'wifi',
@ -139,10 +142,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)
@ -186,11 +200,16 @@ class _GlancesCurses(object):
def load_config(self, config):
"""Load the outputs section of the configuration file."""
# Load the theme
if config is not None and config.has_section('outputs'):
logger.debug('Read the outputs section in the configuration file')
# Load the theme
self.theme['name'] = config.get_value('outputs', 'curse_theme', default='black')
logger.debug('Theme for the curse interface: {}'.format(self.theme['name']))
# Separator ?
self.args.enable_separator = config.get_bool_value('outputs', 'separator', default=True)
# Set the left sidebar list
self._left_sidebar = config.get_list_value('outputs',
'left_menu',
default=self._left_sidebar)
def is_theme(self, name):
"""Return True if the theme *name* should be used."""
@ -236,6 +255,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 +266,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 +304,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 = {
@ -330,6 +356,8 @@ class _GlancesCurses(object):
'PASSWORD': curses.A_PROTECT,
'SELECTED': self.selected_color,
'INFO': self.ifINFO_color,
'ERROR': self.selected_color,
'SEPARATOR': self.separator,
}
def set_cursor(self, value):
@ -360,123 +388,138 @@ class _GlancesCurses(object):
logger.debug("Keypressed (code: {})".format(self.pressedkey))
for hotkey in self._hotkeys:
if self.pressedkey == ord(hotkey) and 'switch' in self._hotkeys[hotkey]:
# Get the option name
# Ex: disable_foo return foo
# enable_foo_bar return foo_bar
option = '_'.join(self._hotkeys[hotkey]['switch'].split('_')[1:])
if self._hotkeys[hotkey]['switch'].startswith('disable_'):
# disable_ switch
if getattr(self.args, self._hotkeys[hotkey]['switch']):
enable(self.args, option)
else:
disable(self.args, option)
elif self._hotkeys[hotkey]['switch'].startswith('enable_'):
# enable_ switch
if getattr(self.args, self._hotkeys[hotkey]['switch']):
disable(self.args, option)
else:
enable(self.args, option)
else:
# Others switchs options (with no enable_ or disable_)
setattr(
self.args,
self._hotkeys[hotkey]['switch'],
not getattr(self.args, self._hotkeys[hotkey]['switch']),
)
if self.pressedkey == ord(hotkey) and 'sort_key' in self._hotkeys[hotkey]:
glances_processes.set_sort_key(
self._hotkeys[hotkey]['sort_key'], self._hotkeys[hotkey]['sort_key'] == 'auto'
)
self._handle_switch(hotkey)
elif self.pressedkey == ord(hotkey) and 'sort_key' in self._hotkeys[hotkey]:
self._handle_sort_key(hotkey)
if self.pressedkey == ord(hotkey) and 'handler' in self._hotkeys[hotkey]:
action = getattr(self, self._hotkeys[hotkey]['handler'])
action()
# Other actions...
if self.pressedkey == ord('\n'):
# 'ENTER' > Edit the process filter
self.edit_filter = not self.edit_filter
elif self.pressedkey == ord('4'):
# '4' > Enable or disable quicklook
self.args.full_quicklook = not self.args.full_quicklook
if self.args.full_quicklook:
self.enable_fullquicklook()
else:
self.disable_fullquicklook()
elif self.pressedkey == ord('5'):
# '5' > Enable or disable top menu
self.args.disable_top = not self.args.disable_top
if self.args.disable_top:
self.disable_top()
else:
self.enable_top()
elif self.pressedkey == ord('9'):
# '9' > Theme from black to white and reverse
self._init_colors()
elif self.pressedkey == ord('e') and not self.args.programs:
# 'e' > Enable/Disable process extended
self.args.enable_process_extended = not self.args.enable_process_extended
if not self.args.enable_process_extended:
glances_processes.disable_extended()
else:
glances_processes.enable_extended()
# When a process is selected (and only in standalone mode), disable the cursor
self.args.disable_cursor = self.args.enable_process_extended and self.args.is_standalone
elif self.pressedkey == ord('E'):
# 'E' > Erase the process filter
glances_processes.process_filter = None
elif self.pressedkey == ord('f'):
# 'f' > Show/hide fs / folder stats
self.args.disable_fs = not self.args.disable_fs
self.args.disable_folders = not self.args.disable_folders
elif self.pressedkey == ord('+'):
# '+' > Increase process nice level
self.increase_nice_process = not self.increase_nice_process
elif self.pressedkey == ord('-'):
# '+' > Decrease process nice level
self.decrease_nice_process = not self.decrease_nice_process
# Other actions with key > 255 (ord will not work) and/or additional test...
if self.pressedkey == ord('e') and not self.args.programs:
self._handle_process_extended()
elif self.pressedkey == ord('k') and not self.args.disable_cursor:
# 'k' > Kill selected process (after confirmation)
self.kill_process = not self.kill_process
elif self.pressedkey == ord('w'):
# 'w' > Delete finished warning logs
glances_events.clean()
elif self.pressedkey == ord('x'):
# 'x' > Delete finished warning and critical logs
glances_events.clean(critical=True)
elif self.pressedkey == ord('z'):
# 'z' > Enable or disable processes
self.args.disable_process = not self.args.disable_process
if self.args.disable_process:
glances_processes.disable()
else:
glances_processes.enable()
self._handle_kill_process()
elif self.pressedkey == curses.KEY_LEFT:
# "<" (left arrow) navigation through process sort
next_sort = (self.loop_position() - 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False)
self._handle_sort_left()
elif self.pressedkey == curses.KEY_RIGHT:
# ">" (right arrow) navigation through process sort
next_sort = (self.loop_position() + 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False)
self._handle_sort_right()
elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65 and not self.args.disable_cursor:
# 'UP' > Up in the server list
if self.args.cursor_position > 0:
self.args.cursor_position -= 1
self._handle_cursor_up()
elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66 and not self.args.disable_cursor:
# 'DOWN' > Down in the server list
# if self.args.cursor_position < glances_processes.max_processes - 2:
if self.args.cursor_position < glances_processes.processes_count:
self.args.cursor_position += 1
self._handle_cursor_down()
elif self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
# 'ESC'|'q' > Quit
if return_to_browser:
logger.info("Stop Glances client and return to the browser")
else:
logger.info("Stop Glances (keypressed: {})".format(self.pressedkey))
self._handle_quit(return_to_browser)
elif self.pressedkey == curses.KEY_F5 or self.pressedkey == 18:
# "F5" or Ctrl-R to force UI refresh
pass
self._handle_refresh()
# Return the key code
return self.pressedkey
def _handle_switch(self, hotkey):
option = '_'.join(self._hotkeys[hotkey]['switch'].split('_')[1:])
if self._hotkeys[hotkey]['switch'].startswith('disable_'):
if getattr(self.args, self._hotkeys[hotkey]['switch']):
enable(self.args, option)
else:
disable(self.args, option)
elif self._hotkeys[hotkey]['switch'].startswith('enable_'):
if getattr(self.args, self._hotkeys[hotkey]['switch']):
disable(self.args, option)
else:
enable(self.args, option)
else:
setattr(
self.args,
self._hotkeys[hotkey]['switch'],
not getattr(self.args, self._hotkeys[hotkey]['switch']),
)
def _handle_sort_key(self, hotkey):
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
def _handle_quicklook(self):
self.args.full_quicklook = not self.args.full_quicklook
if self.args.full_quicklook:
self.enable_fullquicklook()
else:
self.disable_fullquicklook()
def _handle_top_menu(self):
self.args.disable_top = not self.args.disable_top
if self.args.disable_top:
self.disable_top()
else:
self.enable_top()
def _handle_theme(self):
self._init_colors()
def _handle_process_extended(self):
self.args.enable_process_extended = not self.args.enable_process_extended
if not self.args.enable_process_extended:
glances_processes.disable_extended()
else:
glances_processes.enable_extended()
self.args.disable_cursor = self.args.enable_process_extended and self.args.is_standalone
def _handle_erase_filter(self):
glances_processes.process_filter = None
def _handle_fs_stats(self):
self.args.disable_fs = not self.args.disable_fs
self.args.disable_folders = not self.args.disable_folders
def _handle_increase_nice(self):
self.increase_nice_process = not self.increase_nice_process
def _handle_decrease_nice(self):
self.decrease_nice_process = not self.decrease_nice_process
def _handle_kill_process(self):
self.kill_process = not self.kill_process
def _handle_clean_logs(self):
glances_events.clean()
def _handle_clean_critical_logs(self):
glances_events.clean(critical=True)
def _handle_disable_process(self):
self.args.disable_process = not self.args.disable_process
if self.args.disable_process:
glances_processes.disable()
else:
glances_processes.enable()
def _handle_sort_left(self):
next_sort = (self.loop_position() - 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False)
def _handle_sort_right(self):
next_sort = (self.loop_position() + 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False)
def _handle_cursor_up(self):
if self.args.cursor_position > 0:
self.args.cursor_position -= 1
def _handle_cursor_down(self):
if self.args.cursor_position < glances_processes.processes_count:
self.args.cursor_position += 1
def _handle_quit(self, return_to_browser):
if return_to_browser:
logger.info("Stop Glances client and return to the browser")
else:
logger.info("Stop Glances (keypressed: {})".format(self.pressedkey))
def _handle_refresh(self):
pass
def loop_position(self):
"""Return the current sort in the loop"""
for i, v in enumerate(self._sort_loop):
@ -541,8 +584,8 @@ class _GlancesCurses(object):
"""New column in the curses interface."""
self.column = self.next_column
def separator_line(self, color='TITLE'):
"""New separator line in the curses interface."""
def separator_line(self, color='SEPARATOR'):
"""Add a separator line in the curses interface."""
if not self.args.enable_separator:
return
self.new_line()
@ -575,18 +618,23 @@ class _GlancesCurses(object):
for p in stats.getPluginsList(enable=False):
if p == 'quicklook' or p == 'processlist':
# processlist is done later
# - processlist is done later
# because we need to know how many processes could be displayed
# - quicklook is done later
# because it is based on CPU, MEM, SWAP and LOAD
continue
# Compute the plugin max size
plugin_max_width = None
if p in self._left_sidebar:
plugin_max_width = max(self._left_sidebar_min_width, self.term_window.getmaxyx()[1] - 105)
plugin_max_width = min(self._left_sidebar_max_width, plugin_max_width)
plugin_max_width = max(self._left_sidebar_min_width,
self.term_window.getmaxyx()[1] - 105)
plugin_max_width = min(self._left_sidebar_max_width,
plugin_max_width)
# Get the view
ret[p] = stats.get_plugin(p).get_stats_display(args=self.args, max_width=plugin_max_width)
ret[p] = stats.get_plugin(p).get_stats_display(args=self.args,
max_width=plugin_max_width)
return ret
@ -972,7 +1020,8 @@ class _GlancesCurses(object):
# Add the message
for y, m in enumerate(sentence_list):
popup.addnstr(2 + y, 2, m, len(m))
if len(m) > 0:
popup.addnstr(2 + y, 2, m, len(m))
if popup_type == 'info':
# Display the popup
@ -1000,7 +1049,7 @@ class _GlancesCurses(object):
logger.debug("User enters the following string: %s" % textbox.gather())
return textbox.gather()[:-1]
else:
logger.debug("User centers an empty string")
logger.debug("User enters an empty string")
return None
elif popup_type == 'yesno':
# # Create a sub-window for the text field
@ -1203,7 +1252,7 @@ class _GlancesCurses(object):
def wait(self, delay=100):
"""Wait delay in ms"""
curses.napms(100)
curses.napms(delay)
def get_stats_display_width(self, curse_msg, without_option=False):
"""Return the width of the formatted curses message."""

View File

@ -0,0 +1,800 @@
# -*- 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}/description' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_item_description,
)
router.add_api_route(
'/api/%s/{plugin}/{item}/unit' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_item_unit,
)
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))
)
else:
return ORJSONResponse(ret)
def _api_item_description(self, plugin, item):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/item description
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 description
ret = self.stats.get_plugin(plugin).get_item_info(item, 'description')
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cannot get %s description for plugin %s (%s)" % (item, plugin, str(e)),
)
else:
return ORJSONResponse(ret)
def _api_item_unit(self, plugin, item):
"""Glances API RESTful implementation.
Return the JSON representation of the couple plugin/item unit
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 unit
ret = self.stats.get_plugin(plugin).get_item_info(item, 'unit')
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cannot get %s unit for plugin %s (%s)" % (item, plugin, str(e)),
)
else:
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)),
)
else:
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))
else:
return ORJSONResponse(args_json)
def _api_config_section(self, section):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration section
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
config_dict = self.config.as_dict()
if section not in config_dict:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown configuration item %s" % section
)
try:
# Get the RAW value of the config' dict
ret_section = config_dict[section]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get config section %s (%s)" % (section, str(e))
)
return ORJSONResponse(ret_section)
def _api_config_section_item(self, section, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances configuration section/item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
config_dict = self.config.as_dict()
if section not in config_dict:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown configuration item %s" % section
)
try:
# Get the RAW value of the config' dict section
ret_section = config_dict[section]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get config section %s (%s)" % (section, str(e))
)
try:
# Get the RAW value of the config' dict item
ret_item = ret_section[item]
except Exception as e:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cannot get item %s in config section %s (%s)" % (item, section, str(e)),
)
return ORJSONResponse(ret_item)
def _api_args(self):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments
HTTP/200 if OK
HTTP/404 if others error
"""
try:
# Get the RAW value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = vars(self.args)
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get args (%s)" % str(e))
return ORJSONResponse(args_json)
def _api_args_item(self, item):
"""Glances API RESTful implementation.
Return the JSON representation of the Glances command line arguments item
HTTP/200 if OK
HTTP/400 if item is not found
HTTP/404 if others error
"""
if item not in self.args:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Unknown argument item %s" % item)
try:
# Get the RAW value of the args' dict
# Use vars to convert namespace to dict
# Source: https://docs.python.org/%s/library/functions.html#vars
args_json = vars(self.args)[item]
except Exception as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Cannot get args item (%s)" % str(e))
return ORJSONResponse(args_json)

View File

@ -34,7 +34,10 @@ class Sparkline(object):
"""Manage sparklines (see https://pypi.org/project/sparklines/)."""
def __init__(self, size, pre_char='[', post_char=']', empty_char=' ', with_text=True):
def __init__(self, size,
pre_char='[', post_char=']',
unit_char='%',
display_value=True):
# If the sparklines python module available ?
self.__available = sparklines_module
# Sparkline size
@ -44,8 +47,9 @@ class Sparkline(object):
# Char used for the decoration
self.__pre_char = pre_char
self.__post_char = post_char
self.__empty_char = empty_char
self.__with_text = with_text
self.__unit_char = unit_char
# Value should be displayed ?
self.__display_value = display_value
@property
def available(self):
@ -56,7 +60,7 @@ class Sparkline(object):
# Return the sparkline size, with or without decoration
if with_decoration:
return self.__size
if self.__with_text:
if self.__display_value:
return self.__size - 6
@property
@ -75,14 +79,19 @@ class Sparkline(object):
def post_char(self):
return self.__post_char
def get(self):
def get(self, overwrite=''):
"""Return the sparkline."""
ret = sparklines(self.percents, minimum=0, maximum=100)[0]
if self.__with_text:
if self.__display_value:
percents_without_none = [x for x in self.percents if x is not None]
if len(percents_without_none) > 0:
ret = '{}{:5.1f}%'.format(ret, percents_without_none[-1])
return nativestr(ret)
ret = '{}{:5.1f}{}'.format(ret,
percents_without_none[-1],
self.__unit_char)
ret = nativestr(ret)
if overwrite and len(overwrite) < len(ret) - 6:
ret = overwrite + ret[len(overwrite):]
return ret
def __str__(self):
"""Return the sparkline."""

View File

@ -2,7 +2,7 @@
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
@ -13,10 +13,12 @@ from pprint import pformat
import json
import time
from glances import __apiversion__
from glances.logger import logger
from glances.globals import iteritems
API_URL = "http://localhost:61208/api/3"
API_URL = "http://localhost:61208/api/{api_version}".format(api_version=__apiversion__)
APIDOC_HEADER = """\
.. _api:
@ -24,21 +26,33 @@ APIDOC_HEADER = """\
API (Restfull/JSON) documentation
=================================
This documentation describes the Glances API version {api_version} (Restfull/JSON) interface.
For Glances version 3, please have a look on:
``https://github.com/nicolargo/glances/blob/support/glancesv3/docs/api.rst``
Run the Glances API server
--------------------------
The Glances Restfull/API server could be ran using the following command line:
.. code-block:: bash
# glances -w --disable-webui
It is also ran automatically when Glances is started in Web server mode (-w).
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 +60,24 @@ 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 URL
---------------------
The API documentation is embeded in the server and 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 +95,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('')
@ -113,9 +141,30 @@ def print_plugin_description(plugin, stat):
description['description'][:-1]
if description['description'].endswith('.')
else description['description'],
description['unit'],
description['unit']
if 'unit' in description
else 'None'
)
)
if 'rate' in description and description['rate']:
print('* **{}**: {} (unit is *{}* per second)'.format(
field + '_rate_per_sec',
(description['description'][:-1]
if description['description'].endswith('.')
else description['description']) + ' per second',
description['unit']
if 'unit' in description
else 'None'
))
print('* **{}**: {} (unit is *{}*)'.format(
field + '_gauge',
(description['description'][:-1]
if description['description'].endswith('.')
else description['description']) + ' (cumulative)',
description['unit']
if 'unit' in description
else 'None'
))
print('')
else:
logger.error('No fields_description variable defined for plugin {}'.format(plugin))
@ -163,6 +212,45 @@ def print_all():
print('')
def print_top(stats):
time.sleep(1)
stats.update()
sub_title = 'GET top n items of a specific plugin'
print(sub_title)
print('-' * len(sub_title))
print('')
print('Get top 2 processes of the processlist plugin::')
print('')
print(' # curl {}/processlist/top/2'.format(API_URL))
print(indent_stat(stats.get_plugin('processlist').get_export()[:2]))
print('')
print('Note: Only work for plugin with a list of items')
print('')
def print_fields_info(stats):
sub_title = 'GET item description'
print(sub_title)
print('-' * len(sub_title))
print('Get item description (human readable) for a specific plugin/item::')
print('')
print(' # curl {}/diskio/read_bytes/description'.format(API_URL))
print(indent_stat(stats.get_plugin('diskio').get_item_info('read_bytes', 'description')))
print('')
print('Note: the description is defined in the fields_description variable of the plugin.')
print('')
sub_title = 'GET item unit'
print(sub_title)
print('-' * len(sub_title))
print('Get item unit for a specific plugin/item::')
print('')
print(' # curl {}/diskio/read_bytes/unit'.format(API_URL))
print(indent_stat(stats.get_plugin('diskio').get_item_info('read_bytes', 'unit')))
print('')
print('Note: the description is defined in the fields_description variable of the plugin.')
print('')
def print_history(stats):
time.sleep(1)
stats.update()
@ -248,6 +336,13 @@ class GlancesStdoutApiDoc(object):
# Get all stats
print_all()
# Get top stats (only for plugins with a list of items)
# Example for processlist plugin: get top 2 processes
print_top(stats)
# Fields description
print_fields_info(stats)
# History
print_history(stats)

View File

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

View File

@ -19,6 +19,15 @@ body {
display: table-cell;
text-align: right;
}
.width-60 {
width: 60px;
}
.width-80 {
width: 80px;
}
.width-100 {
width: 100px;
}
.plugin {
margin-bottom: 20px;
@ -60,6 +69,21 @@ body {
padding-left: 10px;
}
.separator {
overflow:hidden;
height:5px;
}
.separator:after {
content:'';
display:block;
margin:-25px auto 0;
width:100%;
height:24px;
border-radius:125px / 12px;
box-shadow:0 0 8px #555555;
}
/* Theme */
.title {
@ -110,8 +134,13 @@ body {
color: white;
font-weight: bold;
}
.error {
color: #EE6600;
font-weight: bold;
}
/* Plugins */
#processlist-plugin .table-cell {
padding: 0px 5px 0px 5px;
white-space: nowrap;

View File

@ -28,6 +28,7 @@
</div>
</div>
</div>
<div class="row separator" v-if="args.enable_separator"></div>
<div class="row">
<div class="hidden-xs hidden-sm hidden-md col-lg-6" v-if="!args.disable_quicklook">
<glances-plugin-quicklook :data="data"></glances-plugin-quicklook>
@ -58,82 +59,24 @@
<glances-plugin-load :data="data"></glances-plugin-load>
</div>
</div>
<div class="row separator" v-if="args.enable_separator"></div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-sm-6 sidebar" v-if="!args.disable_left_sidebar">
<div class="table">
<glances-plugin-network
id="plugin-network"
class="plugin table-row-group"
v-if="!args.disable_network"
:data="data"
></glances-plugin-network>
<glances-plugin-wifi
id="plugin-wifi"
class="plugin table-row-group"
v-if="!args.disable_wifi"
:data="data"
></glances-plugin-wifi>
<glances-plugin-connections
id="plugin-connections"
class="plugin table-row-group"
v-if="isLinux && !args.disable_connections"
:data="data"
></glances-plugin-connections>
<glances-plugin-ports
id="plugin-ports"
class="plugin table-row-group"
v-if="!args.disable_ports"
:data="data"
></glances-plugin-ports>
<glances-plugin-diskio
id="plugin-diskio"
class="plugin table-row-group"
v-if="!args.disable_diskio"
:data="data"
></glances-plugin-diskio>
<glances-plugin-fs
id="plugin-fs"
class="plugin table-row-group"
v-if="!args.disable_fs"
:data="data"
></glances-plugin-fs>
<glances-plugin-irq
id="plugin-irq"
class="plugin table-row-group"
v-if="!args.disable_irq"
:data="data"
></glances-plugin-irq>
<glances-plugin-smart
id="plugin-smart"
class="plugin table-row-group"
v-if="!args.disable_smart"
:data="data"
></glances-plugin-smart>
<glances-plugin-folders
id="plugin-folders"
class="plugin table-row-group"
v-if="!args.disable_folders"
:data="data"
></glances-plugin-folders>
<glances-plugin-raid
id="plugin-raid"
class="plugin table-row-group"
v-if="!args.raid"
:data="data"
></glances-plugin-raid>
<glances-plugin-sensors
id="plugin-sensors"
class="plugin table-row-group"
v-if="!args.disable_sensors"
:data="data"
></glances-plugin-sensors>
<glances-plugin-now
id="plugin-now"
class="plugin table-row-group"
:data="data"
></glances-plugin-now>
<!-- When they exist on the same node, v-if has a higher priority than v-for.
That means the v-if condition will not have access to variables from the
scope of the v-for -->
<template v-for="plugin in leftMenu">
<component
v-if="!args[`disable_${plugin}`]"
:is="`glances-plugin-${plugin}`"
:id="`plugin-${plugin}`"
class="plugin table-row-group"
:data="data">
</component>
</template>
</div>
</div>
<div class="col-sm-18">
@ -186,6 +129,8 @@ import GlancesPluginSystem from './components/plugin-system.vue';
import GlancesPluginUptime from './components/plugin-uptime.vue';
import GlancesPluginWifi from './components/plugin-wifi.vue';
import uiconfig from './uiconfig.json';
export default {
components: {
GlancesHelp,
@ -226,6 +171,9 @@ export default {
args() {
return this.store.args || {};
},
config() {
return this.store.config || {};
},
data() {
return this.store.data || {};
},
@ -242,6 +190,11 @@ export default {
const { data } = this;
const title = (data.stats && data.stats.system && data.stats.system.hostname) || '';
return title ? `${title} - Glances` : 'Glances';
},
leftMenu() {
return this.config.outputs.left_menu !== undefined
? this.config.outputs.left_menu.split(',')
: uiconfig.leftMenu;
}
},
watch: {
@ -338,8 +291,8 @@ export default {
this.store.args.disable_process = !this.store.args.disable_process;
});
// SLASH => Enable/disable short processes name
hotkeys('/', () => {
// S => Enable/disable short processes name
hotkeys('shift+S', () => {
this.store.args.process_short_name = !this.store.args.process_short_name;
});

View File

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

View File

@ -11,12 +11,14 @@
<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()">
{{ alert.name }}
<span v-show="!alert.ongoing"> {{ alert.state }} on </span>
<span :class="alert.state.toLowerCase()">
{{ alert.type }}
</span>
({{ $filters.number(alert.max, 1) }})
{{ alert.top }}
</div>
</div>
</div>
@ -41,14 +43,16 @@ export default {
alerts() {
return (this.stats || []).map((alertalertStats) => {
const alert = {};
alert.name = alertalertStats[3];
alert.level = alertalertStats[2];
alert.begin = alertalertStats[0] * 1000;
alert.end = alertalertStats[1] * 1000;
alert.ongoing = alertalertStats[1] == -1;
alert.min = alertalertStats[6];
alert.mean = alertalertStats[5];
alert.max = alertalertStats[4];
var tzoffset = new Date().getTimezoneOffset();
alert.state = alertalertStats.state;
alert.type = alertalertStats.type;
alert.begin = alertalertStats.begin * 1000 - tzoffset * 60 * 1000;
alert.end = alertalertStats.end * 1000 - tzoffset * 60 * 1000;
alert.ongoing = alertalertStats.end == -1;
alert.min = alertalertStats.min;
alert.avg = alertalertStats.avg;
alert.max = alertalertStats.max;
alert.top = alertalertStats.top.join(', ');
if (!alert.ongoing) {
const duration = alert.end - alert.begin;

View File

@ -60,16 +60,16 @@
{{ $filters.bytes(container.limit) }}
</div>
<div class="table-cell">
{{ $filters.bits(container.ior / container.io_time_since_update) }}
{{ $filters.bytes(container.io_rx) }}
</div>
<div class="table-cell">
{{ $filters.bits(container.iow / container.io_time_since_update) }}
{{ $filters.bytes(container.io_wx) }}
</div>
<div class="table-cell">
{{ $filters.bits(container.rx / container.net_time_since_update) }}
{{ $filters.bits(container.network_rx) }}
</div>
<div class="table-cell">
{{ $filters.bits(container.tx / container.net_time_since_update) }}
{{ $filters.bits(container.network_tx) }}
</div>
<div class="table-cell text-left">
{{ container.command }}
@ -107,25 +107,23 @@ export default {
},
containers() {
const { sorter } = this;
const containers = ((this.stats && this.stats.containers) || []).map(
const containers = (this.stats || []).map(
(containerData) => {
// prettier-ignore
return {
'id': containerData.Id,
'id': containerData.id,
'name': containerData.name,
'status': containerData.Status,
'uptime': containerData.Uptime,
'status': containerData.status,
'uptime': containerData.uptime,
'cpu_percent': containerData.cpu.total,
'memory_usage': containerData.memory.usage != undefined ? containerData.memory.usage : '?',
'limit': containerData.memory.limit != undefined ? containerData.memory.limit : '?',
'ior': containerData.io.ior != undefined ? containerData.io.ior : '?',
'iow': containerData.io.iow != undefined ? containerData.io.iow : '?',
'io_time_since_update': containerData.io.time_since_update,
'rx': containerData.network.rx != undefined ? containerData.network.rx : '?',
'tx': containerData.network.tx != undefined ? containerData.network.tx : '?',
'net_time_since_update': containerData.network.time_since_update,
'command': containerData.Command.join(' '),
'image': containerData.Image,
'io_rx': containerData.io_rx != undefined ? containerData.io_rx : '?',
'io_wx': containerData.io_wx != undefined ? containerData.io_wx : '?',
'network_rx': containerData.network_rx != undefined ? containerData.network_rx : '?',
'network_tx': containerData.network_tx != undefined ? containerData.network_tx : '?',
'command': containerData.command,
'image': containerData.image,
'engine': containerData.engine,
'pod_id': containerData.pod_id
};

View File

@ -47,7 +47,7 @@
<div class="table-cell">{{ nice }}%</div>
</div>
<!-- If no nice, display ctx_switches -->
<div class="table-row" v-if="nice == undefined && ctx_switches">
<div class="table-row" v-if="nice == undefined && ctx_switches != undefined">
<div class="table-cell text-left">ctx_sw:</div>
<div class="table-cell" :class="getDecoration('ctx_switches')">
{{ ctx_switches }}
@ -57,36 +57,38 @@
<div class="table-cell text-left">steal:</div>
<div class="table-cell" :class="getDecoration('steal')">{{ steal }}%</div>
</div>
<div class="table-row" v-if="!isLinux && syscalls">
<div class="table-row" v-if="!isLinux && syscalls != undefined">
<div class="table-cell text-left">syscal:</div>
<div class="table-cell">
{{ syscalls }}
</div>
<div class="table-cell">{{ syscalls }}</div>
</div>
</div>
</div>
<div class="hidden-xs hidden-sm hidden-md col-lg-8">
<div class="table">
<!-- If not already display instead of nice, then display ctx_switches -->
<div class="table-row" v-if="nice != undefined && ctx_switches">
<div class="table-row" v-if="nice != undefined && ctx_switches != undefined">
<div class="table-cell text-left">ctx_sw:</div>
<div class="table-cell" :class="getDecoration('ctx_switches')">
{{ ctx_switches }}
</div>
</div>
<!-- If not already display instead of irq, then display interrupts -->
<div class="table-row" v-if="irq != undefined && interrupts">
<div class="table-row" v-if="irq != undefined && interrupts != undefined">
<div class="table-cell text-left">inter:</div>
<div class="table-cell">
{{ interrupts }}
</div>
</div>
<div class="table-row" v-if="!isWindows && !isSunOS && soft_interrupts">
<div class="table-row" v-if="!isWindows && !isSunOS && soft_interrupts != undefined">
<div class="table-cell text-left">sw_int:</div>
<div class="table-cell">
{{ soft_interrupts }}
</div>
</div>
<div class="table-row" v-if="isLinux && guest != undefined">
<div class="table-cell text-left">guest:</div>
<div class="table-cell">{{ guest }}%</div>
</div>
</div>
</div>
</div>
@ -143,6 +145,9 @@ export default {
steal() {
return this.stats.steal;
},
guest() {
return this.stats.guest;
},
ctx_switches() {
const { stats } = this;
return stats.ctx_switches

View File

@ -9,6 +9,9 @@
<div class="table-cell text-left">{{ folder.path }}</div>
<div class="table-cell"></div>
<div class="table-cell" :class="getDecoration(folder)">
<span v-if="folder.errno > 0" class="visible-lg-inline">
?
</span>
{{ $filters.bytes(folder.size) }}
</div>
</div>
@ -31,6 +34,7 @@ export default {
return {
path: folderData['path'],
size: folderData['size'],
errno: folderData['errno'],
careful: folderData['careful'],
warning: folderData['warning'],
critical: folderData['critical']
@ -40,8 +44,8 @@ export default {
},
methods: {
getDecoration(folder) {
if (!Number.isInteger(folder.size)) {
return;
if (folder.errno > 0) {
return 'error';
}
if (folder.critical !== null && folder.size > folder.critical * 1000000) {
return 'critical';

View File

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

View File

@ -19,7 +19,7 @@
<div class="table-cell" v-if="mean.mem == null">N/A</div>
</div>
<div class="table-row" v-if="args.meangpu || gpus.length === 1">
<div class="table-cell text-left">temperature::</div>
<div class="table-cell text-left">temperature:</div>
<div
class="table-cell"
:class="getMeanDecoration('temperature')"

View File

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

View File

@ -3,112 +3,88 @@
<section id="processlist-plugin" class="plugin">
<div class="table">
<div class="table-row">
<div
class="table-cell"
:class="['sortable', sorter.column === 'cpu_percent' && 'sort']"
@click="$emit('update:sorter', 'cpu_percent')"
>
<div class="table-cell width-60" :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-60" :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-80 hidden-xs hidden-sm">VIRT</div>
<div class="table-cell width-80 hidden-xs hidden-sm">RES</div>
<div class="table-cell width-80">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-80 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-60">NI</div>
<div class="table-cell width-60">S</div>
<div v-show="ioReadWritePresent" class="table-cell width-80 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-80 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-60" :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-60" :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-80">
{{ $filters.bytes(process.memvirt) }}
</div>
<div class="table-cell hidden-xs hidden-sm">
<div class="table-cell width-80">
{{ $filters.bytes(process.memres) }}
</div>
<div class="table-cell">
<div class="table-cell width-80">
{{ 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-80 hidden-xs hidden-sm" v-if="process.timeplus == '?'">?</div>
<div class="table-cell width-80 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-60" :class="{ nice: process.isNice }">
{{ $filters.exclamation(process.nice) }}
</div>
<div class="table-cell" :class="{ status: process.status == 'R' }">
<div class="table-cell width-60" :class="{ status: process.status == 'R' }">
{{ process.status }}
</div>
<div class="table-cell hidden-xs hidden-sm" v-show="ioReadWritePresent">
<div class="table-cell width-80 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-80 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,12 @@ 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;
}
if (isWindows && process.username !== null) {
process.username = last(process.username.split('\\'));
}
process.timeplus = '?';
@ -202,14 +182,10 @@ export default {
process.cmdline = process.cmdline.join(' ').replace(/\n/g, ' ');
}
if (process.cmdline === null) {
if (process.cmdline === null || process.cmdline.length === 0) {
process.cmdline = process.name;
}
if (isWindows && process.username !== null) {
process.username = last(process.username.split('\\'));
}
return process;
});

View File

@ -42,41 +42,23 @@
<div class="table-cell">{{ percpu.total }}%</div>
</div>
</template>
<div class="table-row">
<div class="table-cell text-left">MEM</div>
<div class="table-row" v-for="(key) in stats_list_after_cpu">
<div class="table-cell text-left">{{ key.toUpperCase() }}</div>
<div class="table-cell">
<div class="progress">
<div
:class="`progress-bar progress-bar-${getDecoration('mem')}`"
:class="`progress-bar progress-bar-${getDecoration(key)}`"
role="progressbar"
:aria-valuenow="mem"
:aria-valuenow="stats[key]"
aria-valuemin="0"
aria-valuemax="100"
:style="`width: ${mem}%;`"
:style="`width: ${stats[key]}%;`"
>
&nbsp;
</div>
</div>
</div>
<div class="table-cell">{{ mem }}%</div>
</div>
<div class="table-row">
<div class="table-cell text-left">SWAP</div>
<div class="table-cell">
<div class="progress">
<div
:class="`progress-bar progress-bar-${getDecoration('swap')}`"
role="progressbar"
:aria-valuenow="swap"
aria-valuemin="0"
aria-valuemax="100"
:style="`width: ${swap}%;`"
>
&nbsp;
</div>
</div>
</div>
<div class="table-cell">{{ swap }}%</div>
<div class="table-cell">{{ stats[key] }}%</div>
</div>
</div>
</section>
@ -106,9 +88,6 @@ export default {
view() {
return this.data.views['quicklook'];
},
mem() {
return this.stats.mem;
},
cpu() {
return this.stats.cpu;
},
@ -121,15 +100,15 @@ export default {
cpu_hz() {
return this.stats.cpu_hz;
},
swap() {
return this.stats.swap;
},
percpus() {
return this.stats.percpu.map(({ cpu_number: number, total }) => ({
number,
total
}));
}
},
stats_list_after_cpu() {
return this.view.list.filter((key) => !key.includes('cpu'));
},
},
methods: {
getDecoration(value) {

View File

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

View File

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

View File

@ -0,0 +1,16 @@
{
"leftMenu": [
"network",
"wifi",
"connections",
"ports",
"diskio",
"fs",
"irq",
"folders",
"raid",
"smart",
"sensors",
"now"
]
}

View File

@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "static",
"dependencies": {
"bootstrap": "^3.4.1",
"favico.js": "^0.3.10",
@ -109,18 +110,18 @@
}
},
"node_modules/@eslint/js": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz",
"integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz",
"integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
@ -1605,9 +1606,9 @@
}
},
"node_modules/del": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-7.0.0.tgz",
"integrity": "sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/del/-/del-7.1.0.tgz",
"integrity": "sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==",
"dev": true,
"dependencies": {
"globby": "^13.1.2",
@ -1876,16 +1877,16 @@
}
},
"node_modules/eslint": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz",
"integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
"integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "^8.47.0",
"@humanwhocodes/config-array": "^0.11.10",
"@eslint/js": "8.50.0",
"@humanwhocodes/config-array": "^0.11.11",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.12.4",
@ -2335,9 +2336,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true,
"funding": [
{
@ -3953,9 +3954,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
@ -4497,9 +4498,9 @@
}
},
"node_modules/sass": {
"version": "1.65.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.65.1.tgz",
"integrity": "sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA==",
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.68.0.tgz",
"integrity": "sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@ -5797,15 +5798,15 @@
}
},
"@eslint/js": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz",
"integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.50.0.tgz",
"integrity": "sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==",
"dev": true
},
"@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
"integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
@ -7008,9 +7009,9 @@
"dev": true
},
"del": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-7.0.0.tgz",
"integrity": "sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/del/-/del-7.1.0.tgz",
"integrity": "sha512-v2KyNk7efxhlyHpjEvfyxaAihKKK0nWCuf6ZtqZcFFpQRG0bJ12Qsr0RpvsICMjAAZ8DOVCxrlqpxISlMHC4Kg==",
"dev": true,
"requires": {
"globby": "^13.1.2",
@ -7209,16 +7210,16 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"eslint": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz",
"integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==",
"version": "8.50.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
"integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.2",
"@eslint/js": "^8.47.0",
"@humanwhocodes/config-array": "^0.11.10",
"@eslint/js": "8.50.0",
"@humanwhocodes/config-array": "^0.11.11",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.12.4",
@ -7583,9 +7584,9 @@
"dev": true
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true
},
"forwarded": {
@ -8755,9 +8756,9 @@
}
},
"postcss": {
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"requires": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
@ -9124,9 +9125,9 @@
}
},
"sass": {
"version": "1.65.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.65.1.tgz",
"integrity": "sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA==",
"version": "1.68.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.68.0.tgz",
"integrity": "sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,328 @@
# -*- 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
# {
# "begin": "begin",
# "end": "end",
# "state": "WARNING|CRITICAL",
# "type": "CPU|LOAD|MEM",
# "max": MAX,
# "avg": AVG,
# "min": MIN,
# "sum": SUM,
# "count": COUNT,
# "top": [top3 process list],
# "desc": "Processes description",
# "sort": "top sort key"
# }
# 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 = {
'begin': {
'description': 'Begin timestamp of the event',
'unit': 'timestamp',
},
'end': {
'description': 'End timestamp of the event (or -1 if ongoing)',
'unit': 'timestamp',
},
'state': {
'description': 'State of the event (WARNING|CRITICAL)',
'unit': 'string',
},
'type': {
'description': 'Type of the event (CPU|LOAD|MEM)',
'unit': 'string',
},
'max': {
'description': 'Maximum value during the event period',
'unit': 'float',
},
'avg': {
'description': 'Average value during the event period',
'unit': 'float',
},
'min': {
'description': 'Minimum value during the event period',
'unit': 'float',
},
'sum': {
'description': 'Sum of the values during the event period',
'unit': 'float',
},
'count': {
'description': 'Number of values during the event period',
'unit': 'int',
},
'top': {
'description': 'Top 3 processes name during the event period',
'unit': 'list',
},
'desc': {
'description': 'Description of the event',
'unit': 'string',
},
'sort': {
'description': 'Sort key of the top processes',
'unit': 'string',
},
}
# 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=[],
fields_description=fields_description
)
# 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['begin'],
tz=pytz.timezone(tzname[0] if tzname[0] else 'UTC')))
ret.append(self.curse_add_line(msg))
# Duration
if alert['end'] > 0:
# If finished display duration
msg = ' ({})'.format(datetime.fromtimestamp(alert['end']) - datetime.fromtimestamp(alert['begin']))
else:
msg = ' (ongoing)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_add_line(" - "))
# Infos
if alert['end'] > 0:
# If finished do not display status
msg = '{} on {}'.format(alert['state'], alert['type'])
ret.append(self.curse_add_line(msg))
else:
msg = str(alert['type'])
ret.append(self.curse_add_line(msg, decoration=alert['state']))
# Min / Mean / Max
if self.approx_equal(alert['min'], alert['max'], tolerance=0.1):
msg = ' ({:.1f})'.format(alert['avg'])
else:
msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert['min'],
alert['avg'],
alert['max'])
ret.append(self.curse_add_line(msg))
# Top processes
top_process = ', '.join(alert['top'])
if top_process != '':
msg = ': {}'.format(top_process)
ret.append(self.curse_add_line(msg))
return ret
def approx_equal(self, a, b, tolerance=0.0):
"""Compare a with b using the tolerance (if numerical)."""
if str(int(a)).isdigit() and str(int(b)).isdigit():
return abs(a - b) <= max(abs(a), abs(b)) * tolerance
else:
return a == b

View File

@ -1,244 +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.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'
def update(self):
"""Nothing to do here. Just return the global glances_log."""
# Set the stats to the glances_events
self.stats = glances_events.get()
# Define the global message thanks to the current thresholds
# and the decision tree
# !!! Call directly in the msg_curse function
# global_message()
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Build the string message
# Header
ret.append(self.curse_add_line(global_message(), "TITLE"))
# Loop over alerts
for alert in self.stats:
# New line
ret.append(self.curse_new_line())
# Start
msg = str(datetime.fromtimestamp(alert[0]))
ret.append(self.curse_add_line(msg))
# Duration
if alert[1] > 0:
# If finished display duration
msg = ' ({})'.format(datetime.fromtimestamp(alert[1]) - datetime.fromtimestamp(alert[0]))
else:
msg = ' (ongoing)'
ret.append(self.curse_add_line(msg))
ret.append(self.curse_add_line(" - "))
# Infos
if alert[1] > 0:
# If finished do not display status
msg = '{} on {}'.format(alert[2], alert[3])
ret.append(self.curse_add_line(msg))
else:
msg = str(alert[3])
ret.append(self.curse_add_line(msg, decoration=alert[2]))
# Min / Mean / Max
if self.approx_equal(alert[6], alert[4], tolerance=0.1):
msg = ' ({:.1f})'.format(alert[5])
else:
msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert[6], alert[5], alert[4])
ret.append(self.curse_add_line(msg))
# Top processes
top_process = ', '.join([p['name'] for p in alert[9]])
if top_process != '':
msg = ': {}'.format(top_process)
ret.append(self.curse_add_line(msg))
return ret
def approx_equal(self, a, b, tolerance=0.0):
"""Compare a with b using the tolerance (if numerical)."""
if str(int(a)).isdigit() and str(int(b)).isdigit():
return abs(a - b) <= max(abs(a), abs(b)) * tolerance
else:
return a == b

View File

@ -0,0 +1,163 @@
# -*- 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
# 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 = {
'name': {
'description': 'AMP name.'
},
'result': {
'description': 'AMP result (a string).'
},
'refresh': {
'description': 'AMP refresh interval.',
'unit': 'second'
},
'timer': {
'description': 'Time until next refresh.',
'unit': 'second'
},
'count': {
'description': 'Number of matching processes.',
'unit': 'number'
},
'countmin': {
'description': 'Minimum number of matching processes.',
'unit': 'number'
},
'countmax': {
'description': 'Maximum number of matching processes.',
'unit': 'number'
},
}
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=[],
fields_description=fields_description
)
self.args = args
self.config = config
# We want to display the stat in the curse interface
self.display_curse = True
# Init the list of AMP (classes define in the glances/amps_list.py script)
self.glances_amps = glancesAmpsList(self.args, self.config)
def get_key(self):
"""Return the key of the list."""
return 'name'
@GlancesPluginModel._check_decorator
@GlancesPluginModel._log_result_decorator
def update(self):
"""Update the AMP list."""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
for k, v in iteritems(self.glances_amps.update()):
stats.append(
{
'key': self.get_key(),
'name': v.NAME,
'result': v.result(),
'refresh': v.refresh(),
'timer': v.time_until_refresh(),
'count': v.count(),
'countmin': v.count_min(),
'countmax': v.count_max(),
'regex': v.regex() is not None,
},
)
else:
# Not available in SNMP mode
pass
# Update the stats
self.stats = stats
return self.stats
def get_alert(self, nbprocess=0, countmin=None, countmax=None, header="", log=False):
"""Return the alert status relative to the process number."""
if nbprocess is None:
return 'OK'
if countmin is None:
countmin = nbprocess
if countmax is None:
countmax = nbprocess
if nbprocess > 0:
if int(countmin) <= int(nbprocess) <= int(countmax):
return 'OK'
else:
return 'WARNING'
else:
if int(countmin) == 0:
return 'OK'
else:
return 'CRITICAL'
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
# Only process if stats exist and display plugin enable...
ret = []
if not self.stats or args.disable_process or self.is_disabled():
return ret
# Build the string message
for m in self.stats:
# Only display AMP if a result exist
if m['result'] is None:
continue
# Display AMP
first_column = '{}'.format(m['name'])
first_column_style = self.get_alert(m['count'], m['countmin'], m['countmax'])
second_column = '{}'.format(m['count'] if m['regex'] else '')
for line in m['result'].split('\n'):
# Display first column with the process name...
msg = '{:<16} '.format(first_column)
ret.append(self.curse_add_line(msg, first_column_style))
# ... and second column with the number of matching processes...
msg = '{:<4} '.format(second_column)
ret.append(self.curse_add_line(msg))
# ... only on the first line
first_column = second_column = ''
# Display AMP result in the third column
ret.append(self.curse_add_line(line, splittable=True))
ret.append(self.curse_new_line())
# Delete the last empty line
try:
ret.pop()
except IndexError:
pass
return ret

View File

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

View File

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

View File

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

View File

@ -0,0 +1,222 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 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
# 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 = {
'LISTEN': {
'description': 'Number of TCP connections in LISTEN state',
'unit': 'number',
},
'ESTABLISHED': {
'description': 'Number of TCP connections in ESTABLISHED state',
'unit': 'number',
},
'SYN_SENT': {
'description': 'Number of TCP connections in SYN_SENT state',
'unit': 'number',
},
'SYN_RECV': {
'description': 'Number of TCP connections in SYN_RECV state',
'unit': 'number',
},
'initiated': {
'description': 'Number of TCP connections initiated',
'unit': 'number',
},
'terminated': {
'description': 'Number of TCP connections terminated',
'unit': 'number',
},
'nf_conntrack_count': {
'description': 'Number of tracked connections',
'unit': 'number',
},
'nf_conntrack_max': {
'description': 'Maximum number of tracked connections',
'unit': 'number',
},
'nf_conntrack_percent': {
'description': 'Percentage of tracked connections',
'unit': 'percent',
},
}
# 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},
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 connections stats using the input method.
Stats is a dict
"""
# Init new stats
stats = self.get_init_value()
if self.input_method == 'local':
# Update stats using the PSUtils lib
# Grab network interface stat using the psutil net_connections method
if stats['net_connections_enabled']:
try:
net_connections = psutil.net_connections(kind="tcp")
except Exception as e:
logger.warning('Can not get network connections stats ({})'.format(e))
logger.info('Disable connections stats')
stats['net_connections_enabled'] = False
self.stats = stats
return self.stats
for s in self.status_list:
stats[s] = len([c for c in net_connections if c.status == s])
initiated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
initiated += stats[s]
stats['initiated'] = initiated
terminated = 0
for s in self.initiated_states:
stats[s] = len([c for c in net_connections if c.status == s])
terminated += stats[s]
stats['terminated'] = terminated
if stats['nf_conntrack_enabled']:
# Grab connections track directly from the /proc file
for i in self.conntrack:
try:
with open(self.conntrack[i], 'r') as f:
stats[i] = float(f.readline().rstrip("\n"))
except (IOError, FileNotFoundError) as e:
logger.warning('Can not get network connections track ({})'.format(e))
logger.info('Disable connections track')
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
if 'nf_conntrack_max' in stats and 'nf_conntrack_count' in stats:
stats['nf_conntrack_percent'] = stats['nf_conntrack_count'] * 100 / stats['nf_conntrack_max']
else:
stats['nf_conntrack_enabled'] = False
self.stats = stats
return self.stats
elif self.input_method == 'snmp':
# Update stats using SNMP
pass
# Update the stats
self.stats = stats
return self.stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specific information
try:
# Alert and log
if self.stats['nf_conntrack_enabled']:
self.views['nf_conntrack_percent']['decoration'] = self.get_alert(header='nf_conntrack_percent')
except KeyError:
# try/except mandatory for Windows compatibility (no conntrack stats)
pass
def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
# Only process if stats exist and display plugin enable...
if not self.stats or self.is_disabled():
return ret
# Header
if self.stats['net_connections_enabled'] or self.stats['nf_conntrack_enabled']:
msg = '{}'.format('TCP CONNECTIONS')
ret.append(self.curse_add_line(msg, "TITLE"))
# Connections status
if self.stats['net_connections_enabled']:
for s in [psutil.CONN_LISTEN, 'initiated', psutil.CONN_ESTABLISHED, 'terminated']:
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(self.stats[s], width=max_width - len(s) + 2)
ret.append(self.curse_add_line(msg))
# Connections track
if (
self.stats['nf_conntrack_enabled']
and 'nf_conntrack_count' in self.stats
and 'nf_conntrack_max' in self.stats
):
s = 'Tracked'
ret.append(self.curse_new_line())
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
ret.append(self.curse_add_line(msg))
msg = '{:>{width}}'.format(
'{:0.0f}/{:0.0f}'.format(self.stats['nf_conntrack_count'], self.stats['nf_conntrack_max']),
width=max_width - len(s) + 2,
)
ret.append(self.curse_add_line(msg, self.get_views(key='nf_conntrack_percent', option='decoration')))
return ret

View File

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

View File

@ -0,0 +1,484 @@
# -*- 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
# 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 = {
'name': {
'description': 'Container name',
},
'id': {
'description': 'Container ID',
},
'image': {
'description': 'Container image',
},
'status': {
'description': 'Container status',
},
'created': {
'description': 'Container creation date',
},
'command': {
'description': 'Container command',
},
'cpu_percent': {
'description': 'Container CPU consumption',
'unit': 'percent',
},
'memory_usage': {
'description': 'Container memory usage',
'unit': 'byte',
},
'io_rx': {
'description': 'Container IO bytes read rate',
'unit': 'bytepersecond',
},
'io_wx': {
'description': 'Container IO bytes write rate',
'unit': 'bytepersecond',
},
'network_rx': {
'description': 'Container network RX bitrate',
'unit': 'bitpersecond',
},
'network_tx': {
'description': 'Container network TX bitrate',
'unit': 'bitpersecond',
},
'uptime': {
'description': 'Container uptime',
},
'engine': {
'description': 'Container engine (Docker and Podman are currently supported)',
},
'pod_name': {
'description': 'Pod name (only with Podman)',
},
'pod_id': {
'description': 'Pod ID (only with Podman)',
},
}
# 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,
fields_description=fields_description
)
# 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)
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 = 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 update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
if not self.stats:
return False
# Add specifics information
# Alert
for i in self.stats:
# 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 len(self.stats) == 0 or self.is_disabled():
return ret
show_pod_name = False
if any(ct.get("pod_name") for ct in self.stats):
show_pod_name = True
show_engine_name = False
if len(set(ct["engine"] for ct in self.stats)) > 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))
ret.append(self.curse_add_line(msg))
msg = ' sorted by {}'.format(sort_for_human[self.sort_key])
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, 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:
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_rx'])) + 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_wx'])) + 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'] * 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'] * 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(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,
sorted_by=sort_by,
sorted_by_secondary=sort_by_secondary,
# Reverse for all but name
reverse=glances_processes.sort_key != 'name',
)
# Return the main sort key and the sorted stats
return sort_by, stats

View File

@ -295,28 +295,28 @@ class DockerContainersExtension:
# Export name
'name': nativestr(container.name),
# Container Id
'Id': container.id,
'id': container.id,
# Container Status (from attrs)
'Status': container.attrs['State']['Status'],
'Created': container.attrs['Created'],
'Command': [],
'status': container.attrs['State']['Status'],
'created': container.attrs['Created'],
'command': [],
}
# Container Image
try:
# API fails on Unraid - See issue 2233
stats['Image'] = container.image.tags
stats['image'] = ','.join(container.image.tags if container.image.tags else []),
except requests.exceptions.HTTPError:
stats['Image'] = '-'
stats['image'] = ''
if container.attrs['Config'].get('Entrypoint', None):
stats['Command'].extend(container.attrs['Config'].get('Entrypoint', []))
stats['command'].extend(container.attrs['Config'].get('Entrypoint', []))
if container.attrs['Config'].get('Cmd', None):
stats['Command'].extend(container.attrs['Config'].get('Cmd', []))
if not stats['Command']:
stats['Command'] = None
stats['command'].extend(container.attrs['Config'].get('Cmd', []))
if not stats['command']:
stats['command'] = None
if stats['Status'] in self.CONTAINER_ACTIVE_STATUS:
if stats['status'] in self.CONTAINER_ACTIVE_STATUS:
started_at = container.attrs['State']['StartedAt']
stats_fetcher = self.stats_fetchers[container.id]
activity_stats = stats_fetcher.activity_stats
@ -327,22 +327,25 @@ class DockerContainersExtension:
stats['memory_usage'] = stats["memory"].get('usage')
if stats['memory'].get('cache') is not None:
stats['memory_usage'] -= stats['memory']['cache']
stats['io_r'] = stats['io'].get('ior')
stats['io_w'] = stats['io'].get('iow')
stats['network_rx'] = stats['network'].get('rx')
stats['network_tx'] = stats['network'].get('tx')
stats['Uptime'] = pretty_date(parser.parse(started_at).astimezone(tz.tzlocal()).replace(tzinfo=None))
if 'time_since_update' in stats['io']:
stats['io_rx'] = stats['io'].get('ior') // stats['io'].get('time_since_update')
stats['io_wx'] = stats['io'].get('iow') // stats['io'].get('time_since_update')
if 'time_since_update' in stats['network']:
stats['network_rx'] = stats['network'].get('rx') // stats['network'].get('time_since_update')
stats['network_tx'] = stats['network'].get('tx') // stats['network'].get('time_since_update')
stats['uptime'] = pretty_date(parser.parse(started_at).astimezone(tz.tzlocal()).replace(tzinfo=None))
stats['command'] = ' '.join(stats['command'])
else:
stats['io'] = {}
stats['cpu'] = {}
stats['memory'] = {}
stats['network'] = {}
stats['io_r'] = None
stats['io_w'] = None
stats['io_rx'] = None
stats['io_wx'] = None
stats['cpu_percent'] = None
stats['memory_percent'] = None
stats['network_rx'] = None
stats['network_tx'] = None
stats['Uptime'] = None
stats['uptime'] = None
return stats

View File

@ -71,7 +71,8 @@ class PodmanContainerStatsFetcher:
ior = float(api_stats["BlockInput"])
iow = float(api_stats["BlockOutput"])
# Hardcode `time_since_update` to 1 as podman already sends the calculated rate
# Hardcode `time_since_update` to 1 as podman
# already sends the calculated rate per second
result_stats = {
"cpu": {"total": cpu_usage},
"memory": {"usage": mem_usage, "limit": mem_limit},
@ -290,9 +291,9 @@ class PodmanContainersExtension:
pod_stats = self.pods_stats_fetcher.activity_stats
for stats in container_stats:
if stats["Id"][:12] in pod_stats:
stats["pod_name"] = pod_stats[stats["Id"][:12]]["name"]
stats["pod_id"] = pod_stats[stats["Id"][:12]]["pod_id"]
if stats["id"][:12] in pod_stats:
stats["pod_name"] = pod_stats[stats["id"][:12]]["name"]
stats["pod_id"] = pod_stats[stats["id"][:12]]["pod_id"]
return version_stats, container_stats
@ -308,16 +309,16 @@ class PodmanContainersExtension:
# Export name
'name': nativestr(container.name),
# Container Id
'Id': container.id,
'id': container.id,
# Container Image
'Image': str(container.image.tags),
'image': ','.join(container.image.tags if container.image.tags else []),
# Container Status (from attrs)
'Status': container.attrs['State'],
'Created': container.attrs['Created'],
'Command': container.attrs.get('Command') or [],
'status': container.attrs['State'],
'created': container.attrs['Created'],
'command': container.attrs.get('Command') or [],
}
if stats['Status'] in self.CONTAINER_ACTIVE_STATUS:
if stats['status'] in self.CONTAINER_ACTIVE_STATUS:
started_at = datetime.fromtimestamp(container.attrs['StartedAt'])
stats_fetcher = self.container_stats_fetchers[container.id]
activity_stats = stats_fetcher.activity_stats
@ -328,22 +329,23 @@ class PodmanContainersExtension:
stats['memory_usage'] = stats["memory"].get('usage')
if stats['memory'].get('cache') is not None:
stats['memory_usage'] -= stats['memory']['cache']
stats['io_r'] = stats['io'].get('ior')
stats['io_w'] = stats['io'].get('iow')
stats['network_rx'] = stats['network'].get('rx')
stats['network_tx'] = stats['network'].get('tx')
stats['Uptime'] = pretty_date(started_at)
stats['io_rx'] = stats['io'].get('ior') // stats['io'].get('time_since_update')
stats['io_wx'] = stats['io'].get('iow') // stats['io'].get('time_since_update')
stats['network_rx'] = stats['network'].get('rx') // stats['network'].get('time_since_update')
stats['network_tx'] = stats['network'].get('tx') // stats['network'].get('time_since_update')
stats['uptime'] = pretty_date(started_at)
stats['command'] = ' '.join(stats['command'])
else:
stats['io'] = {}
stats['cpu'] = {}
stats['memory'] = {}
stats['network'] = {}
stats['io_r'] = None
stats['io_w'] = None
stats['io_rx'] = None
stats['io_wx'] = None
stats['cpu_percent'] = None
stats['memory_percent'] = None
stats['network_rx'] = None
stats['network_tx'] = None
stats['Uptime'] = None
stats['uptime'] = None
return stats

View File

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

View File

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

View File

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

View File

@ -0,0 +1,383 @@
# -*- 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.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: if True then compute and add *_gauge and *_rate_per_is fields
# 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',
},
'guest': {
'description': '*(Linux)*: time spent running a virtual CPU for guest operating \
systems under the control of the Linux kernel.',
'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
@GlancesPluginModel._manage_rate
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)
# Filter stats to keep only the fields we want (define in fields_description)
# It will also convert psutil objects to a standard Python dict
stats.update(self.filter_stats(cpu_times_percent))
# 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()
# Filter stats to keep only the fields we want (define in fields_description)
# It will also convert psutil objects to a standard Python dict
stats.update(self.filter_stats(cpu_stats))
# Core number is needed to compute the CTX switch limit
stats['cpucore'] = self.nb_log_core
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', 'guest',
'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 = '{:8}'.format('CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
# 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 or guest)
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=' '))
if not LINUX:
# syscalls: number of system calls since boot. Always set to 0 on Linux. (do not display)
ret.extend(self.curse_add_stat('syscalls', width=15, header=' '))
else:
# So instead on Linux we display the guest CPU usage (see #2667)
# guest: time spent running a virtual CPU for guest operating systems under
ret.extend(self.curse_add_stat('guest', width=14, header=' '))
# Return the message with decoration
return ret

View File

@ -1,389 +0,0 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
"""CPU plugin."""
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 in UI
# unit: unit type
# rate: is it a rate ? If yes generate metadata with _gauge and _rate_per_sec
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
# if a key field is defined, the value is used as a key in the stats dict
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._manage_rate
@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()
# Total is shared with perCPU plugin
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()
for stat in cpu_stats._fields:
stats[stat] = getattr(cpu_stats, stat)
# Core number is needed to compute the CTX switch limit
stats['cpucore'] = self.nb_log_core
return stats
def update_snmp(self):
"""Update CPU stats using SNMP."""
# Init new stats
stats = self.get_init_value()
# Update stats using SNMP
if self.short_system_name in ('windows', 'esxi'):
# Windows or VMWare ESXi
# You can find the CPU utilization of windows system by querying the oid
# Give also the number of core (number of element in the table)
try:
cpu_stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True)
except KeyError:
self.reset()
# Iter through CPU and compute the idle CPU stats
stats['nb_log_core'] = 0
stats['idle'] = 0
for c in cpu_stats:
if c.startswith('percent'):
stats['idle'] += float(cpu_stats['percent.3'])
stats['nb_log_core'] += 1
if stats['nb_log_core'] > 0:
stats['idle'] = stats['idle'] / stats['nb_log_core']
stats['idle'] = 100 - stats['idle']
stats['total'] = 100 - stats['idle']
else:
# Default behavior
try:
stats = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name])
except KeyError:
stats = self.get_stats_snmp(snmp_oid=snmp_oid['default'])
if stats['idle'] == '':
self.reset()
return self.stats
# Convert SNMP stats to float
for key in iterkeys(stats):
stats[key] = float(stats[key])
stats['total'] = 100 - stats['idle']
return stats
def update_views(self):
"""Update stats views."""
# Call the father's method
super(PluginModel, self).update_views()
# Add specifics information
# Alert and log
for key in ['user', 'system', 'iowait', 'dpc', 'total']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert_log(self.stats[key], header=key)
# Alert only
for key in ['steal']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(self.stats[key], header=key)
# Alert only but depend on Core number
for key in ['ctx_switches']:
if key in self.stats:
self.views[key]['decoration'] = self.get_alert(
self.stats[key], maximum=100 * self.stats['cpucore'], header=key
)
# Optional
for key in ['nice', 'irq', 'idle', 'steal', 'ctx_switches', 'interrupts', 'soft_interrupts', 'syscalls']:
if key in self.stats:
self.views[key]['optional'] = True
def msg_curse(self, args=None, max_width=None):
"""Return the list to display in the UI."""
# Init the return message
ret = []
# Only process if stats exist and plugin not disable
if not self.stats or self.args.percpu or self.is_disabled():
return ret
# Some tag to enable/disable stats (example: idle_tag triggered on Windows OS)
idle_tag = 'user' not in self.stats
# First line
# Total + (idle) + ctx_sw
msg = '{}'.format('CPU')
ret.append(self.curse_add_line(msg, "TITLE"))
trend_user = self.get_trend('user')
trend_system = self.get_trend('system')
if trend_user is None or trend_user is None:
trend_cpu = None
else:
trend_cpu = trend_user + trend_system
msg = ' {:4}'.format(self.trend_msg(trend_cpu))
ret.append(self.curse_add_line(msg))
# Total CPU usage
msg = '{:5.1f}%'.format(self.stats['total'])
ret.append(self.curse_add_line(msg, self.get_views(key='total', option='decoration')))
# Idle CPU
if 'idle' in self.stats and not idle_tag:
msg = ' {:8}'.format('idle')
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
msg = '{:4.1f}%'.format(self.stats['idle'])
ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
# ctx_switches
# On WINDOWS/SUNOS the ctx_switches is displayed in the third line
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Second line
# user|idle + irq + interrupts
ret.append(self.curse_new_line())
# User CPU
if not idle_tag:
ret.extend(self.curse_add_stat('user', width=15))
elif 'idle' in self.stats:
ret.extend(self.curse_add_stat('idle', width=15))
# IRQ CPU
ret.extend(self.curse_add_stat('irq', width=14, header=' '))
# interrupts
ret.extend(self.curse_add_stat('interrupts', width=15, header=' '))
# Third line
# system|core + nice + sw_int
ret.append(self.curse_new_line())
# System CPU
if not idle_tag:
ret.extend(self.curse_add_stat('system', width=15))
else:
ret.extend(self.curse_add_stat('core', width=15))
# Nice CPU
ret.extend(self.curse_add_stat('nice', width=14, header=' '))
# soft_interrupts
if not WINDOWS and not SUNOS:
ret.extend(self.curse_add_stat('soft_interrupts', width=15, header=' '))
else:
ret.extend(self.curse_add_stat('ctx_switches', width=15, header=' '))
# Fourth line
# iowait + steal + syscalls
ret.append(self.curse_new_line())
if 'iowait' in self.stats:
# IOWait CPU
ret.extend(self.curse_add_stat('iowait', width=15))
elif 'dpc' in self.stats:
# DPC CPU
ret.extend(self.curse_add_stat('dpc', width=15))
# Steal CPU usage
ret.extend(self.curse_add_stat('steal', width=14, header=' '))
# syscalls: number of system calls since boot. Always set to 0 on Linux. (do not display)
if not LINUX:
ret.extend(self.curse_add_stat('syscalls', width=15, header=' '))
# Return the message with decoration
return ret

View File

@ -0,0 +1,260 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# SPDX-FileCopyrightText: 2023 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
# Fields description
# description: human readable description
# short_name: shortname to use un UI
# unit: unit type
# rate: if True then compute and add *_gauge and *_rate_per_is fields
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
fields_description = {
'disk_name': {
'description': 'Disk name.'
},
'read_count': {
'description': 'Number of reads.',
'rate': True,
'unit': 'number',
},
'write_count': {
'description': 'Number of writes.',
'rate': True,
'unit': 'number',
},
'read_bytes': {
'description': 'Number of bytes read.',
'rate': True,
'unit': 'byte',
},
'write_bytes': {
'description': 'Number of bytes written.',
'rate': True,
'unit': 'byte',
}
}
# Define the history items list
items_history_list = [
{
'name': 'read_bytes_rate_per_sec',
'description': 'Bytes read per second',
'y_unit': 'B/s'
},
{
'name': 'write_bytes_rate_per_sec',
'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=[],
fields_description=fields_description
)
# 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']
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':
stats = self.update_local()
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
stats = self.get_init_value()
else:
stats = self.get_init_value()
# Update the stats
self.stats = stats
return stats
@GlancesPluginModel._manage_rate
def update_local(self):
stats = self.get_init_value()
try:
diskio = psutil.disk_io_counters(perdisk=True)
except Exception:
return stats
for disk_name, disk_stat in diskio.items():
# By default, RamFS is not displayed (issue #714)
if self.args is not None and \
not self.args.diskio_show_ramfs and disk_name.startswith('ram'):
continue
# Shall we display the stats ?
if not self.is_display(disk_name):
continue
# Filter stats to keep only the fields we want (define in fields_description)
# It will also convert psutil.sdiskio to a standard Python dict
stat = self.filter_stats(disk_stat)
# Add the key
stat['key'] = self.get_key()
# Add disk name
stat['disk_name'] = disk_name
# Add alias if exist (define in the configuration file)
if self.has_alias(disk_name) is not None:
stat['alias'] = self.has_alias(disk_name)
stats.append(stat)
return 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(
i['read_bytes'],
header=disk_real_name + '_rx'
)
self.views[i[self.get_key()]]['write_bytes']['decoration'] = self.get_alert(
i['write_bytes'],
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
if max_width:
name_max_width = max_width - 13
else:
# No max_width defined, return an emptu curse message
logger.debug("No max_width defined for the {} plugin, it will not be displayed.".format(self.plugin_name))
return ret
# 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(i.get('read_count_rate_per_sec', None))
rxps = self.auto_unit(i.get('write_count_rate_per_sec', None))
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(i.get('read_bytes_rate_per_sec', None))
rxps = self.auto_unit(i.get('write_bytes_rate_per_sec', None))
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()],
key='read_bytes',
option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=i[self.get_key()],
key='write_bytes',
option='decoration')
)
)
return ret

View File

@ -1,243 +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
# Fields description
# description: human readable description
# short_name: shortname to use in UI
# unit: unit type
# rate: is it a rate ? If yes generate metadata with _gauge and _rate_per_sec
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
# if field has a key=True then this field will be used as iterator for the stat (dict of dict)
fields_description = {
'disk_name': {
'key': True,
'description': 'Disk name.',
},
'read_count': {
'description': 'Number of read since the last update.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
},
'write_count': {
'description': 'Number of write since the last update.',
'unit': 'number',
'rate': True,
'min_symbol': 'K',
},
'read_bytes': {
'description': 'Number of bytes read since the last update.',
'unit': 'bytes',
'rate': True,
'min_symbol': 'K',
},
'write_bytes': {
'description': 'Number of bytes write since the last update.',
'unit': 'bytes',
'rate': True,
'min_symbol': 'K',
},
'time_since_update': {
'description': 'Number of seconds since last update.',
'unit': 'seconds'
},
}
# Define the history items list
items_history_list = [
{'name': 'read_bytes_rate_per_sec', 'description': 'Bytes read per second', 'y_unit': 'B/s'},
{'name': 'write_bytes_rate_per_sec', '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,
fields_description=fields_description
)
# 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_rate_per_sec', 'write_bytes_rate_per_sec']
# Force a first update because we need two update to have the first stat
self.update()
self.refresh_timer.set(0)
@GlancesPluginModel._manage_rate
@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
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
# Convert disk stat to plain Python Dict
diskio_stats = {}
for key in diskio[disk]._fields:
diskio_stats[key] = getattr(diskio[disk], key)
stats[disk] = diskio_stats
# Add alias if exist (define in the configuration file)
if self.has_alias(disk) is not None:
stats[disk]['alias'] = self.has_alias(disk)
elif self.input_method == 'snmp':
# Update stats using SNMP
# No standard way for the moment...
pass
# Update the stats
self.stats = stats
return 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 k, v in self.get_raw().items():
if 'read_bytes_rate_per_sec' in v and 'write_bytes_rate_per_sec' in v:
self.views[k]['read_bytes_rate_per_sec']['decoration'] = self.get_alert(
int(v['read_bytes_rate_per_sec']), header=k + '_rx'
)
self.views[k]['read_bytes_rate_per_sec']['decoration'] = self.get_alert(
int(v['write_bytes_rate_per_sec']), header=k + '_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():
for k in sorted(self.stats):
v = self.stats[k]
if 'read_bytes_rate_per_sec' not in v or 'write_bytes_rate_per_sec' not in v:
continue
# Hide stats if never be different from 0 (issue #1787)
if all([self.get_views(item=k, 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(k) if self.has_alias(k) else k
# 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(v['read_count_rate_per_sec'])
rxps = self.auto_unit(v['write_count_rate_per_sec'])
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=k, key='read_count_rate_per_sec', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=k, key='write_count_rate_per_sec', option='decoration')
)
)
else:
# Bitrate
txps = self.auto_unit(v['read_bytes_rate_per_sec'])
rxps = self.auto_unit(v['write_bytes_rate_per_sec'])
msg = '{:>7}'.format(txps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=k, key='read_bytes_rate_per_sec', option='decoration')
)
)
msg = '{:>7}'.format(rxps)
ret.append(
self.curse_add_line(
msg, self.get_views(item=k, key='write_bytes_rate_per_sec', option='decoration')
)
)
return ret

View File

@ -0,0 +1,168 @@
# -*- 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.logger import logger
from glances.globals import nativestr
from glances.folder_list import FolderList as glancesFolderList
from glances.plugins.plugin.model import GlancesPluginModel
# 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 = {
'path': {
'description': 'Absolute path.'
},
'size': {
'description': 'Folder size in bytes.',
'unit': 'byte',
},
'refresh': {
'description': 'Refresh interval in seconds.',
'unit': 'second',
},
'errno': {
'description': 'Return code when retrieving folder size (0 is no error).',
'unit': 'number',
},
'careful': {
'description': 'Careful threshold in MB.',
'unit': 'megabyte',
},
'warning': {
'description': 'Warning threshold in MB.',
'unit': 'megabyte',
},
'critical': {
'description': 'Critical threshold in MB.',
'unit': 'megabyte',
},
}
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=[],
fields_description=fields_description
)
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
if max_width:
name_max_width = max_width - 7
else:
# No max_width defined, return an emptu curse message
logger.debug("No max_width defined for the {} plugin, it will not be displayed.".format(self.plugin_name))
return ret
# 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

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