mirror of
https://github.com/nicolargo/glances.git
synced 2024-09-19 08:37:52 +03:00
Compute rate automaticaly
This commit is contained in:
commit
bd5e297a0e
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@ -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 }}"
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||
|
11
.github/workflows/test.yml
vendored
11
.github/workflows/test.yml
vendored
@ -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
|
||||
|
17
Makefile
17
Makefile
@ -80,8 +80,11 @@ test-min: ## Run unit tests in minimal environment
|
||||
test-min-with-upgrade: venv-min-upgrade ## Upgrade deps and run unit tests in minimal environment
|
||||
./venv-min/bin/python ./unitest.py
|
||||
|
||||
test-restful-api: ## Run unit tests of the RESTful API
|
||||
./venv/bin/python ./unitest-restful.py
|
||||
|
||||
# ===================================================================
|
||||
# Linters and profilers
|
||||
# Linters, profilers and cyber security
|
||||
# ===================================================================
|
||||
|
||||
format: ## Format the code
|
||||
@ -99,7 +102,7 @@ codespell: ## Run codespell to fix common misspellings in text files
|
||||
./venv-dev/bin/codespell -S .git,./docs/_build,./Glances.egg-info,./venv*,./glances/outputs,*.svg -L hart,bu,te,statics
|
||||
|
||||
semgrep: ## Run semgrep to find bugs and enforce code standards
|
||||
./venv-dev/bin/semgrep --config=auto --lang python --use-git-ignore ./glances
|
||||
./venv-dev/bin/semgrep scan --config=auto
|
||||
|
||||
profiling: ## How to start the profiling of the Glances software
|
||||
@echo "Please complete and run: sudo ./venv-dev/bin/py-spy record -o ./docs/_static/glances-flame.svg -d 60 -s --pid <GLANCES PID>"
|
||||
@ -123,6 +126,10 @@ memory-profiling: ## Profile memory usage
|
||||
./venv-dev/bin/mprof plot --output ./docs/_static/glances-memory-profiling-without-history.png
|
||||
rm -f mprofile_*.dat
|
||||
|
||||
# Trivy installation: https://aquasecurity.github.io/trivy/latest/getting-started/installation/
|
||||
trivy: ## Run Trivy to find vulnerabilities in container images
|
||||
trivy fs .
|
||||
|
||||
# ===================================================================
|
||||
# Docs
|
||||
# ===================================================================
|
||||
@ -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
|
||||
# ===================================================================
|
||||
|
18
NEWS.rst
18
NEWS.rst
@ -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
|
||||
|
26
README.rst
26
README.rst
@ -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:
|
||||
|
@ -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
|
||||
|
@ -12,3 +12,5 @@ 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
|
||||
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
|
@ -1,3 +1,3 @@
|
||||
FROM nicolargo/glances:latest as glancesminimal
|
||||
FROM glances:local-alpine-minimal as glancesminimal
|
||||
COPY glances.conf /glances/conf/glances.conf
|
||||
CMD python -m glances -C /glances/conf/glances.conf $GLANCES_OPT
|
||||
|
@ -25,15 +25,16 @@ services:
|
||||
- "/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro"
|
||||
- "./glances.conf:/glances/conf/glances.conf"
|
||||
environment:
|
||||
- GLANCES_OPT: "-C /glances/conf/glances.conf -w"
|
||||
- TZ: "${TZ}"
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
- TZ=${TZ}
|
||||
- "GLANCES_OPT=-C /glances/conf/glances.conf -w"
|
||||
# Uncomment for GPU compatibilty (Nvidia) inside the container
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
labels:
|
||||
- "traefik.port=61208"
|
||||
- "traefik.frontend.rule=Host:glances.docker.localhost"
|
||||
|
@ -13,12 +13,13 @@ services:
|
||||
- "/run/user/1000/podman/podman.sock:/run/user/1000/podman/podman.sock:ro"
|
||||
- "./glances.conf:/glances/conf/glances.conf"
|
||||
environment:
|
||||
- GLANCES_OPT: "-C /glances/conf/glances.conf -w"
|
||||
- TZ: "${TZ}"
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
- TZ=${TZ}
|
||||
- "GLANCES_OPT=-C /glances/conf/glances.conf -w"
|
||||
# Uncomment for GPU compatibilty (Nvidia) inside the container
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
4
docs/_static/glances-flame.svg
vendored
4
docs/_static/glances-flame.svg
vendored
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 145 KiB |
@ -21,11 +21,11 @@ under the ``[containers]`` section:
|
||||
|
||||
[containers]
|
||||
disable=False
|
||||
# Only show specific containers (comma separated list of container name or regular expression)
|
||||
# Only show specific containers (comma-separated list of container name or regular expression)
|
||||
show=thiscontainer,andthisone,andthoseones.*
|
||||
# Hide some containers (comma separated list of container name or regular expression)
|
||||
# Hide some containers (comma-separated list of container name or regular expression)
|
||||
hide=donotshowthisone,andthose.*
|
||||
# Show only specific containers (comma separated list of container name or regular expression)
|
||||
# Show only specific containers (comma-separated list of container name or regular expression)
|
||||
#show=showthisone,andthose.*
|
||||
# Define the maximum containers size name (default is 20 chars)
|
||||
max_name_size=20
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
||||
============= ============
|
||||
|
@ -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.
|
||||
|
@ -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'
|
||||
|
@ -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=|
|
||||
|
892
docs/api.rst
892
docs/api.rst
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,7 @@ Command-Line Options
|
||||
|
||||
.. option:: -V, --version
|
||||
|
||||
show program's version number and exit
|
||||
show the program's version number and exit
|
||||
|
||||
.. option:: -d, --debug
|
||||
|
||||
@ -22,25 +22,29 @@ Command-Line Options
|
||||
|
||||
path to the configuration file
|
||||
|
||||
.. option:: -P PLUGIN_DIRECTORY, --plugins PLUGIN_DIRECTORY
|
||||
|
||||
path to a directory containing additional plugins
|
||||
|
||||
.. option:: --modules-list
|
||||
|
||||
display modules (plugins & exports) list and exit
|
||||
|
||||
.. option:: --disable-plugin PLUGIN
|
||||
|
||||
disable PLUGIN (comma separated list)
|
||||
disable PLUGIN (comma-separated list)
|
||||
|
||||
.. option:: --enable-plugin PLUGIN
|
||||
|
||||
enable PLUGIN (comma separated list)
|
||||
enable PLUGIN (comma-separated list)
|
||||
|
||||
.. option:: --stdout PLUGINS_STATS
|
||||
|
||||
display stats to stdout (comma separated list of plugins/plugins.attribute)
|
||||
display stats to stdout (comma-separated list of plugins/plugins.attribute)
|
||||
|
||||
.. option:: --export EXPORT
|
||||
|
||||
enable EXPORT module (comma separated list)
|
||||
enable EXPORT module (comma-separated list)
|
||||
|
||||
.. option:: --export-csv-file EXPORT_CSV_FILE
|
||||
|
||||
@ -60,7 +64,7 @@ Command-Line Options
|
||||
|
||||
.. option:: --light, --enable-light
|
||||
|
||||
light mode for Curses UI (disable all but top menu)
|
||||
light mode for Curses UI (disable all but the top menu)
|
||||
|
||||
.. option:: -0, --disable-irix
|
||||
|
||||
@ -84,7 +88,7 @@ Command-Line Options
|
||||
|
||||
.. option:: -5, --disable-top
|
||||
|
||||
disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
|
||||
disable top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
|
||||
|
||||
.. option:: -6, --meangpu
|
||||
|
||||
@ -168,7 +172,7 @@ Command-Line Options
|
||||
|
||||
.. option:: -w, --webserver
|
||||
|
||||
run Glances in web server mode (bottle lib needed)
|
||||
run Glances in web server mode (FastAPI lib needed)
|
||||
|
||||
.. option:: --cached-time CACHED_TIME
|
||||
|
||||
@ -192,11 +196,11 @@ Command-Line Options
|
||||
|
||||
.. option:: --hide-kernel-threads
|
||||
|
||||
hide kernel threads in process list (not available on Windows)
|
||||
hide kernel threads in the process list (not available on Windows)
|
||||
|
||||
.. option:: -b, --byte
|
||||
|
||||
display network rate in byte per second
|
||||
display network rate in bytes per second
|
||||
|
||||
.. option:: --diskio-show-ramfs
|
||||
|
||||
@ -216,11 +220,11 @@ Command-Line Options
|
||||
|
||||
.. option:: --theme-white
|
||||
|
||||
optimize display colors for white background
|
||||
optimize display colors for a white background
|
||||
|
||||
.. option:: --disable-check-update
|
||||
|
||||
disable online Glances version ckeck
|
||||
disable online Glances version check
|
||||
|
||||
Interactive Commands
|
||||
--------------------
|
||||
@ -232,7 +236,7 @@ The following commands (key pressed) are supported while in Glances:
|
||||
|
||||
.. note:: On macOS please use ``CTRL-H`` to delete filter.
|
||||
|
||||
Filter is a regular expression pattern:
|
||||
The filter is a regular expression pattern:
|
||||
|
||||
- ``gnome``: matches all processes starting with the ``gnome``
|
||||
string
|
||||
@ -250,7 +254,7 @@ The following commands (key pressed) are supported while in Glances:
|
||||
- If CPU iowait ``>60%``, sort processes by I/O read and write
|
||||
|
||||
``A``
|
||||
Enable/disable Application Monitoring Process
|
||||
Enable/disable the Application Monitoring Process
|
||||
|
||||
``b``
|
||||
Switch between bit/s or Byte/s for network I/O
|
||||
@ -274,7 +278,7 @@ The following commands (key pressed) are supported while in Glances:
|
||||
Enable/disable top extended stats
|
||||
|
||||
``E``
|
||||
Erase current process filter
|
||||
Erase the current process filter
|
||||
|
||||
``f``
|
||||
Show/hide file system and folder monitoring stats
|
||||
@ -352,7 +356,7 @@ The following commands (key pressed) are supported while in Glances:
|
||||
Sort process by CPU times (TIME+)
|
||||
|
||||
``T``
|
||||
View network I/O as combination
|
||||
View network I/O as a combination
|
||||
|
||||
``u``
|
||||
Sort processes by USER
|
||||
@ -375,13 +379,13 @@ The following commands (key pressed) are supported while in Glances:
|
||||
``0``
|
||||
Enable/disable Irix/Solaris mode
|
||||
|
||||
Task's CPU usage will be divided by the total number of CPUs
|
||||
The task's CPU usage will be divided by the total number of CPUs
|
||||
|
||||
``1``
|
||||
Switch between global CPU and per-CPU stats
|
||||
|
||||
``2``
|
||||
Enable/disable left sidebar
|
||||
Enable/disable the left sidebar
|
||||
|
||||
``3``
|
||||
Enable/disable the quick look module
|
||||
@ -390,7 +394,7 @@ The following commands (key pressed) are supported while in Glances:
|
||||
Enable/disable all but quick look and load module
|
||||
|
||||
``5``
|
||||
Enable/disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
|
||||
Enable/disable the top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
|
||||
|
||||
``6``
|
||||
Enable/disable mean GPU mode
|
||||
@ -405,10 +409,10 @@ The following commands (key pressed) are supported while in Glances:
|
||||
Refresh user interface
|
||||
|
||||
``LEFT``
|
||||
Navigation left through process sort
|
||||
Navigation left through the process sort
|
||||
|
||||
``RIGHT``
|
||||
Navigation right through process sort
|
||||
Navigation right through the process sort
|
||||
|
||||
``UP``
|
||||
Up in the processes list
|
||||
|
@ -5,7 +5,7 @@ Configuration
|
||||
|
||||
No configuration file is mandatory to use Glances.
|
||||
|
||||
Furthermore a configuration file is needed to access more settings.
|
||||
Furthermore, a configuration file is needed to access more settings.
|
||||
|
||||
Location
|
||||
--------
|
||||
@ -14,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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -3,13 +3,13 @@ Glances
|
||||
|
||||
.. image:: _static/screenshot-wide.png
|
||||
|
||||
Glances is a cross-platform monitoring tool which aims to present a
|
||||
maximum of information in a minimum of space through a curses or Web
|
||||
based interface. It can adapt dynamically the displayed information
|
||||
depending on the terminal size.
|
||||
Glances is a cross-platform monitoring tool that aims to present
|
||||
maximum information in minimal space through either a curses-based
|
||||
or Web-based interface. It can dynamically adapt the displayed
|
||||
information depending on the terminal size.
|
||||
|
||||
It can also work in client/server mode. Remote monitoring could be
|
||||
done via terminal, Web interface or API (XMLRPC and RESTful).
|
||||
It can also work in client/server mode. Remote monitoring can be
|
||||
done via terminal, Web interface, or API (XMLRPC and RESTful).
|
||||
|
||||
Glances is written in Python and uses the `psutil`_ library to get
|
||||
information from your system.
|
||||
|
@ -3,8 +3,8 @@
|
||||
Install
|
||||
=======
|
||||
|
||||
Glances is on ``PyPI``. By using PyPI, you are sure to have the latest
|
||||
stable version.
|
||||
Glances is available on ``PyPI``. By using PyPI, you are sure to have the
|
||||
latest stable version.
|
||||
|
||||
To install, simply use ``pip``:
|
||||
|
||||
@ -12,13 +12,13 @@ To install, simply use ``pip``:
|
||||
|
||||
pip install glances
|
||||
|
||||
*Note*: Python headers are required to install `psutil`_. For example,
|
||||
on Debian/Ubuntu you need to install first the *python-dev* package.
|
||||
For Fedora/CentOS/RHEL install first *python-devel* package. For Windows,
|
||||
just install psutil from the binary installation file.
|
||||
*Note*: Python headers are required to install `psutil`_. For instance,
|
||||
on Debian/Ubuntu, you must first install the *python-dev* package.
|
||||
On Fedora/CentOS/RHEL, first, install the *python-devel* package. For Windows,
|
||||
psutil can be installed from the binary installation file.
|
||||
|
||||
You can also install the following libraries in order to use optional
|
||||
features (like the Web interface, export modules...):
|
||||
You can also install the following libraries to use the optional
|
||||
features (such as the web interface, export modules, etc.):
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
|
@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "GLANCES" "1" "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 program’s version number and exit
|
||||
show the program’s version number and exit
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@ -68,28 +68,33 @@ path to the configuration file
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-P PLUGIN_DIRECTORY, \-\-plugins PLUGIN_DIRECTORY
|
||||
path to a directory containing additional plugins
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-modules\-list
|
||||
display modules (plugins & exports) list and exit
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-disable\-plugin PLUGIN
|
||||
disable PLUGIN (comma separated list)
|
||||
disable PLUGIN (comma\-separated list)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-enable\-plugin PLUGIN
|
||||
enable PLUGIN (comma separated list)
|
||||
enable PLUGIN (comma\-separated list)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-stdout PLUGINS_STATS
|
||||
display stats to stdout (comma separated list of plugins/plugins.attribute)
|
||||
display stats to stdout (comma\-separated list of plugins/plugins.attribute)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-export EXPORT
|
||||
enable EXPORT module (comma separated list)
|
||||
enable EXPORT module (comma\-separated list)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@ -114,7 +119,7 @@ disable the Web UI (only the RESTful API will respond)
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-light, \-\-enable\-light
|
||||
light mode for Curses UI (disable all but top menu)
|
||||
light mode for Curses UI (disable all but the top menu)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@ -144,7 +149,7 @@ disable all but quick look and load
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-5, \-\-disable\-top
|
||||
disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
|
||||
disable top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@ -249,7 +254,7 @@ set refresh time in seconds [default: 3 sec]
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-w, \-\-webserver
|
||||
run Glances in web server mode (bottle lib needed)
|
||||
run Glances in web server mode (FastAPI lib needed)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@ -279,12 +284,12 @@ force short name for processes name
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-hide\-kernel\-threads
|
||||
hide kernel threads in process list (not available on Windows)
|
||||
hide kernel threads in the process list (not available on Windows)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-b, \-\-byte
|
||||
display network rate in byte per second
|
||||
display network rate in bytes per second
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@ -309,12 +314,12 @@ display FS free space instead of used
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-theme\-white
|
||||
optimize display colors for white background
|
||||
optimize display colors for a white background
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-\-disable\-check\-update
|
||||
disable online Glances version ckeck
|
||||
disable online Glances version check
|
||||
.UNINDENT
|
||||
.SH INTERACTIVE COMMANDS
|
||||
.sp
|
||||
@ -331,7 +336,7 @@ On macOS please use \fBCTRL\-H\fP to delete filter.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
Filter is a regular expression pattern:
|
||||
The filter is a regular expression pattern:
|
||||
.INDENT 7.0
|
||||
.IP \(bu 2
|
||||
\fBgnome\fP: matches all processes starting with the \fBgnome\fP
|
||||
@ -353,7 +358,7 @@ If CPU iowait \fB>60%\fP, sort processes by I/O read and write
|
||||
.UNINDENT
|
||||
.TP
|
||||
.B \fBA\fP
|
||||
Enable/disable Application Monitoring Process
|
||||
Enable/disable the Application Monitoring Process
|
||||
.TP
|
||||
.B \fBb\fP
|
||||
Switch between bit/s or Byte/s for network I/O
|
||||
@ -377,7 +382,7 @@ Enable/disable Docker stats
|
||||
Enable/disable top extended stats
|
||||
.TP
|
||||
.B \fBE\fP
|
||||
Erase current process filter
|
||||
Erase the current process filter
|
||||
.TP
|
||||
.B \fBf\fP
|
||||
Show/hide file system and folder monitoring stats
|
||||
@ -455,7 +460,7 @@ Enable/disable spark lines
|
||||
Sort process by CPU times (TIME+)
|
||||
.TP
|
||||
.B \fBT\fP
|
||||
View network I/O as combination
|
||||
View network I/O as a combination
|
||||
.TP
|
||||
.B \fBu\fP
|
||||
Sort processes by USER
|
||||
@ -478,13 +483,13 @@ Show/hide processes stats
|
||||
.B \fB0\fP
|
||||
Enable/disable Irix/Solaris mode
|
||||
.sp
|
||||
Task’s CPU usage will be divided by the total number of CPUs
|
||||
The task’s CPU usage will be divided by the total number of CPUs
|
||||
.TP
|
||||
.B \fB1\fP
|
||||
Switch between global CPU and per\-CPU stats
|
||||
.TP
|
||||
.B \fB2\fP
|
||||
Enable/disable left sidebar
|
||||
Enable/disable the left sidebar
|
||||
.TP
|
||||
.B \fB3\fP
|
||||
Enable/disable the quick look module
|
||||
@ -493,7 +498,7 @@ Enable/disable the quick look module
|
||||
Enable/disable all but quick look and load module
|
||||
.TP
|
||||
.B \fB5\fP
|
||||
Enable/disable top menu (QuickLook, CPU, MEM, SWAP and LOAD)
|
||||
Enable/disable the top menu (QuickLook, CPU, MEM, SWAP, and LOAD)
|
||||
.TP
|
||||
.B \fB6\fP
|
||||
Enable/disable mean GPU mode
|
||||
@ -508,10 +513,10 @@ Switch between process command line or command name
|
||||
Refresh user interface
|
||||
.TP
|
||||
.B \fBLEFT\fP
|
||||
Navigation left through process sort
|
||||
Navigation left through the process sort
|
||||
.TP
|
||||
.B \fBRIGHT\fP
|
||||
Navigation right through process sort
|
||||
Navigation right through the process sort
|
||||
.TP
|
||||
.B \fBUP\fP
|
||||
Up in the processes list
|
||||
@ -540,7 +545,7 @@ Quit Glances
|
||||
.sp
|
||||
No configuration file is mandatory to use Glances.
|
||||
.sp
|
||||
Furthermore a configuration file is needed to access more settings.
|
||||
Furthermore, a configuration file is needed to access more settings.
|
||||
.SH LOCATION
|
||||
.sp
|
||||
\fBNOTE:\fP
|
||||
@ -551,7 +556,7 @@ A template is available in the \fB/usr{,/local}/share/doc/glances\fP
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
You can put your own \fBglances.conf\fP file in the following locations:
|
||||
You can place your \fBglances.conf\fP file in the following locations:
|
||||
.TS
|
||||
center;
|
||||
|l|l|.
|
||||
@ -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.
|
||||
.
|
||||
|
@ -3,8 +3,8 @@
|
||||
Quickstart
|
||||
==========
|
||||
|
||||
This page gives a good introduction in how to get started with Glances.
|
||||
Glances offers 3 modes:
|
||||
This page gives a good introduction to how to get started with Glances.
|
||||
Glances offers three modes:
|
||||
|
||||
- Standalone
|
||||
- Client/Server
|
||||
@ -61,7 +61,7 @@ Note: It will display one line per stat per refresh.
|
||||
Client/Server Mode
|
||||
------------------
|
||||
|
||||
If you want to remotely monitor a machine, called ``server``, from
|
||||
If you want to remotely monitor a machine called ``server``, from
|
||||
another one, called ``client``, just run on the server:
|
||||
|
||||
.. code-block:: console
|
||||
@ -118,7 +118,7 @@ To start the central client, use the following option:
|
||||
|
||||
.. note::
|
||||
|
||||
Use ``--disable-autodiscover`` to disable the auto discovery mode.
|
||||
Use ``--disable-autodiscover`` to disable the auto-discovery mode.
|
||||
|
||||
When the list is displayed, you can navigate through the Glances servers with
|
||||
up/down keys. It is also possible to sort the server using:
|
||||
@ -137,7 +137,7 @@ client, the latter will try to grab stats using the ``SNMP`` protocol:
|
||||
client$ glances -c @snmpserver
|
||||
|
||||
.. note::
|
||||
Stats grabbed by SNMP request are limited and OS dependent.
|
||||
Stats grabbed by SNMP request are limited and OS-dependent.
|
||||
A SNMP server should be installed and configured...
|
||||
|
||||
|
||||
@ -152,14 +152,14 @@ Web Server Mode
|
||||
|
||||
.. image:: _static/screenshot-web.png
|
||||
|
||||
If you want to remotely monitor a machine, called ``server``, from any
|
||||
If you want to remotely monitor a machine called ``server``, from any
|
||||
device with a web browser, just run the server with the ``-w`` option:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
server$ glances -w
|
||||
|
||||
then on the client enter the following URL in your favorite web browser:
|
||||
then, on the client, enter the following URL in your favorite web browser:
|
||||
|
||||
::
|
||||
|
||||
@ -167,7 +167,7 @@ then on the client enter the following URL in your favorite web browser:
|
||||
|
||||
where ``@server`` is the IP address or hostname of the server.
|
||||
|
||||
To change the refresh rate of the page, just add the period in seconds
|
||||
To change the refresh rate of the page, add the period in seconds
|
||||
at the end of the URL. For example, to refresh the page every ``10``
|
||||
seconds:
|
||||
|
||||
@ -181,10 +181,10 @@ Here's a screenshot from Chrome on Android:
|
||||
|
||||
.. image:: _static/screenshot-web2.png
|
||||
|
||||
How to protect your server (or Web server) with a login/password ?
|
||||
How do you protect your server (or Web server) with a login/password ?
|
||||
------------------------------------------------------------------
|
||||
|
||||
You can set a password to access to the server using the ``--password``.
|
||||
You can set a password to access the server using the ``--password``.
|
||||
By default, the login is ``glances`` but you can change it with
|
||||
``--username``.
|
||||
|
||||
@ -192,8 +192,8 @@ If you want, the SHA password will be stored in ``<login>.pwd`` file (in
|
||||
the same folder where the Glances configuration file is stored, so
|
||||
~/.config/glances/ on GNU Linux operating system).
|
||||
|
||||
Next time your run the server/client, password will not be asked. To set a
|
||||
specific username you can use the -u <username> option.
|
||||
Next time you run the server/client, password will not be asked. To set a
|
||||
specific username, you can use the -u <username> option.
|
||||
|
||||
It is also possible to set the default password in the Glances configuration
|
||||
file:
|
||||
|
@ -7,7 +7,7 @@ To post a question about Glances use cases, please post it to the
|
||||
official Q&A `forum
|
||||
<https://groups.google.com/forum/?hl=en#!forum/glances-users>`_.
|
||||
|
||||
To report a bug or a feature request use the GitHub `issue
|
||||
To report a bug or a feature request, use the GitHub `issue
|
||||
<https://github.com/nicolargo/glances/issues>`_ tracker.
|
||||
|
||||
Feel free to contribute!
|
||||
|
@ -12,7 +12,7 @@ globals.py Share variables upon modules
|
||||
main.py Main script to rule them up...
|
||||
client.py Glances client
|
||||
server.py Glances server
|
||||
webserver.py Glances web server (Bottle-based)
|
||||
webserver.py Glances web server (Based on FastAPI)
|
||||
autodiscover.py Glances autodiscover module (via zeroconf)
|
||||
standalone.py Glances standalone (curses interface)
|
||||
password.py Manage password for Glances client/server
|
||||
@ -27,7 +27,7 @@ plugins
|
||||
outputs
|
||||
=> Glances UI
|
||||
glances_curses.py The curses interface
|
||||
glances_bottle.py The web interface
|
||||
glances_restful-api.py The HTTP/API & Web based interface
|
||||
...
|
||||
exports
|
||||
=> Glances exports
|
||||
|
@ -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(
|
||||
|
@ -42,7 +42,7 @@ class GlancesAmp(object):
|
||||
|
||||
# AMP name (= module name without glances_)
|
||||
if name is None:
|
||||
self.amp_name = self.__class__.__module__[len('glances_') :]
|
||||
self.amp_name = self.__class__.__module__
|
||||
else:
|
||||
self.amp_name = name
|
||||
|
@ -29,7 +29,7 @@ from subprocess import check_output, STDOUT, CalledProcessError
|
||||
|
||||
from glances.globals import u, to_ascii
|
||||
from glances.logger import logger
|
||||
from glances.amps.glances_amp import GlancesAmp
|
||||
from glances.amps.amp import GlancesAmp
|
||||
|
||||
|
||||
class Amp(GlancesAmp):
|
@ -47,7 +47,7 @@ status_url=http://localhost/nginx_status
|
||||
import requests
|
||||
|
||||
from glances.logger import logger
|
||||
from glances.amps.glances_amp import GlancesAmp
|
||||
from glances.amps.amp import GlancesAmp
|
||||
|
||||
|
||||
class Amp(GlancesAmp):
|
@ -39,7 +39,7 @@ from subprocess import check_output, CalledProcessError
|
||||
|
||||
from glances.logger import logger
|
||||
from glances.globals import iteritems, to_ascii
|
||||
from glances.amps.glances_amp import GlancesAmp
|
||||
from glances.amps.amp import GlancesAmp
|
||||
|
||||
|
||||
class Amp(GlancesAmp):
|
@ -38,7 +38,7 @@ from subprocess import check_output, STDOUT
|
||||
|
||||
from glances.logger import logger
|
||||
from glances.globals import iteritems
|
||||
from glances.amps.glances_amp import GlancesAmp
|
||||
from glances.amps.amp import GlancesAmp
|
||||
|
||||
|
||||
class Amp(GlancesAmp):
|
@ -45,41 +45,30 @@ class AmpsList(object):
|
||||
if self.config is None:
|
||||
return False
|
||||
|
||||
# Display a warning (deprecated) message if the monitor section exist
|
||||
if "monitor" in self.config.sections():
|
||||
logger.warning(
|
||||
"A deprecated [monitor] section exists in the Glances configuration file. You should use the new \
|
||||
Applications Monitoring Process module instead \
|
||||
(http://glances.readthedocs.io/en/develop/aoa/amps.html)."
|
||||
)
|
||||
|
||||
# TODO: Change the way AMP are loaded (use folder/module instead of glances_foo.py file)
|
||||
# See https://github.com/nicolargo/glances/issues/1930
|
||||
header = "glances_"
|
||||
# For each AMP script, call the load_config method
|
||||
for s in self.config.sections():
|
||||
if s.startswith("amp_"):
|
||||
# An AMP section exists in the configuration file
|
||||
# If an AMP script exist in the glances/amps folder, use it
|
||||
amp_conf_name = s[4:]
|
||||
amp_script = os.path.join(amps_path, header + s[4:] + ".py")
|
||||
if not os.path.exists(amp_script):
|
||||
# If an AMP module exist in amps_path (glances/amps) folder then use it
|
||||
amp_name = s[4:]
|
||||
amp_module = os.path.join(amps_path, amp_name)
|
||||
if not os.path.exists(amp_module):
|
||||
# If not, use the default script
|
||||
amp_script = os.path.join(amps_path, "glances_default.py")
|
||||
amp_module = os.path.join(amps_path, "default")
|
||||
try:
|
||||
amp = __import__(os.path.basename(amp_script)[:-3])
|
||||
amp = __import__(os.path.basename(amp_module))
|
||||
except ImportError as e:
|
||||
logger.warning("Missing Python Lib ({}), cannot load {} AMP".format(e, amp_conf_name))
|
||||
logger.warning("Missing Python Lib ({}), cannot load AMP {}".format(e, amp_name))
|
||||
except Exception as e:
|
||||
logger.warning("Cannot load {} AMP ({})".format(amp_conf_name, e))
|
||||
logger.warning("Cannot load AMP {} ({})".format(amp_name, e))
|
||||
else:
|
||||
# Add the AMP to the dictionary
|
||||
# The key is the AMP name
|
||||
# for example, the file glances_xxx.py
|
||||
# generate self._amps_list["xxx"] = ...
|
||||
self.__amps_dict[amp_conf_name] = amp.Amp(name=amp_conf_name, args=self.args)
|
||||
self.__amps_dict[amp_name] = amp.Amp(name=amp_name, args=self.args)
|
||||
# Load the AMP configuration
|
||||
self.__amps_dict[amp_conf_name].load_config(self.config)
|
||||
self.__amps_dict[amp_name].load_config(self.config)
|
||||
# Log AMPs list
|
||||
logger.debug("AMPs list: {}".format(self.getList()))
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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 = {}
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
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()
|
||||
|
||||
|
@ -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]
|
||||
|
180
glances/main.py
180
glances/main.py
@ -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):
|
||||
|
@ -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...
|
||||
|
@ -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):
|
||||
|
@ -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
|
@ -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
|
||||
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,122 +388,137 @@ 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
|
||||
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 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:
|
||||
self._handle_kill_process()
|
||||
elif self.pressedkey == curses.KEY_LEFT:
|
||||
self._handle_sort_left()
|
||||
elif self.pressedkey == curses.KEY_RIGHT:
|
||||
self._handle_sort_right()
|
||||
elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65 and not self.args.disable_cursor:
|
||||
self._handle_cursor_up()
|
||||
elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66 and not self.args.disable_cursor:
|
||||
self._handle_cursor_down()
|
||||
elif self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
|
||||
self._handle_quit(return_to_browser)
|
||||
elif self.pressedkey == curses.KEY_F5 or self.pressedkey == 18:
|
||||
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_'):
|
||||
# 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'
|
||||
)
|
||||
|
||||
# Other actions...
|
||||
if self.pressedkey == ord('\n'):
|
||||
# 'ENTER' > Edit the process filter
|
||||
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
|
||||
elif self.pressedkey == ord('4'):
|
||||
# '4' > Enable or disable quicklook
|
||||
|
||||
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()
|
||||
elif self.pressedkey == ord('5'):
|
||||
# '5' > Enable or disable top menu
|
||||
|
||||
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()
|
||||
elif self.pressedkey == ord('9'):
|
||||
# '9' > Theme from black to white and reverse
|
||||
|
||||
def _handle_theme(self):
|
||||
self._init_colors()
|
||||
elif self.pressedkey == ord('e') and not self.args.programs:
|
||||
# 'e' > Enable/Disable process extended
|
||||
|
||||
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()
|
||||
# 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
|
||||
|
||||
def _handle_erase_filter(self):
|
||||
glances_processes.process_filter = None
|
||||
elif self.pressedkey == ord('f'):
|
||||
# 'f' > Show/hide fs / folder stats
|
||||
|
||||
def _handle_fs_stats(self):
|
||||
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
|
||||
|
||||
def _handle_increase_nice(self):
|
||||
self.increase_nice_process = not self.increase_nice_process
|
||||
elif self.pressedkey == ord('-'):
|
||||
# '+' > Decrease process nice level
|
||||
|
||||
def _handle_decrease_nice(self):
|
||||
self.decrease_nice_process = not self.decrease_nice_process
|
||||
elif self.pressedkey == ord('k') and not self.args.disable_cursor:
|
||||
# 'k' > Kill selected process (after confirmation)
|
||||
|
||||
def _handle_kill_process(self):
|
||||
self.kill_process = not self.kill_process
|
||||
elif self.pressedkey == ord('w'):
|
||||
# 'w' > Delete finished warning logs
|
||||
|
||||
def _handle_clean_logs(self):
|
||||
glances_events.clean()
|
||||
elif self.pressedkey == ord('x'):
|
||||
# 'x' > Delete finished warning and critical logs
|
||||
|
||||
def _handle_clean_critical_logs(self):
|
||||
glances_events.clean(critical=True)
|
||||
elif self.pressedkey == ord('z'):
|
||||
# 'z' > Enable or disable processes
|
||||
|
||||
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()
|
||||
elif self.pressedkey == curses.KEY_LEFT:
|
||||
# "<" (left arrow) navigation through process sort
|
||||
|
||||
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)
|
||||
elif self.pressedkey == curses.KEY_RIGHT:
|
||||
# ">" (right arrow) navigation through process sort
|
||||
|
||||
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)
|
||||
elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65 and not self.args.disable_cursor:
|
||||
# 'UP' > Up in the server list
|
||||
|
||||
def _handle_cursor_up(self):
|
||||
if self.args.cursor_position > 0:
|
||||
self.args.cursor_position -= 1
|
||||
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:
|
||||
|
||||
def _handle_cursor_down(self):
|
||||
if self.args.cursor_position < glances_processes.processes_count:
|
||||
self.args.cursor_position += 1
|
||||
elif self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
|
||||
# 'ESC'|'q' > Quit
|
||||
|
||||
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))
|
||||
elif self.pressedkey == curses.KEY_F5 or self.pressedkey == 18:
|
||||
# "F5" or Ctrl-R to force UI refresh
|
||||
pass
|
||||
|
||||
# Return the key code
|
||||
return self.pressedkey
|
||||
def _handle_refresh(self):
|
||||
pass
|
||||
|
||||
def loop_position(self):
|
||||
"""Return the current sort in the 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,6 +1020,7 @@ class _GlancesCurses(object):
|
||||
|
||||
# Add the message
|
||||
for y, m in enumerate(sentence_list):
|
||||
if len(m) > 0:
|
||||
popup.addnstr(2 + y, 2, m, len(m))
|
||||
|
||||
if popup_type == 'info':
|
||||
@ -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."""
|
||||
|
800
glances/outputs/glances_restful_api.py
Normal file
800
glances/outputs/glances_restful_api.py
Normal 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)
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
||||
|
@ -65,7 +65,7 @@ static
|
||||
|
|
||||
|--- public # path where builds are put
|
||||
|
|
||||
|--- templates (bottle)
|
||||
|--- templates
|
||||
```
|
||||
|
||||
## Data
|
||||
|
@ -19,6 +19,15 @@ body {
|
||||
display: table-cell;
|
||||
text-align: right;
|
||||
}
|
||||
.width-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;
|
||||
|
@ -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"
|
||||
<!-- 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"
|
||||
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>
|
||||
: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;
|
||||
});
|
||||
|
||||
|
@ -266,7 +266,7 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
fetch('api/3/help', { method: 'GET' })
|
||||
fetch('api/4/help', { method: 'GET' })
|
||||
.then((response) => response.json())
|
||||
.then((response) => (this.help = response));
|
||||
}
|
||||
|
@ -11,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;
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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']);
|
||||
|
@ -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')"
|
||||
|
@ -64,13 +64,13 @@ export default {
|
||||
}
|
||||
function getColumnLabel(value) {
|
||||
const labels = {
|
||||
io_counters: 'disk IO',
|
||||
cpu_percent: 'CPU consumption',
|
||||
memory_percent: 'memory consumption',
|
||||
cpu_times: 'process time',
|
||||
username: 'user name',
|
||||
name: 'process name',
|
||||
timemillis: 'process time',
|
||||
cpu_times: 'process time',
|
||||
io_counters: 'disk IO',
|
||||
name: 'process name',
|
||||
None: 'None'
|
||||
};
|
||||
return labels[value] || value;
|
||||
|
@ -3,112 +3,88 @@
|
||||
<section id="processlist-plugin" class="plugin">
|
||||
<div class="table">
|
||||
<div class="table-row">
|
||||
<div
|
||||
class="table-cell"
|
||||
:class="['sortable', sorter.column === 'cpu_percent' && 'sort']"
|
||||
@click="$emit('update:sorter', 'cpu_percent')"
|
||||
>
|
||||
<div class="table-cell width-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;
|
||||
});
|
||||
|
||||
|
@ -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]}%;`"
|
||||
>
|
||||
|
||||
</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}%;`"
|
||||
>
|
||||
|
||||
</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) {
|
||||
|
@ -74,11 +74,15 @@ 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) {
|
||||
if (begin) {
|
||||
return input.substring(0, max - 1) + '_';
|
||||
} else {
|
||||
return '_' + input.substring(input.length - max + 1);
|
||||
}
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
|
@ -2,15 +2,15 @@ import { store } from './store.js';
|
||||
import Favico from 'favico.js';
|
||||
|
||||
// prettier-ignore
|
||||
const fetchAll = () => fetch('api/3/all', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchAll = () => fetch('api/4/all', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchAllViews = () => fetch('api/3/all/views', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchAllViews = () => fetch('api/4/all/views', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchAllLimits = () => fetch('api/3/all/limits', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchAllLimits = () => fetch('api/4/all/limits', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchArgs = () => fetch('api/3/args', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchArgs = () => fetch('api/4/args', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchConfig = () => fetch('api/3/config', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchConfig = () => fetch('api/4/config', { method: 'GET' }).then((response) => response.json());
|
||||
|
||||
class GlancesHelperService {
|
||||
limits = {};
|
||||
|
16
glances/outputs/static/js/uiconfig.json
Normal file
16
glances/outputs/static/js/uiconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"leftMenu": [
|
||||
"network",
|
||||
"wifi",
|
||||
"connections",
|
||||
"ports",
|
||||
"diskio",
|
||||
"fs",
|
||||
"irq",
|
||||
"folders",
|
||||
"raid",
|
||||
"smart",
|
||||
"sensors",
|
||||
"now"
|
||||
]
|
||||
}
|
93
glances/outputs/static/package-lock.json
generated
93
glances/outputs/static/package-lock.json
generated
@ -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",
|
||||
|
BIN
glances/outputs/static/public/glances.js
vendored
BIN
glances/outputs/static/public/glances.js
vendored
Binary file not shown.
22
glances/outputs/static/templates/index.html
Normal file
22
glances/outputs/static/templates/index.html
Normal 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>
|
@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Glances</title>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<script>
|
||||
window.__GLANCES__ = {
|
||||
'refresh-time': '{{ refresh_time }}'
|
||||
}
|
||||
</script>
|
||||
<script src="glances.js" defer></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
@ -16,7 +16,7 @@ import sys
|
||||
import uuid
|
||||
from io import open
|
||||
|
||||
from glances.globals import b, safe_makedirs
|
||||
from glances.globals import b, safe_makedirs, weak_lru_cache
|
||||
from glances.config import user_config_dir
|
||||
from glances.logger import logger
|
||||
|
||||
@ -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
|
||||
|
@ -6,23 +6,16 @@ This is the Glances plugins folder.
|
||||
|
||||
A Glances plugin is a Python module hosted in a folder.
|
||||
|
||||
It should be based on the MVC model.
|
||||
- model: data model (where the stats will be updated)
|
||||
- view: input for UI (where the stats are displayed)
|
||||
- controler: output from UI (where the stats are controled)
|
||||
It should implement a Class named PluginModel (inherited from GlancesPluginModel).
|
||||
|
||||
////
|
||||
TODO
|
||||
////
|
||||
This class should be based on the MVC model.
|
||||
- model: where the stats are updated (update method)
|
||||
- view: where the stats are prepare to be displayed (update_views)
|
||||
- controler: where the stats are displayed (msg_curse method)
|
||||
|
||||
A plugin should define the following global variables:
|
||||
|
||||
- fields_description: a dict twith the field description/option
|
||||
- items_history_list: define items history
|
||||
- items_history_list (optional): define items history
|
||||
|
||||
A plugin should implement the following methods:
|
||||
|
||||
- update(): update the self.stats variable (most of the time a dict or a list of dict)
|
||||
- msg_curse(): return a list of messages to display in UI
|
||||
|
||||
Have a look of all Glances plugin's methods in the plugin.py file.
|
||||
Have a look of all Glances plugin's methods in the plugin folder (where the GlancesPluginModel is defined).
|
||||
|
@ -0,0 +1,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
|
@ -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
|
@ -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
|
@ -1,123 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""Monitor plugin."""
|
||||
|
||||
from glances.globals import iteritems
|
||||
from glances.amps_list import AmpsList as glancesAmpsList
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Glances AMPs plugin."""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[])
|
||||
self.args = args
|
||||
self.config = config
|
||||
|
||||
# We want to display the stat in the curse interface
|
||||
self.display_curse = True
|
||||
|
||||
# Init the list of AMP (classes define in the glances/amps_list.py script)
|
||||
self.glances_amps = glancesAmpsList(self.args, self.config)
|
||||
|
||||
def get_key(self):
|
||||
"""Return the key of the list."""
|
||||
return 'name'
|
||||
|
||||
@GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update the AMP list."""
|
||||
# Init new stats
|
||||
stats = self.get_init_value()
|
||||
|
||||
if self.input_method == 'local':
|
||||
for k, v in iteritems(self.glances_amps.update()):
|
||||
stats.append(
|
||||
{
|
||||
'key': self.get_key(),
|
||||
'name': v.NAME,
|
||||
'result': v.result(),
|
||||
'refresh': v.refresh(),
|
||||
'timer': v.time_until_refresh(),
|
||||
'count': v.count(),
|
||||
'countmin': v.count_min(),
|
||||
'countmax': v.count_max(),
|
||||
'regex': v.regex() is not None,
|
||||
},
|
||||
)
|
||||
else:
|
||||
# Not available in SNMP mode
|
||||
pass
|
||||
|
||||
# Update the stats
|
||||
self.stats = stats
|
||||
|
||||
return self.stats
|
||||
|
||||
def get_alert(self, nbprocess=0, countmin=None, countmax=None, header="", log=False):
|
||||
"""Return the alert status relative to the process number."""
|
||||
if nbprocess is None:
|
||||
return 'OK'
|
||||
if countmin is None:
|
||||
countmin = nbprocess
|
||||
if countmax is None:
|
||||
countmax = nbprocess
|
||||
if nbprocess > 0:
|
||||
if int(countmin) <= int(nbprocess) <= int(countmax):
|
||||
return 'OK'
|
||||
else:
|
||||
return 'WARNING'
|
||||
else:
|
||||
if int(countmin) == 0:
|
||||
return 'OK'
|
||||
else:
|
||||
return 'CRITICAL'
|
||||
|
||||
def msg_curse(self, args=None, max_width=None):
|
||||
"""Return the dict to display in the curse interface."""
|
||||
# Init the return message
|
||||
# Only process if stats exist and display plugin enable...
|
||||
ret = []
|
||||
|
||||
if not self.stats or args.disable_process or self.is_disabled():
|
||||
return ret
|
||||
|
||||
# Build the string message
|
||||
for m in self.stats:
|
||||
# Only display AMP if a result exist
|
||||
if m['result'] is None:
|
||||
continue
|
||||
# Display AMP
|
||||
first_column = '{}'.format(m['name'])
|
||||
first_column_style = self.get_alert(m['count'], m['countmin'], m['countmax'])
|
||||
second_column = '{}'.format(m['count'] if m['regex'] else '')
|
||||
for line in m['result'].split('\n'):
|
||||
# Display first column with the process name...
|
||||
msg = '{:<16} '.format(first_column)
|
||||
ret.append(self.curse_add_line(msg, first_column_style))
|
||||
# ... and second column with the number of matching processes...
|
||||
msg = '{:<4} '.format(second_column)
|
||||
ret.append(self.curse_add_line(msg))
|
||||
# ... only on the first line
|
||||
first_column = second_column = ''
|
||||
# Display AMP result in the third column
|
||||
ret.append(self.curse_add_line(line, splittable=True))
|
||||
ret.append(self.curse_new_line())
|
||||
|
||||
# Delete the last empty line
|
||||
try:
|
||||
ret.pop()
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return ret
|
@ -0,0 +1,220 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""Cloud plugin.
|
||||
|
||||
Supported Cloud API:
|
||||
- OpenStack meta data (class ThreadOpenStack) - Vanilla OpenStack
|
||||
- OpenStackEC2 meta data (class ThreadOpenStackEC2) - Amazon EC2 compatible
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from glances.globals import iteritems, to_ascii
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
from glances.logger import logger
|
||||
|
||||
# Import plugin specific dependency
|
||||
try:
|
||||
import requests
|
||||
except ImportError as e:
|
||||
import_error_tag = True
|
||||
# Display debug message if import error
|
||||
logger.warning("Missing Python Lib ({}), Cloud plugin is disabled".format(e))
|
||||
else:
|
||||
import_error_tag = False
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Glances' cloud plugin.
|
||||
|
||||
The goal of this plugin is to retrieve additional information
|
||||
concerning the datacenter where the host is connected.
|
||||
|
||||
See https://github.com/nicolargo/glances/issues/1029
|
||||
|
||||
stats is a dict
|
||||
"""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(args=args, config=config)
|
||||
|
||||
# We want to display the stat in the curse interface
|
||||
self.display_curse = True
|
||||
|
||||
# Init the stats
|
||||
self.reset()
|
||||
|
||||
# Init thread to grab OpenStack stats asynchronously
|
||||
self.OPENSTACK = ThreadOpenStack()
|
||||
self.OPENSTACKEC2 = ThreadOpenStackEC2()
|
||||
|
||||
# Run the thread
|
||||
self.OPENSTACK.start()
|
||||
self.OPENSTACKEC2.start()
|
||||
|
||||
def exit(self):
|
||||
"""Overwrite the exit method to close threads."""
|
||||
self.OPENSTACK.stop()
|
||||
self.OPENSTACKEC2.stop()
|
||||
# Call the father class
|
||||
super(PluginModel, self).exit()
|
||||
|
||||
@GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update the cloud stats.
|
||||
|
||||
Return the stats (dict)
|
||||
"""
|
||||
# Init new stats
|
||||
stats = self.get_init_value()
|
||||
|
||||
# Requests lib is needed to get stats from the Cloud API
|
||||
if import_error_tag:
|
||||
return stats
|
||||
|
||||
# Update the stats
|
||||
if self.input_method == 'local':
|
||||
stats = self.OPENSTACK.stats
|
||||
if not stats:
|
||||
stats = self.OPENSTACKEC2.stats
|
||||
# Example:
|
||||
# Uncomment to test on physical computer (only for test purpose)
|
||||
# stats = {'id': 'ami-id',
|
||||
# 'name': 'My VM',
|
||||
# 'type': 'Gold',
|
||||
# 'region': 'France',
|
||||
# 'platform': 'OpenStack'}
|
||||
|
||||
# Update the stats
|
||||
self.stats = stats
|
||||
|
||||
return self.stats
|
||||
|
||||
def msg_curse(self, args=None, max_width=None):
|
||||
"""Return the string to display in the curse interface."""
|
||||
# Init the return message
|
||||
ret = []
|
||||
|
||||
if not self.stats or self.stats == {} or self.is_disabled():
|
||||
return ret
|
||||
|
||||
# Generate the output
|
||||
msg = self.stats.get('platform', 'Unknown')
|
||||
ret.append(self.curse_add_line(msg, "TITLE"))
|
||||
msg = ' {} instance {} ({})'.format(
|
||||
self.stats.get('type', 'Unknown'), self.stats.get('name', 'Unknown'), self.stats.get('region', 'Unknown')
|
||||
)
|
||||
ret.append(self.curse_add_line(msg))
|
||||
|
||||
# Return the message with decoration
|
||||
# logger.info(ret)
|
||||
return ret
|
||||
|
||||
|
||||
class ThreadOpenStack(threading.Thread):
|
||||
"""
|
||||
Specific thread to grab OpenStack stats.
|
||||
|
||||
stats is a dict
|
||||
"""
|
||||
|
||||
# The metadata service provides a way for instances to retrieve
|
||||
# instance-specific data via a REST API. Instances access this
|
||||
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
|
||||
# All types of metadata, be it user-, nova- or vendor-provided,
|
||||
# can be accessed via this service.
|
||||
# https://docs.openstack.org/nova/latest/user/metadata-service.html
|
||||
OPENSTACK_PLATFORM = "OpenStack"
|
||||
OPENSTACK_API_URL = 'http://169.254.169.254/openstack/latest/meta-data'
|
||||
OPENSTACK_API_METADATA = {
|
||||
'id': 'project_id',
|
||||
'name': 'name',
|
||||
'type': 'meta/role',
|
||||
'region': 'availability_zone',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Init the class."""
|
||||
logger.debug("cloud plugin - Create thread for OpenStack metadata")
|
||||
super(ThreadOpenStack, self).__init__()
|
||||
# Event needed to stop properly the thread
|
||||
self._stopper = threading.Event()
|
||||
# The class return the stats as a dict
|
||||
self._stats = {}
|
||||
|
||||
def run(self):
|
||||
"""Grab plugin's stats.
|
||||
|
||||
Infinite loop, should be stopped by calling the stop() method
|
||||
"""
|
||||
if import_error_tag:
|
||||
self.stop()
|
||||
return False
|
||||
|
||||
for k, v in iteritems(self.OPENSTACK_API_METADATA):
|
||||
r_url = '{}/{}'.format(self.OPENSTACK_API_URL, v)
|
||||
try:
|
||||
# Local request, a timeout of 3 seconds is OK
|
||||
r = requests.get(r_url, timeout=3)
|
||||
except Exception as e:
|
||||
logger.debug('cloud plugin - Cannot connect to the OpenStack metadata API {}: {}'.format(r_url, e))
|
||||
break
|
||||
else:
|
||||
if r.ok:
|
||||
self._stats[k] = to_ascii(r.content)
|
||||
else:
|
||||
# No break during the loop, so we can set the platform
|
||||
self._stats['platform'] = self.OPENSTACK_PLATFORM
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def stats(self):
|
||||
"""Stats getter."""
|
||||
return self._stats
|
||||
|
||||
@stats.setter
|
||||
def stats(self, value):
|
||||
"""Stats setter."""
|
||||
self._stats = value
|
||||
|
||||
def stop(self, timeout=None):
|
||||
"""Stop the thread."""
|
||||
logger.debug("cloud plugin - Close thread for OpenStack metadata")
|
||||
self._stopper.set()
|
||||
|
||||
def stopped(self):
|
||||
"""Return True is the thread is stopped."""
|
||||
return self._stopper.is_set()
|
||||
|
||||
|
||||
class ThreadOpenStackEC2(ThreadOpenStack):
|
||||
"""
|
||||
Specific thread to grab OpenStack EC2 (Amazon cloud) stats.
|
||||
|
||||
stats is a dict
|
||||
"""
|
||||
|
||||
# The metadata service provides a way for instances to retrieve
|
||||
# instance-specific data via a REST API. Instances access this
|
||||
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
|
||||
# All types of metadata, be it user-, nova- or vendor-provided,
|
||||
# can be accessed via this service.
|
||||
# https://docs.openstack.org/nova/latest/user/metadata-service.html
|
||||
OPENSTACK_PLATFORM = "Amazon EC2"
|
||||
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data'
|
||||
OPENSTACK_API_METADATA = {
|
||||
'id': 'ami-id',
|
||||
'name': 'instance-id',
|
||||
'type': 'instance-type',
|
||||
'region': 'placement/availability-zone',
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""Cloud plugin.
|
||||
|
||||
Supported Cloud API:
|
||||
- OpenStack meta data (class ThreadOpenStack) - Vanilla OpenStack
|
||||
- OpenStackEC2 meta data (class ThreadOpenStackEC2) - Amazon EC2 compatible
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from glances.globals import iteritems, to_ascii
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
from glances.logger import logger
|
||||
|
||||
# Import plugin specific dependency
|
||||
try:
|
||||
import requests
|
||||
except ImportError as e:
|
||||
import_error_tag = True
|
||||
# Display debug message if import error
|
||||
logger.warning("Missing Python Lib ({}), Cloud plugin is disabled".format(e))
|
||||
else:
|
||||
import_error_tag = False
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Glances' cloud plugin.
|
||||
|
||||
The goal of this plugin is to retrieve additional information
|
||||
concerning the datacenter where the host is connected.
|
||||
|
||||
See https://github.com/nicolargo/glances/issues/1029
|
||||
|
||||
stats is a dict
|
||||
"""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(args=args, config=config)
|
||||
|
||||
# We want to display the stat in the curse interface
|
||||
self.display_curse = True
|
||||
|
||||
# Init the stats
|
||||
self.reset()
|
||||
|
||||
# Init thread to grab OpenStack stats asynchronously
|
||||
self.OPENSTACK = ThreadOpenStack()
|
||||
self.OPENSTACKEC2 = ThreadOpenStackEC2()
|
||||
|
||||
# Run the thread
|
||||
self.OPENSTACK.start()
|
||||
self.OPENSTACKEC2.start()
|
||||
|
||||
def exit(self):
|
||||
"""Overwrite the exit method to close threads."""
|
||||
self.OPENSTACK.stop()
|
||||
self.OPENSTACKEC2.stop()
|
||||
# Call the father class
|
||||
super(PluginModel, self).exit()
|
||||
|
||||
@GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update the cloud stats.
|
||||
|
||||
Return the stats (dict)
|
||||
"""
|
||||
# Init new stats
|
||||
stats = self.get_init_value()
|
||||
|
||||
# Requests lib is needed to get stats from the Cloud API
|
||||
if import_error_tag:
|
||||
return stats
|
||||
|
||||
# Update the stats
|
||||
if self.input_method == 'local':
|
||||
stats = self.OPENSTACK.stats
|
||||
if not stats:
|
||||
stats = self.OPENSTACKEC2.stats
|
||||
# Example:
|
||||
# Uncomment to test on physical computer (only for test purpose)
|
||||
# stats = {'id': 'ami-id',
|
||||
# 'name': 'My VM',
|
||||
# 'type': 'Gold',
|
||||
# 'region': 'France',
|
||||
# 'platform': 'OpenStack'}
|
||||
|
||||
# Update the stats
|
||||
self.stats = stats
|
||||
|
||||
return self.stats
|
||||
|
||||
def msg_curse(self, args=None, max_width=None):
|
||||
"""Return the string to display in the curse interface."""
|
||||
# Init the return message
|
||||
ret = []
|
||||
|
||||
if not self.stats or self.stats == {} or self.is_disabled():
|
||||
return ret
|
||||
|
||||
# Generate the output
|
||||
msg = self.stats.get('platform', 'Unknown')
|
||||
ret.append(self.curse_add_line(msg, "TITLE"))
|
||||
msg = ' {} instance {} ({})'.format(
|
||||
self.stats.get('type', 'Unknown'), self.stats.get('name', 'Unknown'), self.stats.get('region', 'Unknown')
|
||||
)
|
||||
ret.append(self.curse_add_line(msg))
|
||||
|
||||
# Return the message with decoration
|
||||
# logger.info(ret)
|
||||
return ret
|
||||
|
||||
|
||||
class ThreadOpenStack(threading.Thread):
|
||||
"""
|
||||
Specific thread to grab OpenStack stats.
|
||||
|
||||
stats is a dict
|
||||
"""
|
||||
|
||||
# The metadata service provides a way for instances to retrieve
|
||||
# instance-specific data via a REST API. Instances access this
|
||||
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
|
||||
# All types of metadata, be it user-, nova- or vendor-provided,
|
||||
# can be accessed via this service.
|
||||
# https://docs.openstack.org/nova/latest/user/metadata-service.html
|
||||
OPENSTACK_PLATFORM = "OpenStack"
|
||||
OPENSTACK_API_URL = 'http://169.254.169.254/openstack/latest/meta-data'
|
||||
OPENSTACK_API_METADATA = {
|
||||
'id': 'project_id',
|
||||
'name': 'name',
|
||||
'type': 'meta/role',
|
||||
'region': 'availability_zone',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Init the class."""
|
||||
logger.debug("cloud plugin - Create thread for OpenStack metadata")
|
||||
super(ThreadOpenStack, self).__init__()
|
||||
# Event needed to stop properly the thread
|
||||
self._stopper = threading.Event()
|
||||
# The class return the stats as a dict
|
||||
self._stats = {}
|
||||
|
||||
def run(self):
|
||||
"""Grab plugin's stats.
|
||||
|
||||
Infinite loop, should be stopped by calling the stop() method
|
||||
"""
|
||||
if import_error_tag:
|
||||
self.stop()
|
||||
return False
|
||||
|
||||
for k, v in iteritems(self.OPENSTACK_API_METADATA):
|
||||
r_url = '{}/{}'.format(self.OPENSTACK_API_URL, v)
|
||||
try:
|
||||
# Local request, a timeout of 3 seconds is OK
|
||||
r = requests.get(r_url, timeout=3)
|
||||
except Exception as e:
|
||||
logger.debug('cloud plugin - Cannot connect to the OpenStack metadata API {}: {}'.format(r_url, e))
|
||||
break
|
||||
else:
|
||||
if r.ok:
|
||||
self._stats[k] = to_ascii(r.content)
|
||||
else:
|
||||
# No break during the loop, so we can set the platform
|
||||
self._stats['platform'] = self.OPENSTACK_PLATFORM
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def stats(self):
|
||||
"""Stats getter."""
|
||||
return self._stats
|
||||
|
||||
@stats.setter
|
||||
def stats(self, value):
|
||||
"""Stats setter."""
|
||||
self._stats = value
|
||||
|
||||
def stop(self, timeout=None):
|
||||
"""Stop the thread."""
|
||||
logger.debug("cloud plugin - Close thread for OpenStack metadata")
|
||||
self._stopper.set()
|
||||
|
||||
def stopped(self):
|
||||
"""Return True is the thread is stopped."""
|
||||
return self._stopper.is_set()
|
||||
|
||||
|
||||
class ThreadOpenStackEC2(ThreadOpenStack):
|
||||
"""
|
||||
Specific thread to grab OpenStack EC2 (Amazon cloud) stats.
|
||||
|
||||
stats is a dict
|
||||
"""
|
||||
|
||||
# The metadata service provides a way for instances to retrieve
|
||||
# instance-specific data via a REST API. Instances access this
|
||||
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
|
||||
# All types of metadata, be it user-, nova- or vendor-provided,
|
||||
# can be accessed via this service.
|
||||
# https://docs.openstack.org/nova/latest/user/metadata-service.html
|
||||
OPENSTACK_PLATFORM = "Amazon EC2"
|
||||
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data'
|
||||
OPENSTACK_API_METADATA = {
|
||||
'id': 'ami-id',
|
||||
'name': 'instance-id',
|
||||
'type': 'instance-type',
|
||||
'region': 'placement/availability-zone',
|
||||
}
|
@ -0,0 +1,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
|
@ -1,176 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""Connections plugin."""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from glances.logger import logger
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
from glances.globals import nativestr
|
||||
|
||||
import psutil
|
||||
|
||||
# Define the history items list
|
||||
# items_history_list = [{'name': 'rx',
|
||||
# 'description': 'Download rate per second',
|
||||
# 'y_unit': 'bit/s'},
|
||||
# {'name': 'tx',
|
||||
# 'description': 'Upload rate per second',
|
||||
# 'y_unit': 'bit/s'}]
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Glances connections plugin.
|
||||
|
||||
stats is a dict
|
||||
"""
|
||||
|
||||
status_list = [psutil.CONN_LISTEN, psutil.CONN_ESTABLISHED]
|
||||
initiated_states = [psutil.CONN_SYN_SENT, psutil.CONN_SYN_RECV]
|
||||
terminated_states = [
|
||||
psutil.CONN_FIN_WAIT1,
|
||||
psutil.CONN_FIN_WAIT2,
|
||||
psutil.CONN_TIME_WAIT,
|
||||
psutil.CONN_CLOSE,
|
||||
psutil.CONN_CLOSE_WAIT,
|
||||
psutil.CONN_LAST_ACK,
|
||||
]
|
||||
conntrack = {
|
||||
'nf_conntrack_count': '/proc/sys/net/netfilter/nf_conntrack_count',
|
||||
'nf_conntrack_max': '/proc/sys/net/netfilter/nf_conntrack_max',
|
||||
}
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(
|
||||
args=args,
|
||||
config=config,
|
||||
# items_history_list=items_history_list,
|
||||
stats_init_value={'net_connections_enabled': True, 'nf_conntrack_enabled': True},
|
||||
)
|
||||
|
||||
# We want to display the stat in the curse interface
|
||||
self.display_curse = True
|
||||
|
||||
@GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update connections stats using the input method.
|
||||
|
||||
Stats is a dict
|
||||
"""
|
||||
# Init new stats
|
||||
stats = self.get_init_value()
|
||||
|
||||
if self.input_method == 'local':
|
||||
# Update stats using the PSUtils lib
|
||||
|
||||
# Grab network interface stat using the psutil net_connections method
|
||||
if stats['net_connections_enabled']:
|
||||
try:
|
||||
net_connections = psutil.net_connections(kind="tcp")
|
||||
except Exception as e:
|
||||
logger.warning('Can not get network connections stats ({})'.format(e))
|
||||
logger.info('Disable connections stats')
|
||||
stats['net_connections_enabled'] = False
|
||||
self.stats = stats
|
||||
return self.stats
|
||||
|
||||
for s in self.status_list:
|
||||
stats[s] = len([c for c in net_connections if c.status == s])
|
||||
initiated = 0
|
||||
for s in self.initiated_states:
|
||||
stats[s] = len([c for c in net_connections if c.status == s])
|
||||
initiated += stats[s]
|
||||
stats['initiated'] = initiated
|
||||
terminated = 0
|
||||
for s in self.initiated_states:
|
||||
stats[s] = len([c for c in net_connections if c.status == s])
|
||||
terminated += stats[s]
|
||||
stats['terminated'] = terminated
|
||||
|
||||
if stats['nf_conntrack_enabled']:
|
||||
# Grab connections track directly from the /proc file
|
||||
for i in self.conntrack:
|
||||
try:
|
||||
with open(self.conntrack[i], 'r') as f:
|
||||
stats[i] = float(f.readline().rstrip("\n"))
|
||||
except (IOError, FileNotFoundError) as e:
|
||||
logger.warning('Can not get network connections track ({})'.format(e))
|
||||
logger.info('Disable connections track')
|
||||
stats['nf_conntrack_enabled'] = False
|
||||
self.stats = stats
|
||||
return self.stats
|
||||
if 'nf_conntrack_max' in stats and 'nf_conntrack_count' in stats:
|
||||
stats['nf_conntrack_percent'] = stats['nf_conntrack_count'] * 100 / stats['nf_conntrack_max']
|
||||
else:
|
||||
stats['nf_conntrack_enabled'] = False
|
||||
self.stats = stats
|
||||
return self.stats
|
||||
|
||||
elif self.input_method == 'snmp':
|
||||
# Update stats using SNMP
|
||||
pass
|
||||
|
||||
# Update the stats
|
||||
self.stats = stats
|
||||
return self.stats
|
||||
|
||||
def update_views(self):
|
||||
"""Update stats views."""
|
||||
# Call the father's method
|
||||
super(PluginModel, self).update_views()
|
||||
|
||||
# Add specific information
|
||||
try:
|
||||
# Alert and log
|
||||
if self.stats['nf_conntrack_enabled']:
|
||||
self.views['nf_conntrack_percent']['decoration'] = self.get_alert(header='nf_conntrack_percent')
|
||||
except KeyError:
|
||||
# try/except mandatory for Windows compatibility (no conntrack stats)
|
||||
pass
|
||||
|
||||
def msg_curse(self, args=None, max_width=None):
|
||||
"""Return the dict to display in the curse interface."""
|
||||
# Init the return message
|
||||
ret = []
|
||||
|
||||
# Only process if stats exist and display plugin enable...
|
||||
if not self.stats or self.is_disabled():
|
||||
return ret
|
||||
|
||||
# Header
|
||||
if self.stats['net_connections_enabled'] or self.stats['nf_conntrack_enabled']:
|
||||
msg = '{}'.format('TCP CONNECTIONS')
|
||||
ret.append(self.curse_add_line(msg, "TITLE"))
|
||||
# Connections status
|
||||
if self.stats['net_connections_enabled']:
|
||||
for s in [psutil.CONN_LISTEN, 'initiated', psutil.CONN_ESTABLISHED, 'terminated']:
|
||||
ret.append(self.curse_new_line())
|
||||
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = '{:>{width}}'.format(self.stats[s], width=max_width - len(s) + 2)
|
||||
ret.append(self.curse_add_line(msg))
|
||||
# Connections track
|
||||
if (
|
||||
self.stats['nf_conntrack_enabled']
|
||||
and 'nf_conntrack_count' in self.stats
|
||||
and 'nf_conntrack_max' in self.stats
|
||||
):
|
||||
s = 'Tracked'
|
||||
ret.append(self.curse_new_line())
|
||||
msg = '{:{width}}'.format(nativestr(s).capitalize(), width=len(s))
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = '{:>{width}}'.format(
|
||||
'{:0.0f}/{:0.0f}'.format(self.stats['nf_conntrack_count'], self.stats['nf_conntrack_max']),
|
||||
width=max_width - len(s) + 2,
|
||||
)
|
||||
ret.append(self.curse_add_line(msg, self.get_views(key='nf_conntrack_percent', option='decoration')))
|
||||
|
||||
return ret
|
@ -0,0 +1,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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,430 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""Containers plugin."""
|
||||
|
||||
import os
|
||||
from copy import deepcopy
|
||||
|
||||
from glances.logger import logger
|
||||
from glances.plugins.containers.engines.docker import DockerContainersExtension, import_docker_error_tag
|
||||
from glances.plugins.containers.engines.podman import PodmanContainersExtension, import_podman_error_tag
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
from glances.processes import glances_processes
|
||||
from glances.processes import sort_stats as sort_stats_processes
|
||||
|
||||
# Define the items history list (list of items to add to history)
|
||||
# TODO: For the moment limited to the CPU. Had to change the graph exports
|
||||
# method to display one graph per container.
|
||||
# items_history_list = [{'name': 'cpu_percent',
|
||||
# 'description': 'Container CPU consumption in %',
|
||||
# 'y_unit': '%'},
|
||||
# {'name': 'memory_usage',
|
||||
# 'description': 'Container memory usage in bytes',
|
||||
# 'y_unit': 'B'},
|
||||
# {'name': 'network_rx',
|
||||
# 'description': 'Container network RX bitrate in bits per second',
|
||||
# 'y_unit': 'bps'},
|
||||
# {'name': 'network_tx',
|
||||
# 'description': 'Container network TX bitrate in bits per second',
|
||||
# 'y_unit': 'bps'},
|
||||
# {'name': 'io_r',
|
||||
# 'description': 'Container IO bytes read per second',
|
||||
# 'y_unit': 'Bps'},
|
||||
# {'name': 'io_w',
|
||||
# 'description': 'Container IO bytes write per second',
|
||||
# 'y_unit': 'Bps'}]
|
||||
items_history_list = [{'name': 'cpu_percent', 'description': 'Container CPU consumption in %', 'y_unit': '%'}]
|
||||
|
||||
# List of key to remove before export
|
||||
export_exclude_list = ['cpu', 'io', 'memory', 'network']
|
||||
|
||||
# Sort dictionary for human
|
||||
sort_for_human = {
|
||||
'io_counters': 'disk IO',
|
||||
'cpu_percent': 'CPU consumption',
|
||||
'memory_usage': 'memory consumption',
|
||||
'cpu_times': 'uptime',
|
||||
'name': 'container name',
|
||||
None: 'None',
|
||||
}
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Glances Docker plugin.
|
||||
|
||||
stats is a dict: {'version': {...}, 'containers': [{}, {}]}
|
||||
"""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(args=args, config=config, items_history_list=items_history_list)
|
||||
|
||||
# The plugin can be disabled using: args.disable_docker
|
||||
self.args = args
|
||||
|
||||
# Default config keys
|
||||
self.config = config
|
||||
|
||||
# We want to display the stat in the curse interface
|
||||
self.display_curse = True
|
||||
|
||||
# Init the Docker API
|
||||
self.docker_extension = DockerContainersExtension() if not import_docker_error_tag else None
|
||||
|
||||
# Init the Podman API
|
||||
if import_podman_error_tag:
|
||||
self.podman_client = None
|
||||
else:
|
||||
self.podman_client = PodmanContainersExtension(podman_sock=self._podman_sock())
|
||||
|
||||
# Sort key
|
||||
self.sort_key = None
|
||||
|
||||
# Force a first update because we need two update to have the first stat
|
||||
self.update()
|
||||
self.refresh_timer.set(0)
|
||||
|
||||
def _podman_sock(self):
|
||||
"""Return the podman sock.
|
||||
Could be desfined in the [docker] section thanks to the podman_sock option.
|
||||
Default value: unix:///run/user/1000/podman/podman.sock
|
||||
"""
|
||||
conf_podman_sock = self.get_conf_value('podman_sock')
|
||||
if len(conf_podman_sock) == 0:
|
||||
return "unix:///run/user/1000/podman/podman.sock"
|
||||
else:
|
||||
return conf_podman_sock[0]
|
||||
|
||||
def exit(self):
|
||||
"""Overwrite the exit method to close threads."""
|
||||
if self.docker_extension:
|
||||
self.docker_extension.stop()
|
||||
if self.podman_client:
|
||||
self.podman_client.stop()
|
||||
# Call the father class
|
||||
super(PluginModel, self).exit()
|
||||
|
||||
def get_key(self):
|
||||
"""Return the key of the list."""
|
||||
return 'name'
|
||||
|
||||
def get_export(self):
|
||||
"""Overwrite the default export method.
|
||||
|
||||
- Only exports containers
|
||||
- The key is the first container name
|
||||
"""
|
||||
try:
|
||||
ret = deepcopy(self.stats['containers'])
|
||||
except KeyError as e:
|
||||
logger.debug("docker plugin - Docker export error {}".format(e))
|
||||
ret = []
|
||||
|
||||
# Remove fields uses to compute rate
|
||||
for container in ret:
|
||||
for i in export_exclude_list:
|
||||
container.pop(i)
|
||||
|
||||
return ret
|
||||
|
||||
def _all_tag(self):
|
||||
"""Return the all tag of the Glances/Docker configuration file.
|
||||
|
||||
# By default, Glances only display running containers
|
||||
# Set the following key to True to display all containers
|
||||
all=True
|
||||
"""
|
||||
all_tag = self.get_conf_value('all')
|
||||
if len(all_tag) == 0:
|
||||
return False
|
||||
else:
|
||||
return all_tag[0].lower() == 'true'
|
||||
|
||||
@GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update Docker and podman stats using the input method."""
|
||||
# Connection should be ok
|
||||
if self.docker_extension is None and self.podman_client is None:
|
||||
return self.get_init_value()
|
||||
|
||||
if self.input_method == 'local':
|
||||
# Update stats
|
||||
stats_docker = self.update_docker() if self.docker_extension else {}
|
||||
stats_podman = self.update_podman() if self.podman_client else {}
|
||||
stats = {
|
||||
'version': stats_docker.get('version', {}),
|
||||
'version_podman': stats_podman.get('version', {}),
|
||||
'containers': stats_docker.get('containers', []) + stats_podman.get('containers', []),
|
||||
}
|
||||
elif self.input_method == 'snmp':
|
||||
# Update stats using SNMP
|
||||
# Not available
|
||||
pass
|
||||
|
||||
# Sort and update the stats
|
||||
# @TODO: Have a look because sort did not work for the moment (need memory stats ?)
|
||||
self.sort_key, self.stats = sort_docker_stats(stats)
|
||||
|
||||
return self.stats
|
||||
|
||||
def update_docker(self):
|
||||
"""Update Docker stats using the input method."""
|
||||
version, containers = self.docker_extension.update(all_tag=self._all_tag())
|
||||
for container in containers:
|
||||
container["engine"] = 'docker'
|
||||
return {"version": version, "containers": containers}
|
||||
|
||||
def update_podman(self):
|
||||
"""Update Podman stats."""
|
||||
version, containers = self.podman_client.update(all_tag=self._all_tag())
|
||||
for container in containers:
|
||||
container["engine"] = 'podman'
|
||||
return {"version": version, "containers": containers}
|
||||
|
||||
def get_user_ticks(self):
|
||||
"""Return the user ticks by reading the environment variable."""
|
||||
return os.sysconf(os.sysconf_names['SC_CLK_TCK'])
|
||||
|
||||
def get_stats_action(self):
|
||||
"""Return stats for the action.
|
||||
|
||||
Docker will return self.stats['containers']
|
||||
"""
|
||||
return self.stats['containers']
|
||||
|
||||
def update_views(self):
|
||||
"""Update stats views."""
|
||||
# Call the father's method
|
||||
super(PluginModel, self).update_views()
|
||||
|
||||
if 'containers' not in self.stats:
|
||||
return False
|
||||
|
||||
# Add specifics information
|
||||
# Alert
|
||||
for i in self.stats['containers']:
|
||||
# Init the views for the current container (key = container name)
|
||||
self.views[i[self.get_key()]] = {'cpu': {}, 'mem': {}}
|
||||
# CPU alert
|
||||
if 'cpu' in i and 'total' in i['cpu']:
|
||||
# Looking for specific CPU container threshold in the conf file
|
||||
alert = self.get_alert(i['cpu']['total'], header=i['name'] + '_cpu', action_key=i['name'])
|
||||
if alert == 'DEFAULT':
|
||||
# Not found ? Get back to default CPU threshold value
|
||||
alert = self.get_alert(i['cpu']['total'], header='cpu')
|
||||
self.views[i[self.get_key()]]['cpu']['decoration'] = alert
|
||||
# MEM alert
|
||||
if 'memory' in i and 'usage' in i['memory']:
|
||||
# Looking for specific MEM container threshold in the conf file
|
||||
alert = self.get_alert(
|
||||
i['memory']['usage'], maximum=i['memory']['limit'], header=i['name'] + '_mem', action_key=i['name']
|
||||
)
|
||||
if alert == 'DEFAULT':
|
||||
# Not found ? Get back to default MEM threshold value
|
||||
alert = self.get_alert(i['memory']['usage'], maximum=i['memory']['limit'], header='mem')
|
||||
self.views[i[self.get_key()]]['mem']['decoration'] = alert
|
||||
|
||||
return True
|
||||
|
||||
def msg_curse(self, args=None, max_width=None):
|
||||
"""Return the dict to display in the curse interface."""
|
||||
# Init the return message
|
||||
ret = []
|
||||
|
||||
# Only process if stats exist (and non null) and display plugin enable...
|
||||
if not self.stats or 'containers' not in self.stats or len(self.stats['containers']) == 0 or self.is_disabled():
|
||||
return ret
|
||||
|
||||
show_pod_name = False
|
||||
if any(ct.get("pod_name") for ct in self.stats["containers"]):
|
||||
show_pod_name = True
|
||||
|
||||
show_engine_name = False
|
||||
if len(set(ct["engine"] for ct in self.stats["containers"])) > 1:
|
||||
show_engine_name = True
|
||||
|
||||
# Build the string message
|
||||
# Title
|
||||
msg = '{}'.format('CONTAINERS')
|
||||
ret.append(self.curse_add_line(msg, "TITLE"))
|
||||
msg = ' {}'.format(len(self.stats['containers']))
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = ' sorted by {}'.format(sort_for_human[self.sort_key])
|
||||
ret.append(self.curse_add_line(msg))
|
||||
# msg = ' (served by Docker {})'.format(self.stats['version']["Version"])
|
||||
# ret.append(self.curse_add_line(msg))
|
||||
ret.append(self.curse_new_line())
|
||||
# Header
|
||||
ret.append(self.curse_new_line())
|
||||
# Get the maximum containers name
|
||||
# Max size is configurable. See feature request #1723.
|
||||
name_max_width = min(
|
||||
self.config.get_int_value('containers', 'max_name_size', default=20) if self.config is not None else 20,
|
||||
len(max(self.stats['containers'], key=lambda x: len(x['name']))['name']),
|
||||
)
|
||||
|
||||
if show_engine_name:
|
||||
msg = ' {:{width}}'.format('Engine', width=6)
|
||||
ret.append(self.curse_add_line(msg))
|
||||
if show_pod_name:
|
||||
msg = ' {:{width}}'.format('Pod', width=12)
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = ' {:{width}}'.format('Name', width=name_max_width)
|
||||
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'name' else 'DEFAULT'))
|
||||
msg = '{:>10}'.format('Status')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = '{:>10}'.format('Uptime')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = '{:>6}'.format('CPU%')
|
||||
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'cpu_percent' else 'DEFAULT'))
|
||||
msg = '{:>7}'.format('MEM')
|
||||
ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'memory_usage' else 'DEFAULT'))
|
||||
msg = '/{:<7}'.format('MAX')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = '{:>7}'.format('IOR/s')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = ' {:<7}'.format('IOW/s')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = '{:>7}'.format('Rx/s')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = ' {:<7}'.format('Tx/s')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
msg = ' {:8}'.format('Command')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
|
||||
# Data
|
||||
for container in self.stats['containers']:
|
||||
ret.append(self.curse_new_line())
|
||||
if show_engine_name:
|
||||
ret.append(self.curse_add_line(' {:{width}}'.format(container["engine"], width=6)))
|
||||
if show_pod_name:
|
||||
ret.append(self.curse_add_line(' {:{width}}'.format(container.get("pod_id", "-"), width=12)))
|
||||
# Name
|
||||
ret.append(self.curse_add_line(self._msg_name(container=container, max_width=name_max_width)))
|
||||
# Status
|
||||
status = self.container_alert(container['Status'])
|
||||
msg = '{:>10}'.format(container['Status'][0:10])
|
||||
ret.append(self.curse_add_line(msg, status))
|
||||
# Uptime
|
||||
if container['Uptime']:
|
||||
msg = '{:>10}'.format(container['Uptime'])
|
||||
else:
|
||||
msg = '{:>10}'.format('_')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
# CPU
|
||||
try:
|
||||
msg = '{:>6.1f}'.format(container['cpu']['total'])
|
||||
except KeyError:
|
||||
msg = '{:>6}'.format('_')
|
||||
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='cpu', option='decoration')))
|
||||
# MEM
|
||||
try:
|
||||
msg = '{:>7}'.format(self.auto_unit(container['memory']['usage']))
|
||||
except KeyError:
|
||||
msg = '{:>7}'.format('_')
|
||||
ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='mem', option='decoration')))
|
||||
try:
|
||||
msg = '/{:<7}'.format(self.auto_unit(container['memory']['limit']))
|
||||
except KeyError:
|
||||
msg = '/{:<7}'.format('_')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
# IO R/W
|
||||
unit = 'B'
|
||||
try:
|
||||
value = self.auto_unit(int(container['io']['ior'] // container['io']['time_since_update'])) + unit
|
||||
msg = '{:>7}'.format(value)
|
||||
except KeyError:
|
||||
msg = '{:>7}'.format('_')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
try:
|
||||
value = self.auto_unit(int(container['io']['iow'] // container['io']['time_since_update'])) + unit
|
||||
msg = ' {:<7}'.format(value)
|
||||
except KeyError:
|
||||
msg = ' {:<7}'.format('_')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
# NET RX/TX
|
||||
if args.byte:
|
||||
# Bytes per second (for dummy)
|
||||
to_bit = 1
|
||||
unit = ''
|
||||
else:
|
||||
# Bits per second (for real network administrator | Default)
|
||||
to_bit = 8
|
||||
unit = 'b'
|
||||
try:
|
||||
value = (
|
||||
self.auto_unit(
|
||||
int(container['network']['rx'] // container['network']['time_since_update'] * to_bit)
|
||||
)
|
||||
+ unit
|
||||
)
|
||||
msg = '{:>7}'.format(value)
|
||||
except KeyError:
|
||||
msg = '{:>7}'.format('_')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
try:
|
||||
value = (
|
||||
self.auto_unit(
|
||||
int(container['network']['tx'] // container['network']['time_since_update'] * to_bit)
|
||||
)
|
||||
+ unit
|
||||
)
|
||||
msg = ' {:<7}'.format(value)
|
||||
except KeyError:
|
||||
msg = ' {:<7}'.format('_')
|
||||
ret.append(self.curse_add_line(msg))
|
||||
# Command
|
||||
if container['Command'] is not None:
|
||||
msg = ' {}'.format(' '.join(container['Command']))
|
||||
else:
|
||||
msg = ' {}'.format('_')
|
||||
ret.append(self.curse_add_line(msg, splittable=True))
|
||||
|
||||
return ret
|
||||
|
||||
def _msg_name(self, container, max_width):
|
||||
"""Build the container name."""
|
||||
name = container['name'][:max_width]
|
||||
return ' {:{width}}'.format(name, width=max_width)
|
||||
|
||||
def container_alert(self, status):
|
||||
"""Analyse the container status."""
|
||||
if status == 'running':
|
||||
return 'OK'
|
||||
elif status == 'exited':
|
||||
return 'WARNING'
|
||||
elif status == 'dead':
|
||||
return 'CRITICAL'
|
||||
else:
|
||||
return 'CAREFUL'
|
||||
|
||||
|
||||
def sort_docker_stats(stats):
|
||||
# Sort Docker stats using the same function than processes
|
||||
sort_by = glances_processes.sort_key
|
||||
sort_by_secondary = 'memory_usage'
|
||||
if sort_by == 'memory_percent':
|
||||
sort_by = 'memory_usage'
|
||||
sort_by_secondary = 'cpu_percent'
|
||||
elif sort_by in ['username', 'io_counters', 'cpu_times']:
|
||||
sort_by = 'cpu_percent'
|
||||
|
||||
# Sort docker stats
|
||||
sort_stats_processes(
|
||||
stats['containers'],
|
||||
sorted_by=sort_by,
|
||||
sorted_by_secondary=sort_by_secondary,
|
||||
# Reverse for all but name
|
||||
reverse=glances_processes.sort_key != 'name',
|
||||
)
|
||||
|
||||
# Return the main sort key and the sorted stats
|
||||
return sort_by, stats
|
@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""CPU core plugin."""
|
||||
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
|
||||
import psutil
|
||||
|
||||
# Fields description
|
||||
fields_description = {
|
||||
'phys': {'description': 'Number of physical cores (hyper thread CPUs are excluded).', 'unit': 'number'},
|
||||
'log': {
|
||||
'description': 'Number of logical CPUs. A logical CPU is the number of \
|
||||
physical cores multiplied by the number of threads that can run on each core.',
|
||||
'unit': 'number',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Glances CPU core plugin.
|
||||
|
||||
Get stats about CPU core number.
|
||||
|
||||
stats is integer (number of core)
|
||||
"""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(args=args, config=config, fields_description=fields_description)
|
||||
|
||||
# We dot not want to display the stat in the curse interface
|
||||
# The core number is displayed by the load plugin
|
||||
self.display_curse = False
|
||||
|
||||
# Do *NOT* uncomment the following line
|
||||
# @GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update core stats.
|
||||
|
||||
Stats is a dict (with both physical and log cpu number) instead of a integer.
|
||||
"""
|
||||
# Init new stats
|
||||
stats = self.get_init_value()
|
||||
|
||||
if self.input_method == 'local':
|
||||
# Update stats using the standard system lib
|
||||
|
||||
# The psutil 2.0 include psutil.cpu_count() and psutil.cpu_count(logical=False)
|
||||
# Return a dict with:
|
||||
# - phys: physical cores only (hyper thread CPUs are excluded)
|
||||
# - log: logical CPUs in the system
|
||||
# Return None if undefined
|
||||
try:
|
||||
stats["phys"] = psutil.cpu_count(logical=False)
|
||||
stats["log"] = psutil.cpu_count()
|
||||
except NameError:
|
||||
self.reset()
|
||||
|
||||
elif self.input_method == 'snmp':
|
||||
# Update stats using SNMP
|
||||
# http://stackoverflow.com/questions/5662467/how-to-find-out-the-number-of-cpus-using-snmp
|
||||
pass
|
||||
|
||||
# Update the stats
|
||||
self.stats = stats
|
||||
|
||||
return self.stats
|
@ -1,76 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""CPU core plugin."""
|
||||
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
|
||||
import psutil
|
||||
|
||||
# Fields description
|
||||
fields_description = {
|
||||
'phys': {'description': 'Number of physical cores (hyper thread CPUs are excluded).', 'unit': 'number'},
|
||||
'log': {
|
||||
'description': 'Number of logical CPUs. A logical CPU is the number of \
|
||||
physical cores multiplied by the number of threads that can run on each core.',
|
||||
'unit': 'number',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Glances CPU core plugin.
|
||||
|
||||
Get stats about CPU core number.
|
||||
|
||||
stats is integer (number of core)
|
||||
"""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(args=args, config=config, fields_description=fields_description)
|
||||
|
||||
# We dot not want to display the stat in the curse interface
|
||||
# The core number is displayed by the load plugin
|
||||
self.display_curse = False
|
||||
|
||||
# Do *NOT* uncomment the following line
|
||||
# @GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update core stats.
|
||||
|
||||
Stats is a dict (with both physical and log cpu number) instead of a integer.
|
||||
"""
|
||||
# Init new stats
|
||||
stats = self.get_init_value()
|
||||
|
||||
if self.input_method == 'local':
|
||||
# Update stats using the standard system lib
|
||||
|
||||
# The psutil 2.0 include psutil.cpu_count() and psutil.cpu_count(logical=False)
|
||||
# Return a dict with:
|
||||
# - phys: physical cores only (hyper thread CPUs are excluded)
|
||||
# - log: logical CPUs in the system
|
||||
# Return None if undefined
|
||||
try:
|
||||
stats["phys"] = psutil.cpu_count(logical=False)
|
||||
stats["log"] = psutil.cpu_count()
|
||||
except NameError:
|
||||
self.reset()
|
||||
|
||||
elif self.input_method == 'snmp':
|
||||
# Update stats using SNMP
|
||||
# http://stackoverflow.com/questions/5662467/how-to-find-out-the-number-of-cpus-using-snmp
|
||||
pass
|
||||
|
||||
# Update the stats
|
||||
self.stats = stats
|
||||
|
||||
return self.stats
|
@ -0,0 +1,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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
Loading…
Reference in New Issue
Block a user