Merge branch 'develop' into issue1985

# Conflicts:
#	optional-requirements.txt
This commit is contained in:
Raz Crimson 2023-04-05 01:11:01 +05:30
commit 50102c1496
42 changed files with 1846 additions and 2132 deletions

View File

@ -8,8 +8,10 @@ env:
DEFAULT_DOCKER_IMAGE: nicolargo/glances DEFAULT_DOCKER_IMAGE: nicolargo/glances
NODE_ENV: ${{ (contains('refs/heads/master', github.ref) || startsWith(github.ref, 'refs/tags/v')) && 'prod' || 'dev' }} NODE_ENV: ${{ (contains('refs/heads/master', github.ref) || startsWith(github.ref, 'refs/tags/v')) && 'prod' || 'dev' }}
PUSH_BRANCH: ${{ 'refs/heads/develop' == github.ref || 'refs/heads/master' == github.ref || startsWith(github.ref, 'refs/tags/v') }} PUSH_BRANCH: ${{ 'refs/heads/develop' == github.ref || 'refs/heads/master' == github.ref || startsWith(github.ref, 'refs/tags/v') }}
# linux/arm/v6 support following issue #2120 # linux/arm/v7 (drop support for v6) support following issue - See issue #2120
DOCKER_PLATFORMS: linux/amd64,linux/arm/v7,linux/arm64,linux/386 DOCKER_PLATFORMS: linux/amd64,linux/arm/v7,linux/arm64,linux/386
# Ubuntu image only support linux/amd64 and linux/arm64 - See issue #2185
DOCKER_PLATFORMS_UBUNTU: linux/amd64,linux/arm64
on: on:
pull_request: pull_request:
@ -96,7 +98,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: ['alpine'] os: ['alpine', 'ubuntu']
tag: ${{ fromJson(needs.create_Docker_builds.outputs.tags) }} tag: ${{ fromJson(needs.create_Docker_builds.outputs.tags) }}
steps: steps:
- name: Checkout - name: Checkout
@ -144,7 +146,7 @@ jobs:
CHANGING_ARG=${{ github.sha }} CHANGING_ARG=${{ github.sha }}
context: . context: .
file: "docker-files/${{ matrix.os }}.Dockerfile" file: "docker-files/${{ matrix.os }}.Dockerfile"
platforms: ${{env.DOCKER_PLATFORMS}} platforms: ${{ matrix.os != 'ubuntu' && env.DOCKER_PLATFORMS || env.DOCKER_PLATFORMS_UBUNTU }}
target: ${{ matrix.tag.target }} target: ${{ matrix.tag.target }}
labels: ${{ steps.docker_meta.outputs.labels }} labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache

View File

@ -133,7 +133,7 @@ flatpak: venv-dev-upgrade ## Generate FlatPack JSON file
# Docker # Docker
# =================================================================== # ===================================================================
docker: docker-alpine ## Generate local docker images docker: docker-alpine docker-ubuntu## Generate local docker images
docker-alpine: ## Generate local docker images (Alpine) docker-alpine: ## Generate local docker images (Alpine)
docker build --target full -f ./docker-files/alpine.Dockerfile -t glances:local-alpine-full . docker build --target full -f ./docker-files/alpine.Dockerfile -t glances:local-alpine-full .
@ -159,22 +159,22 @@ run-local-conf: ## Start Glances in console mode with the system conf file
./venv/bin/python -m glances ./venv/bin/python -m glances
run-docker-alpine-minimal: ## Start Glances Alpine Docker minimal in console mode run-docker-alpine-minimal: ## Start Glances Alpine Docker minimal in console mode
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-alpine-minimal docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-alpine-minimal
run-docker-alpine-full: ## Start Glances Alpine Docker full in console mode run-docker-alpine-full: ## Start Glances Alpine Docker full in console mode
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-alpine-full docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-alpine-full
run-docker-alpine-dev: ## Start Glances Alpine Docker dev in console mode run-docker-alpine-dev: ## Start Glances Alpine Docker dev in console mode
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-alpine-dev docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-alpine-dev
run-docker-ubuntu-minimal: ## Start Glances Ubuntu Docker minimal in console mode run-docker-ubuntu-minimal: ## Start Glances Ubuntu Docker minimal in console mode
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-ubuntu-minimal docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-ubuntu-minimal
run-docker-ubuntu-full: ## Start Glances Ubuntu Docker full in console mode run-docker-ubuntu-full: ## Start Glances Ubuntu Docker full in console mode
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-ubuntu-full docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-ubuntu-full
run-docker-ubuntu-dev: ## Start Glances Ubuntu Docker dev in console mode run-docker-ubuntu-dev: ## Start Glances Ubuntu Docker dev in console mode
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-ubuntu-dev docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it glances:local-ubuntu-dev
run-webserver: ## Start Glances in Web server mode run-webserver: ## Start Glances in Web server mode
./venv/bin/python -m glances -C ./conf/glances.conf -w ./venv/bin/python -m glances -C ./conf/glances.conf -w

View File

@ -220,7 +220,7 @@ Run last version of Glances container in *console mode*:
.. code-block:: console .. code-block:: console
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it nicolargo/glances:latest-full docker run --rm -e TZ="${TZ}" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host --network host -it nicolargo/glances:latest-full
Additionally, if you want to use your own glances.conf file, you can Additionally, if you want to use your own glances.conf file, you can
create your own Dockerfile: create your own Dockerfile:
@ -237,7 +237,7 @@ variable setting parameters for the glances startup command):
.. code-block:: console .. code-block:: console
docker run -v `pwd`/glances.conf:/etc/glances.conf -v /var/run/docker.sock:/var/run/docker.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:/etc/glances.conf -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host -e GLANCES_OPT="-C /etc/glances.conf" -it nicolargo/glances:latest-full
Where \`pwd\`/glances.conf is a local directory containing your glances.conf file. Where \`pwd\`/glances.conf is a local directory containing your glances.conf file.
@ -245,7 +245,7 @@ Run the container in *Web server mode*:
.. code-block:: console .. code-block:: console
docker run -d --restart="always" -p 61208-61209:61208-61209 -e GLANCES_OPT="-w" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host nicolargo/glances:latest-full docker run -d --restart="always" -p 61208-61209:61208-61209 -e TZ="${TZ}" -e GLANCES_OPT="-w" -v /var/run/docker.sock:/var/run/docker.sock:ro --pid host nicolargo/glances:latest-full
GNU/Linux GNU/Linux
--------- ---------

View File

@ -237,12 +237,12 @@ critical=90
#allow=shm #allow=shm
[irq] [irq]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/irq.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/irq.html
# This plugin is disabled by default # This plugin is disabled by default
disable=True disable=True
[folders] [folders]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/folders.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/folders.html
disable=False disable=False
# Define a folder list to monitor # Define a folder list to monitor
# The list is composed of items (list_#nb <= 10) # The list is composed of items (list_#nb <= 10)
@ -263,13 +263,18 @@ disable=False
#folder_3_path=/nonexisting #folder_3_path=/nonexisting
#folder_4_path=/root #folder_4_path=/root
[cloud]
# Documentation: https://glances.readthedocs.io/en/latest/aoa/cloud.html
# This plugin is disabled by default
disable=True
[raid] [raid]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/raid.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/raid.html
# This plugin is disabled by default # This plugin is disabled by default
disable=True disable=True
[smart] [smart]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/smart.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/smart.html
# This plugin is disabled by default # This plugin is disabled by default
disable=True disable=True

View File

@ -21,9 +21,11 @@ services:
privileged: true privileged: true
network_mode: "host" network_mode: "host"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./glances.conf:/glances/conf/glances.conf"
environment: environment:
- "GLANCES_OPT=-w" - GLANCES_OPT: "-C /glances/conf/glances.conf -w"
- TZ: "${TZ}"
labels: labels:
- "traefik.port=61208" - "traefik.port=61208"
- "traefik.frontend.rule=Host:glances.docker.localhost" - "traefik.frontend.rule=Host:glances.docker.localhost"

View File

@ -5,13 +5,12 @@ services:
context: ./ context: ./
dockerfile: Dockerfile dockerfile: Dockerfile
restart: always restart: always
ports:
- "61208:61208"
environment:
GLANCES_OPT: "-C /glances/conf/glances.conf -w"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./glances.conf:/glances/conf/glances.conf"
pid: "host" pid: "host"
privileged: true privileged: true
network_mode: "host" network_mode: "host"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./glances.conf:/glances/conf/glances.conf"
environment:
- GLANCES_OPT: "-C /glances/conf/glances.conf -w"
- TZ: "${TZ}"

View File

@ -237,12 +237,12 @@ critical=90
#allow=shm #allow=shm
[irq] [irq]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/irq.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/irq.html
# This plugin is disabled by default # This plugin is disabled by default
disable=True disable=True
[folders] [folders]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/folders.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/folders.html
disable=False disable=False
# Define a folder list to monitor # Define a folder list to monitor
# The list is composed of items (list_#nb <= 10) # The list is composed of items (list_#nb <= 10)
@ -263,13 +263,18 @@ disable=False
#folder_3_path=/nonexisting #folder_3_path=/nonexisting
#folder_4_path=/root #folder_4_path=/root
[cloud]
# Documentation: https://glances.readthedocs.io/en/latest/aoa/cloud.html
# This plugin is disabled by default
disable=True
[raid] [raid]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/raid.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/raid.html
# This plugin is disabled by default # This plugin is disabled by default
disable=True disable=True
[smart] [smart]
# Documentation: https://glances.readthedocs.io/en/stable/aoa/smart.html # Documentation: https://glances.readthedocs.io/en/latest/aoa/smart.html
# This plugin is disabled by default # This plugin is disabled by default
disable=True disable=True

View File

@ -27,7 +27,8 @@ RUN apk add --no-cache \
lm-sensors \ lm-sensors \
wireless-tools \ wireless-tools \
smartmontools \ smartmontools \
iputils iputils \
tzdata
############################################################################## ##############################################################################
# Install the dependencies beforehand to make them cacheable # Install the dependencies beforehand to make them cacheable
@ -95,7 +96,8 @@ RUN apk add --no-cache \
lm-sensors \ lm-sensors \
wireless-tools \ wireless-tools \
smartmontools \ smartmontools \
iputils iputils \
tzdata
COPY --from=buildRequirements /root/.local/bin /usr/local/bin/ COPY --from=buildRequirements /root/.local/bin /usr/local/bin/
COPY --from=buildRequirements /root/.local/lib/python${PYTHON_VERSION}/site-packages /usr/lib/python${PYTHON_VERSION}/site-packages/ COPY --from=buildRequirements /root/.local/lib/python${PYTHON_VERSION}/site-packages /usr/lib/python${PYTHON_VERSION}/site-packages/

View File

@ -8,11 +8,13 @@
# Ex: Python 3.10 for Ubuntu 22.04 # Ex: Python 3.10 for Ubuntu 22.04
# Note: ENV is for future running containers. ARG for building your Docker image. # Note: ENV is for future running containers. ARG for building your Docker image.
ARG IMAGE_VERSION=12.0.1-base-ubuntu22.04 ARG IMAGE_VERSION=12.1.0-base-ubuntu22.04
ARG PYTHON_VERSION=3.10 ARG PYTHON_VERSION=3.10
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/ ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
FROM nvidia/cuda:${IMAGE_VERSION} as build FROM nvidia/cuda:${IMAGE_VERSION} as build
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
python3 \ python3 \
@ -27,6 +29,7 @@ RUN apt-get update \
wireless-tools \ wireless-tools \
smartmontools \ smartmontools \
net-tools \ net-tools \
tzdata \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@ -92,7 +95,6 @@ FROM nvidia/cuda:${IMAGE_VERSION} as minimal
ARG PYTHON_VERSION ARG PYTHON_VERSION
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
@ -105,6 +107,7 @@ RUN apt-get update \
wireless-tools \ wireless-tools \
smartmontools \ smartmontools \
net-tools \ net-tools \
tzdata \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

BIN
docs/_static/cloud.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
docs/_static/glances-architecture.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
docs/_static/processlist-extended.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

15
docs/aoa/cloud.rst Normal file
View File

@ -0,0 +1,15 @@
.. _cloud:
CLOUD
=====
This plugin diplays information about the cloud provider if your host is running on OpenStack.
The plugin use the standard OpenStack `metadata`_ service to retrieve the information.
This plugin is disable by default, please use the --enable-plugin cloud option
to enable it.
.. image:: ../_static/cloud.png
.. _metadata: https://docs.openstack.org/nova/latest/user/metadata.html

View File

@ -34,6 +34,7 @@ Legend:
fs fs
irq irq
folders folders
cloud
raid raid
smart smart
sensors sensors

View File

@ -15,10 +15,15 @@ Filtered view:
.. image:: ../_static/processlist-filter.png .. image:: ../_static/processlist-filter.png
Extended view:
.. image:: ../_static/processlist-extended.png
The process view consists of 3 parts: The process view consists of 3 parts:
- Processes summary - Processes summary
- Monitored processes list (optional) - Monitored processes list (optional, only in standalone mode)
- Extended stats for the selected process (optional)
- Processes list - Processes list
The processes summary line displays: The processes summary line displays:
@ -56,12 +61,15 @@ You can also set the sort key in the UI:
* - c * - c
- --sort-processes cpu_percent - --sort-processes cpu_percent
- Sort by CPU - Sort by CPU
* - e
- N/A
- Pin the process and display extended stats
* - i * - i
- --sort-processes io_counters - --sort-processes io_counters
- Sort by DISK I/O - Sort by DISK I/O
* - j * - j
- --programs - --programs
- Accumulate processes by program - Accumulate processes by program (extended stats disable in this mode)
* - m * - m
- --sort-processes memory_percent - --sort-processes memory_percent
- Sort by MEM - Sort by MEM

View File

@ -31,6 +31,12 @@ There is no alert on this information.
unitname_fan_speed_alias=Alias for fan speed unitname_fan_speed_alias=Alias for fan speed
.. note 4:: .. note 4::
If a sensors has multiple identical features names (see #2280), then
Glances will add a suffix to the feature name.
For example, if you have one sensor with two Composite features, the
second one will be named Composite_1.
.. note 5::
The plugin could crash on some operating system (FreeBSD) with the The plugin could crash on some operating system (FreeBSD) with the
TCP or UDP blackhole option > 0 (see issue #2106). In this case, you TCP or UDP blackhole option > 0 (see issue #2106). In this case, you
should disable the sensors (--disable-plugin sensors or from the should disable the sensors (--disable-plugin sensors or from the

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,20 @@ Get the Glances container:
.. code-block:: console .. code-block:: console
docker pull nicolargo/glances docker pull nicolargo/glances:<version>
Available tags (all images are based on the Alpine Operating System):
- `latest` for a minimal Glances image (latest release) version with Console, WebUI and Docker dependencies (Recommended)
- `latest-full` for a full Glances image (latest release) with all dependencies
- `dev` for a full Glances image (development branch) with all dependencies (may be instable)
You can also specify a version (example: 3.3.0.4). All available versions can be found on `DockerHub`_.
An Example to pull the `latest` tag:
.. code-block:: console
docker pull nicolargo/glances:latest
Run the container in *console mode*: Run the container in *console mode*:
@ -152,3 +165,6 @@ You can add a ``[passwords]`` block to the Glances configuration file as mention
# Additionally (and optionally) a default password could be defined # Additionally (and optionally) a default password could be defined
localhost=mylocalhostpassword localhost=mylocalhostpassword
default=mydefaultpassword default=mydefaultpassword
.. _DockerHub: https://hub.docker.com/r/nicolargo/glances/tags

View File

@ -31,9 +31,13 @@ Glances InfluxDB data model:
| | time_since_update... | | | | time_since_update... | |
|  | | | |  | | |
+---------------+-----------------------+-----------------------+ +---------------+-----------------------+-----------------------+
| docker | cpu_percent | hostname | | docker | cpu_percent | hostname |
| | memory_usage... | name | | | memory_usage... | name |
+---------------+-----------------------+-----------------------+ +---------------+-----------------------+-----------------------+
| gpu | proc | hostname |
| | mem | gpu_id |
| | temperature... | |
+---------------+-----------------------+-----------------------+
InfluxDB (up to version 1.7.x) InfluxDB (up to version 1.7.x)
------------------------------ ------------------------------

View File

@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "GLANCES" "1" "Jan 30, 2023" "3.4.0_beta1" "Glances" .TH "GLANCES" "1" "Mar 11, 2023" "3.4.0_beta1" "Glances"
.SH NAME .SH NAME
glances \- An eye on your system glances \- An eye on your system
.SH SYNOPSIS .SH SYNOPSIS

View File

@ -61,7 +61,7 @@ if PY3:
def __signal_handler(signal, frame): def __signal_handler(signal, frame):
"""Callback for CTRL-C.""" logger.debug("Signal {} catched".format(signal))
end() end()
@ -74,7 +74,7 @@ def end():
# ...after starting the server mode (issue #1175) # ...after starting the server mode (issue #1175)
pass pass
logger.info("Glances stopped (key pressed: CTRL-C)") logger.info("Glances stopped gracefully")
# The end... # The end...
sys.exit(0) sys.exit(0)
@ -156,8 +156,9 @@ def main():
Select the mode (standalone, client or server) Select the mode (standalone, client or server)
Run it... Run it...
""" """
# Catch the CTRL-C signal # Catch the kill signal
signal.signal(signal.SIGINT, __signal_handler) for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
signal.signal(sig, __signal_handler)
# Log Glances and psutil version # Log Glances and psutil version
logger.info('Start Glances {}'.format(__version__)) logger.info('Start Glances {}'.format(__version__))

View File

@ -99,7 +99,7 @@ class GlancesClient(object):
# Fallback to SNMP # Fallback to SNMP
self.client_mode = 'snmp' self.client_mode = 'snmp'
logger.error("Connection to Glances server failed ({} {})".format(err.errno, err.strerror)) logger.error("Connection to Glances server failed ({} {})".format(err.errno, err.strerror))
fall_back_msg = 'No Glances server found on {}. Trying fallback to SNMP...'.format(self.uri) fall_back_msg = 'No Glances server found. Trying fallback to SNMP...'
if not self.return_to_browser: if not self.return_to_browser:
print(fall_back_msg) print(fall_back_msg)
else: else:

View File

@ -93,7 +93,7 @@ class GlancesClientBrowser(object):
try: try:
s = ServerProxy(uri, transport=t) s = ServerProxy(uri, transport=t)
except Exception as e: except Exception as e:
logger.warning("Client browser couldn't create socket {}: {}".format(uri, e)) logger.warning("Client browser couldn't create socket ({})".format(e))
else: else:
# Mandatory stats # Mandatory stats
try: try:
@ -105,7 +105,7 @@ class GlancesClientBrowser(object):
# OS (Human Readable name) # OS (Human Readable name)
server['hr_name'] = ujson.loads(s.getSystem())['hr_name'] server['hr_name'] = ujson.loads(s.getSystem())['hr_name']
except (socket.error, Fault, KeyError) as e: except (socket.error, Fault, KeyError) as e:
logger.debug("Error while grabbing stats form {}: {}".format(uri, e)) logger.debug("Error while grabbing stats form server ({})".format(e))
server['status'] = 'OFFLINE' server['status'] = 'OFFLINE'
except ProtocolError as e: except ProtocolError as e:
if e.errcode == 401: if e.errcode == 401:
@ -115,7 +115,7 @@ class GlancesClientBrowser(object):
server['status'] = 'PROTECTED' server['status'] = 'PROTECTED'
else: else:
server['status'] = 'OFFLINE' server['status'] = 'OFFLINE'
logger.debug("Cannot grab stats from {} ({} {})".format(uri, e.errcode, e.errmsg)) logger.debug("Cannot grab stats from server ({} {})".format(e.errcode, e.errmsg))
else: else:
# Status # Status
server['status'] = 'ONLINE' server['status'] = 'ONLINE'
@ -126,7 +126,7 @@ class GlancesClientBrowser(object):
load_min5 = ujson.loads(s.getLoad())['min5'] load_min5 = ujson.loads(s.getLoad())['min5']
server['load_min5'] = '{:.2f}'.format(load_min5) server['load_min5'] = '{:.2f}'.format(load_min5)
except Exception as e: except Exception as e:
logger.warning("Error while grabbing stats form {}: {}".format(uri, e)) logger.warning("Error while grabbing stats form server ({})".format(e))
return server return server

View File

@ -236,6 +236,13 @@ Examples of use:
dest='enable_separator', dest='enable_separator',
help='enable separator in the UI', help='enable separator in the UI',
), ),
parser.add_argument(
'--disable-cursor',
action='store_true',
default=False,
dest='disable_cursor',
help='disable cursor (process selection) in the UI',
),
# Sort processes list # Sort processes list
parser.add_argument( parser.add_argument(
'--sort-processes', '--sort-processes',
@ -542,7 +549,6 @@ Examples of use:
logger.setLevel(DEBUG) logger.setLevel(DEBUG)
else: else:
from warnings import simplefilter from warnings import simplefilter
simplefilter("ignore") simplefilter("ignore")
# Plugins refresh rate # Plugins refresh rate
@ -696,9 +702,12 @@ Examples of use:
sys.exit(2) sys.exit(2)
# Filter is only available in standalone mode # Filter is only available in standalone mode
if args.process_filter is not None and not self.is_standalone(): if not args.process_filter and not self.is_standalone():
logger.critical("Process filter is only available in standalone mode") logger.debug("Process filter is only available in standalone mode")
sys.exit(2)
# Cursor option is only available in standalone mode
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 # Disable HDDTemp if sensors are disabled
if getattr(self.args, 'disable_sensors', False): if getattr(self.args, 'disable_sensors', False):
@ -727,8 +736,17 @@ Examples of use:
self.args.is_server = self.is_server() self.args.is_server = self.is_server()
self.args.is_webserver = self.is_webserver() self.args.is_webserver = self.is_webserver()
# Check mode compatibility
self.check_mode_compatibility()
return args return args
def check_mode_compatibility(self):
"""Check mode compatibility"""
if self.args.is_server and self.args.is_webserver:
logger.critical("Server and Web server mode are incompatible")
sys.exit(2)
def is_standalone(self): def is_standalone(self):
"""Return True if Glances is running in standalone mode.""" """Return True if Glances is running in standalone mode."""
return not self.args.client and not self.args.browser and not self.args.server and not self.args.webserver return not self.args.client and not self.args.browser and not self.args.server and not self.args.webserver

View File

@ -230,7 +230,7 @@ class GlancesBottle(object):
""" """
response.status = 200 response.status = 200
return None return "Active"
@compress @compress
def _api_help(self): def _api_help(self):

View File

@ -268,6 +268,7 @@ class _GlancesCurses(object):
self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
self.ifWARNING_color2 = curses.color_pair(5) | A_BOLD self.ifWARNING_color2 = curses.color_pair(5) | A_BOLD
self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
self.ifINFO_color = curses.color_pair(8)
self.filter_color = A_BOLD self.filter_color = A_BOLD
self.selected_color = A_BOLD self.selected_color = A_BOLD
@ -301,6 +302,7 @@ class _GlancesCurses(object):
self.ifCAREFUL_color2 = curses.A_UNDERLINE self.ifCAREFUL_color2 = curses.A_UNDERLINE
self.ifWARNING_color2 = A_BOLD self.ifWARNING_color2 = A_BOLD
self.ifCRITICAL_color2 = curses.A_REVERSE self.ifCRITICAL_color2 = curses.A_REVERSE
self.ifINFO_color = A_BOLD
self.filter_color = A_BOLD self.filter_color = A_BOLD
self.selected_color = A_BOLD self.selected_color = A_BOLD
@ -328,6 +330,7 @@ class _GlancesCurses(object):
'CRITICAL_LOG': self.ifCRITICAL_color, 'CRITICAL_LOG': self.ifCRITICAL_color,
'PASSWORD': curses.A_PROTECT, 'PASSWORD': curses.A_PROTECT,
'SELECTED': self.selected_color, 'SELECTED': self.selected_color,
'INFO': self.ifINFO_color
} }
def set_cursor(self, value): def set_cursor(self, value):
@ -407,13 +410,15 @@ class _GlancesCurses(object):
elif self.pressedkey == ord('9'): elif self.pressedkey == ord('9'):
# '9' > Theme from black to white and reverse # '9' > Theme from black to white and reverse
self._init_colors() self._init_colors()
elif self.pressedkey == ord('e'): elif self.pressedkey == ord('e') and not self.args.programs:
# 'e' > Enable/Disable process extended # 'e' > Enable/Disable process extended
self.args.enable_process_extended = not self.args.enable_process_extended self.args.enable_process_extended = not self.args.enable_process_extended
if not self.args.enable_process_extended: if not self.args.enable_process_extended:
glances_processes.disable_extended() glances_processes.disable_extended()
else: else:
glances_processes.enable_extended() 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'): elif self.pressedkey == ord('E'):
# 'E' > Erase the process filter # 'E' > Erase the process filter
glances_processes.process_filter = None glances_processes.process_filter = None
@ -427,7 +432,7 @@ class _GlancesCurses(object):
elif self.pressedkey == ord('-'): elif self.pressedkey == ord('-'):
# '+' > Decrease process nice level # '+' > Decrease process nice level
self.decrease_nice_process = not self.decrease_nice_process self.decrease_nice_process = not self.decrease_nice_process
elif self.pressedkey == ord('k'): elif self.pressedkey == ord('k') and not self.args.disable_cursor:
# 'k' > Kill selected process (after confirmation) # 'k' > Kill selected process (after confirmation)
self.kill_process = not self.kill_process self.kill_process = not self.kill_process
elif self.pressedkey == ord('w'): elif self.pressedkey == ord('w'):
@ -451,11 +456,11 @@ class _GlancesCurses(object):
# ">" (right arrow) navigation through process sort # ">" (right arrow) navigation through process sort
next_sort = (self.loop_position() + 1) % len(self._sort_loop) next_sort = (self.loop_position() + 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False) glances_processes.set_sort_key(self._sort_loop[next_sort], False)
elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65: elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65 and not self.args.disable_cursor:
# 'UP' > Up in the server list # 'UP' > Up in the server list
if self.args.cursor_position > 0: if self.args.cursor_position > 0:
self.args.cursor_position -= 1 self.args.cursor_position -= 1
elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66: elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66 and not self.args.disable_cursor:
# 'DOWN' > Down in the server list # 'DOWN' > Down in the server list
# if self.args.cursor_position < glances_processes.max_processes - 2: # if self.args.cursor_position < glances_processes.max_processes - 2:
if self.args.cursor_position < glances_processes.processes_count: if self.args.cursor_position < glances_processes.processes_count:

View File

@ -77,7 +77,7 @@ class Sparkline(object):
def get(self): def get(self):
"""Return the sparkline.""" """Return the sparkline."""
ret = sparklines(self.percents)[0] ret = sparklines(self.percents, minimum=0, maximum=100)[0]
if self.__with_text: if self.__with_text:
percents_without_none = [x for x in self.percents if x is not None] percents_without_none = [x for x in self.percents if x is not None]
if len(percents_without_none) > 0: if len(percents_without_none) > 0:

View File

@ -16,12 +16,12 @@ export default {
return this.data.stats['cloud']; return this.data.stats['cloud'];
}, },
provider() { provider() {
return this.stats['ami-id'] !== undefined ? 'AWS EC2' : null; return this.stats['id'] !== undefined ? `${stats['platform']}` : null;
}, },
instance() { instance() {
const { stats } = this; const { stats } = this;
return this.stats['ami-id'] !== undefined return this.stats['id'] !== undefined
? `${stats['instance-type']} instance ${stats['instance-id']} (${stats['reggion']})` ? `${stats['type']} instance ${stats['name']} (${stats['region']})`
: null; : null;
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,8 @@
"""Cloud plugin. """Cloud plugin.
Supported Cloud API: Supported Cloud API:
- OpenStack meta data (class ThreadOpenStack, see below): AWS, OVH... - OpenStack meta data (class ThreadOpenStack) - Vanilla OpenStack
- OpenStackEC2 meta data (class ThreadOpenStackEC2) - Amazon EC2 compatible
""" """
import threading import threading
@ -53,13 +54,16 @@ class Plugin(GlancesPlugin):
# Init thread to grab OpenStack stats asynchronously # Init thread to grab OpenStack stats asynchronously
self.OPENSTACK = ThreadOpenStack() self.OPENSTACK = ThreadOpenStack()
self.OPENSTACKEC2 = ThreadOpenStackEC2()
# Run the thread # Run the thread
self.OPENSTACK.start() self.OPENSTACK.start()
self.OPENSTACKEC2.start()
def exit(self): def exit(self):
"""Overwrite the exit method to close threads.""" """Overwrite the exit method to close threads."""
self.OPENSTACK.stop() self.OPENSTACK.stop()
self.OPENSTACKEC2.stop()
# Call the father class # Call the father class
super(Plugin, self).exit() super(Plugin, self).exit()
@ -80,12 +84,15 @@ class Plugin(GlancesPlugin):
# Update the stats # Update the stats
if self.input_method == 'local': if self.input_method == 'local':
stats = self.OPENSTACK.stats stats = self.OPENSTACK.stats
if not stats:
stats = self.OPENSTACKEC2.stats
# Example: # Example:
# Uncomment to test on physical computer # Uncomment to test on physical computer (only for test purpose)
# stats = {'ami-id': 'ami-id', # stats = {'id': 'ami-id',
# 'instance-id': 'instance-id', # 'name': 'My VM',
# 'instance-type': 'instance-type', # 'type': 'Gold',
# 'region': 'placement/availability-zone'} # 'region': 'France',
# 'platform': 'OpenStack'}
# Update the stats # Update the stats
self.stats = stats self.stats = stats
@ -101,13 +108,14 @@ class Plugin(GlancesPlugin):
return ret return ret
# Generate the output # Generate the output
if 'instance-type' in self.stats and 'instance-id' in self.stats and 'region' in self.stats: msg = self.stats.get('platform', 'Unknown')
msg = 'Cloud ' ret.append(self.curse_add_line(msg, "TITLE"))
ret.append(self.curse_add_line(msg, "TITLE")) msg = ' {} instance {} ({})'.format(
msg = '{} instance {} ({})'.format( self.stats.get('type', 'Unknown'),
self.stats['instance-type'], self.stats['instance-id'], self.stats['region'] self.stats.get('name', 'Unknown'),
) self.stats.get('region', 'Unknown')
ret.append(self.curse_add_line(msg)) )
ret.append(self.curse_add_line(msg))
# Return the message with decoration # Return the message with decoration
# logger.info(ret) # logger.info(ret)
@ -121,13 +129,19 @@ class ThreadOpenStack(threading.Thread):
stats is a dict 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 # https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data' OPENSTACK_PLATFORM = "OpenStack"
OPENSTACK_API_URL = 'http://169.254.169.254/openstack/latest/meta-data'
OPENSTACK_API_METADATA = { OPENSTACK_API_METADATA = {
'ami-id': 'ami-id', 'id': 'project_id',
'instance-id': 'instance-id', 'name': 'name',
'instance-type': 'instance-type', 'type': 'meta/role',
'region': 'placement/availability-zone', 'region': 'availability_zone',
} }
def __init__(self): def __init__(self):
@ -159,6 +173,9 @@ class ThreadOpenStack(threading.Thread):
else: else:
if r.ok: if r.ok:
self._stats[k] = to_ascii(r.content) 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 return True
@ -180,3 +197,26 @@ class ThreadOpenStack(threading.Thread):
def stopped(self): def stopped(self):
"""Return True is the thread is stopped.""" """Return True is the thread is stopped."""
return self._stopper.is_set() return self._stopper.is_set()
class ThreadOpenStackEC2(ThreadOpenStack):
"""
Specific thread to grab OpenStack EC2 (Amazon cloud) stats.
stats is a dict
"""
# The metadata service provides a way for instances to retrieve
# instance-specific data via a REST API. Instances access this
# service at 169.254.169.254 or at fe80::a9fe:a9fe.
# All types of metadata, be it user-, nova- or vendor-provided,
# can be accessed via this service.
# https://docs.openstack.org/nova/latest/user/metadata-service.html
OPENSTACK_PLATFORM = "Amazon EC2"
OPENSTACK_API_URL = 'http://169.254.169.254/latest/meta-data'
OPENSTACK_API_METADATA = {
'id': 'ami-id',
'name': 'instance-id',
'type': 'instance-type',
'region': 'placement/availability-zone',
}

View File

@ -13,6 +13,7 @@ from __future__ import unicode_literals
import operator import operator
from glances.compat import u, nativestr, PermissionError from glances.compat import u, nativestr, PermissionError
from glances.logger import logger
from glances.plugins.glances_plugin import GlancesPlugin from glances.plugins.glances_plugin import GlancesPlugin
import psutil import psutil
@ -95,14 +96,26 @@ class Plugin(GlancesPlugin):
try: try:
fs_stat = psutil.disk_partitions(all=False) fs_stat = psutil.disk_partitions(all=False)
except (UnicodeDecodeError, PermissionError): except (UnicodeDecodeError, PermissionError):
logger.debug("Plugin - fs: PsUtil fetch failed")
return self.stats return self.stats
# Optional hack to allow logical mounts points (issue #448) # Optional hack to allow logical mounts points (issue #448)
for fs_type in self.get_conf_value('allow'): allowed_fs_types = self.get_conf_value('allow')
if allowed_fs_types:
# Avoid Psutil call unless mounts need to be allowed
try: try:
fs_stat += [f for f in psutil.disk_partitions(all=True) if f.fstype.find(fs_type) >= 0] all_mounted_fs = psutil.disk_partitions(all=True)
except UnicodeDecodeError: except (UnicodeDecodeError, PermissionError):
return self.stats logger.debug("Plugin - fs: PsUtil extended fetch failed")
else:
# Discard duplicates (#2299) and add entries matching allowed fs types
tracked_mnt_points = set(f.mountpoint for f in fs_stat)
for f in all_mounted_fs:
if (
any(f.fstype.find(fs_type) >= 0 for fs_type in allowed_fs_types)
and f.mountpoint not in tracked_mnt_points
):
fs_stat.append(f)
# Loop over fs # Loop over fs
for fs in fs_stat: for fs in fs_stat:

View File

@ -84,7 +84,8 @@ class Plugin(GlancesPlugin):
# "name": "Fake GeForce GTX", # "name": "Fake GeForce GTX",
# "mem": 5.792331695556641, # "mem": 5.792331695556641,
# "proc": 4, # "proc": 4,
# "temperature": 26 # "temperature": 26,
# "fan_speed": 30
# } # }
# ] # ]
# Two GPU sample: # Two GPU sample:
@ -95,7 +96,8 @@ class Plugin(GlancesPlugin):
# "name": "Fake GeForce GTX1", # "name": "Fake GeForce GTX1",
# "mem": 5.792331695556641, # "mem": 5.792331695556641,
# "proc": 4, # "proc": 4,
# "temperature": 26 # "temperature": 26,
# "fan_speed": 30
# }, # },
# { # {
# "key": "gpu_id", # "key": "gpu_id",
@ -103,7 +105,8 @@ class Plugin(GlancesPlugin):
# "name": "Fake GeForce GTX2", # "name": "Fake GeForce GTX2",
# "mem": 15, # "mem": 15,
# "proc": 8, # "proc": 8,
# "temperature": 65 # "temperature": 65,
# "fan_speed": 75
# } # }
# ] # ]
return self.stats return self.stats
@ -274,6 +277,8 @@ class Plugin(GlancesPlugin):
device_stats['proc'] = get_proc(device_handle) device_stats['proc'] = get_proc(device_handle)
# Processor temperature in °C # Processor temperature in °C
device_stats['temperature'] = get_temperature(device_handle) device_stats['temperature'] = get_temperature(device_handle)
# Fan speed in %
device_stats['fan_speed'] = get_fan_speed(device_handle)
stats.append(device_stats) stats.append(device_stats)
return stats return stats
@ -324,8 +329,16 @@ def get_proc(device_handle):
def get_temperature(device_handle): def get_temperature(device_handle):
"""Get GPU device CPU consumption in percent.""" """Get GPU device CPU temperature in Celsius."""
try: try:
return pynvml.nvmlDeviceGetTemperature(device_handle, pynvml.NVML_TEMPERATURE_GPU) return pynvml.nvmlDeviceGetTemperature(device_handle, pynvml.NVML_TEMPERATURE_GPU)
except pynvml.NVMLError: except pynvml.NVMLError:
return None return None
def get_fan_speed(device_handle):
"""Get GPU device fan speed in percent."""
try:
return pynvml.nvmlDeviceGetFanSpeed(device_handle)
except pynvml.NVMLError:
return None

View File

@ -19,7 +19,7 @@ from glances.processes import glances_processes, sort_stats
from glances.outputs.glances_unicode import unicode_message from glances.outputs.glances_unicode import unicode_message
from glances.plugins.glances_core import Plugin as CorePlugin from glances.plugins.glances_core import Plugin as CorePlugin
from glances.plugins.glances_plugin import GlancesPlugin from glances.plugins.glances_plugin import GlancesPlugin
from glances.programs import processes_to_programs from glances.outputs.glances_bars import Bar
def seconds_to_hms(input_seconds): def seconds_to_hms(input_seconds):
@ -146,9 +146,10 @@ class Plugin(GlancesPlugin):
# Update stats using the standard system lib # Update stats using the standard system lib
# Note: Update is done in the processcount plugin # Note: Update is done in the processcount plugin
# Just return the processes list # Just return the processes list
stats = glances_processes.getlist()
if self.args.programs: if self.args.programs:
stats = processes_to_programs(stats) stats = glances_processes.getlist(as_programs=True)
else:
stats = glances_processes.getlist()
elif self.input_method == 'snmp': elif self.input_method == 'snmp':
# No SNMP grab for processes # No SNMP grab for processes
@ -353,11 +354,11 @@ class Plugin(GlancesPlugin):
- selected is a tag=True if p is the selected process - selected is a tag=True if p is the selected process
""" """
ret = [self.curse_new_line()] ret = [self.curse_new_line()]
# When a process is selected: # When a process is selected:
# * display a special character at the beginning of the line # * display a special character at the beginning of the line
# * underline the command name # * underline the command name
if args.is_standalone: ret.append(self.curse_add_line(unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'))
ret.append(self.curse_add_line(unicode_message('PROCESS_SELECTOR') if selected else ' ', 'SELECTED'))
# CPU # CPU
ret.append(self._get_process_curses_cpu(p, selected, args)) ret.append(self._get_process_curses_cpu(p, selected, args))
@ -404,7 +405,7 @@ class Plugin(GlancesPlugin):
cmdline = p.get('cmdline', '?') cmdline = p.get('cmdline', '?')
try: try:
process_decoration = 'PROCESS_SELECTED' if (selected and args.is_standalone) else 'PROCESS' process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS'
if cmdline: if cmdline:
path, cmd, arguments = split_cmdline(bare_process_name, cmdline) path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
# Manage end of line in arguments (see #1692) # Manage end of line in arguments (see #1692)
@ -428,74 +429,14 @@ class Plugin(GlancesPlugin):
logger.debug("Can not decode command line '{}' ({})".format(cmdline, e)) logger.debug("Can not decode command line '{}' ({})".format(cmdline, e))
ret.append(self.curse_add_line('', splittable=True)) ret.append(self.curse_add_line('', splittable=True))
# Add extended stats but only for the top processes
if args.cursor_position == 0 and 'extended_stats' in p and args.enable_process_extended:
# Left padding
xpad = ' ' * 13
# First line is CPU affinity
if 'cpu_affinity' in p and p['cpu_affinity'] is not None:
ret.append(self.curse_new_line())
msg = xpad + 'CPU affinity: ' + str(len(p['cpu_affinity'])) + ' cores'
ret.append(self.curse_add_line(msg, splittable=True))
# Second line is memory info
if 'memory_info' in p and p['memory_info'] is not None:
ret.append(self.curse_new_line())
msg = '{}Memory info: {}'.format(xpad, p['memory_info'])
if 'memory_swap' in p and p['memory_swap'] is not None:
msg += ' swap ' + self.auto_unit(p['memory_swap'], low_precision=False)
ret.append(self.curse_add_line(msg, splittable=True))
# Third line is for open files/network sessions
msg = ''
if 'num_threads' in p and p['num_threads'] is not None:
msg += str(p['num_threads']) + ' threads '
if 'num_fds' in p and p['num_fds'] is not None:
msg += str(p['num_fds']) + ' files '
if 'num_handles' in p and p['num_handles'] is not None:
msg += str(p['num_handles']) + ' handles '
if 'tcp' in p and p['tcp'] is not None:
msg += str(p['tcp']) + ' TCP '
if 'udp' in p and p['udp'] is not None:
msg += str(p['udp']) + ' UDP'
if msg != '':
ret.append(self.curse_new_line())
msg = xpad + 'Open: ' + msg
ret.append(self.curse_add_line(msg, splittable=True))
# Fourth line is IO nice level (only Linux and Windows OS)
if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'):
ret.append(self.curse_new_line())
msg = xpad + 'IO nice: '
k = 'Class is '
v = p['ionice'].ioclass
# Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
# Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
if WINDOWS:
if v == 0:
msg += k + 'Very Low'
elif v == 1:
msg += k + 'Low'
elif v == 2:
msg += 'No specific I/O priority'
else:
msg += k + str(v)
else:
if v == 0:
msg += 'No specific I/O priority'
elif v == 1:
msg += k + 'Real Time'
elif v == 2:
msg += k + 'Best Effort'
elif v == 3:
msg += k + 'IDLE'
else:
msg += k + str(v)
# value is a number which goes from 0 to 7.
# The higher the value, the lower the I/O priority of the process.
if hasattr(p['ionice'], 'value') and p['ionice'].value != 0:
msg += ' (value %s/7)' % str(p['ionice'].value)
ret.append(self.curse_add_line(msg, splittable=True))
return ret return ret
def is_selected_process(self, args):
return args.is_standalone and \
self.args.enable_process_extended and \
args.cursor_position is not None and \
glances_processes.extended_process is not None
def msg_curse(self, args=None, max_width=None): def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface.""" """Return the dict to display in the curse interface."""
# Init the return message # Init the return message
@ -507,6 +448,16 @@ class Plugin(GlancesPlugin):
# Compute the sort key # Compute the sort key
process_sort_key = glances_processes.sort_key process_sort_key = glances_processes.sort_key
processes_list_sorted = self.__sort_stats(process_sort_key)
# Display extended stats for selected process
#############################################
if self.is_selected_process(args):
self.__msg_curse_extended_process(ret, glances_processes.extended_process)
# Display others processes list
###############################
# Header # Header
self.__msg_curse_header(ret, process_sort_key, args) self.__msg_curse_header(ret, process_sort_key, args)
@ -515,10 +466,10 @@ class Plugin(GlancesPlugin):
# Loop over processes (sorted by the sort key previously compute) # Loop over processes (sorted by the sort key previously compute)
# This is a Glances bottleneck (see flame graph), # This is a Glances bottleneck (see flame graph),
# get_process_curses_data should be optimzed # get_process_curses_data should be optimzed
i = 0 for position, process in enumerate(processes_list_sorted):
for p in self.__sort_stats(process_sort_key): ret.extend(self.get_process_curses_data(process,
ret.extend(self.get_process_curses_data(p, i == args.cursor_position, args)) position == args.cursor_position,
i += 1 args))
# A filter is set Display the stats summaries # A filter is set Display the stats summaries
if glances_processes.process_filter is not None: if glances_processes.process_filter is not None:
@ -532,6 +483,116 @@ class Plugin(GlancesPlugin):
# Return the message with decoration # Return the message with decoration
return ret return ret
def __msg_curse_extended_process(self, ret, p):
"""Get extended curses data for the selected process (see issue #2225)
The result depends of the process type (process or thread).
Input p is a dict with the following keys:
{'status': 'S',
'memory_info': pmem(rss=466890752, vms=3365347328, shared=68153344, text=659456, lib=0, data=774647808, dirty=0),
'pid': 4980,
'io_counters': [165385216, 0, 165385216, 0, 1],
'num_threads': 20,
'nice': 0,
'memory_percent': 5.958135664449709,
'cpu_percent': 0.0,
'gids': pgids(real=1000, effective=1000, saved=1000),
'cpu_times': pcputimes(user=696.38, system=119.98, children_user=0.0, children_system=0.0, iowait=0.0),
'name': 'WebExtensions',
'key': 'pid',
'time_since_update': 2.1997854709625244,
'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'],
'username': 'nicolargo',
'cpu_min': 0.0,
'cpu_max': 7.0,
'cpu_mean': 3.2}
"""
if self.args.programs:
self.__msg_curse_extended_process_program(ret, p)
else:
self.__msg_curse_extended_process_thread(ret, p)
def __msg_curse_extended_process_program(self, ret, p):
# Title
msg = "Pinned program {} ('e' to unpin)".format(p['name'])
ret.append(self.curse_add_line(msg, "TITLE"))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_extended_process_thread(self, ret, p):
# Title
ret.append(self.curse_add_line("Pinned thread ", "TITLE"))
ret.append(self.curse_add_line(p['name'], "UNDERLINE"))
ret.append(self.curse_add_line(" ('e' to unpin)"))
# First line is CPU affinity
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' CPU Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['cpu_min'], p['cpu_max'], p['cpu_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'cpu_affinity' in p and p['cpu_affinity'] is not None:
ret.append(self.curse_add_line(' Affinity: '))
ret.append(self.curse_add_line(str(len(p['cpu_affinity'])), decoration='INFO'))
ret.append(self.curse_add_line(' cores', decoration='INFO'))
if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'):
msg = ' IO nice: '
k = 'Class is '
v = p['ionice'].ioclass
# Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
# Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
if WINDOWS:
if v == 0:
msg += k + 'Very Low'
elif v == 1:
msg += k + 'Low'
elif v == 2:
msg += 'No specific I/O priority'
else:
msg += k + str(v)
else:
if v == 0:
msg += 'No specific I/O priority'
elif v == 1:
msg += k + 'Real Time'
elif v == 2:
msg += k + 'Best Effort'
elif v == 3:
msg += k + 'IDLE'
else:
msg += k + str(v)
# value is a number which goes from 0 to 7.
# The higher the value, the lower the I/O priority of the process.
if hasattr(p['ionice'], 'value') and p['ionice'].value != 0:
msg += ' (value %s/7)' % str(p['ionice'].value)
ret.append(self.curse_add_line(msg, splittable=True))
# Second line is memory info
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' MEM Min/Max/Mean: '))
msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['memory_min'], p['memory_max'], p['memory_mean'])
ret.append(self.curse_add_line(msg, decoration='INFO'))
if 'memory_info' in p and p['memory_info'] is not None:
ret.append(self.curse_add_line(' Memory info: '))
for k in p['memory_info']._asdict():
ret.append(self.curse_add_line(self.auto_unit(p['memory_info']._asdict()[k], low_precision=False), decoration='INFO', splittable=True))
ret.append(self.curse_add_line(' ' + k + ' ', splittable=True))
if 'memory_swap' in p and p['memory_swap'] is not None:
ret.append(self.curse_add_line(self.auto_unit(p['memory_swap'], low_precision=False), decoration='INFO', splittable=True))
ret.append(self.curse_add_line(' swap ', splittable=True))
# Third line is for open files/network sessions
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(' Open: '))
for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']:
if stat_prefix in p and p[stat_prefix] is not None:
ret.append(self.curse_add_line(str(p[stat_prefix]), decoration='INFO'))
ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', ''))))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())
def __msg_curse_header(self, ret, process_sort_key, args=None): def __msg_curse_header(self, ret, process_sort_key, args=None):
"""Build the header and add it to the ret dict.""" """Build the header and add it to the ret dict."""
sort_style = 'SORT' sort_style = 'SORT'
@ -578,10 +639,15 @@ class Plugin(GlancesPlugin):
msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
) )
) )
if not self.args.programs: if args.is_standalone and not args.disable_cursor:
msg = self.layout_header['command'].format('Command', "('k' to kill)" if args.is_standalone else "") if self.args.programs:
shortkey = "('k' to kill)"
else:
shortkey = "('e' to pin | 'k' to kill)"
else: else:
msg = self.layout_header['command'].format('Programs', "('k' to kill)" if args.is_standalone else "") shortkey = ""
msg = self.layout_header['command'].format("Programs" if self.args.programs else "Command",
shortkey)
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT')) ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None): def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None):

View File

@ -92,6 +92,8 @@ class Plugin(GlancesPlugin):
# New line # New line
ret.append(self.curse_new_line()) ret.append(self.curse_new_line())
# Display the current status # Display the current status
if not isinstance(self.stats[array], dict):
continue
status = self.raid_alert( status = self.raid_alert(
self.stats[array]['status'], self.stats[array]['status'],
self.stats[array]['used'], self.stats[array]['used'],

View File

@ -175,7 +175,7 @@ class Plugin(GlancesPlugin):
# Alert processing # Alert processing
if i['type'] == SENSOR_TEMP_TYPE: if i['type'] == SENSOR_TEMP_TYPE:
if self.is_limit('critical', stat_name='sensors_temperature_' + i['label']): if self.is_limit('critical', stat_name='sensors_temperature_' + i['label']):
# By default use the thresholds confiured in the glances.conf file (see #2058) # By default use the thresholds configured in the glances.conf file (see #2058)
alert = self.get_alert(current=i['value'], header='temperature_' + i['label']) alert = self.get_alert(current=i['value'], header='temperature_' + i['label'])
else: else:
# Else use the system thresholds # Else use the system thresholds
@ -197,7 +197,7 @@ class Plugin(GlancesPlugin):
self.views[i[self.get_key()]]['value']['decoration'] = alert self.views[i[self.get_key()]]['value']['decoration'] = alert
def battery_trend(self, stats): def battery_trend(self, stats):
"""Return the trend characterr for the battery""" """Return the trend character for the battery"""
if 'status' not in stats: if 'status' not in stats:
return '' return ''
if stats['status'].startswith('Charg'): if stats['status'].startswith('Charg'):
@ -331,12 +331,15 @@ class GlancesGrabSensors(object):
else: else:
return ret return ret
for chip_name, chip in iteritems(input_list): for chip_name, chip in iteritems(input_list):
i = 1 label_index = 1
for feature in chip: for chip_name_index, feature in enumerate(chip):
sensors_current = {} sensors_current = {}
# Sensor name # Sensor name
if feature.label == '': if feature.label == '':
sensors_current['label'] = chip_name + ' ' + str(i) sensors_current['label'] = chip_name + ' ' + str(chip_name_index)
elif feature.label in [i['label'] for i in ret]:
sensors_current['label'] = feature.label + ' ' + str(label_index)
label_index += 1
else: else:
sensors_current['label'] = feature.label sensors_current['label'] = feature.label
# Sensors value, limit and unit # Sensors value, limit and unit
@ -348,7 +351,6 @@ class GlancesGrabSensors(object):
sensors_current['critical'] = int(system_critical) if system_critical is not None else None sensors_current['critical'] = int(system_critical) if system_critical is not None else None
# Add sensor to the list # Add sensor to the list
ret.append(sensors_current) ret.append(sensors_current)
i += 1
return ret return ret
def get(self, sensor_type=SENSOR_TEMP_TYPE): def get(self, sensor_type=SENSOR_TEMP_TYPE):

View File

@ -13,6 +13,7 @@ from glances.compat import iterkeys
from glances.globals import BSD, LINUX, MACOS, WINDOWS from glances.globals import BSD, LINUX, MACOS, WINDOWS
from glances.timer import Timer, getTimeSinceLastUpdate from glances.timer import Timer, getTimeSinceLastUpdate
from glances.filter import GlancesFilter from glances.filter import GlancesFilter
from glances.programs import processes_to_programs
from glances.logger import logger from glances.logger import logger
import psutil import psutil
@ -37,6 +38,10 @@ class GlancesProcesses(object):
def __init__(self, cache_timeout=60): def __init__(self, cache_timeout=60):
"""Init the class to collect stats about processes.""" """Init the class to collect stats about processes."""
# Init the args, coming from the GlancesStandalone class
# Should be set by the set_args method
self.args = None
# Add internals caches because psutil do not cache all the stats # Add internals caches because psutil do not cache all the stats
# See: https://github.com/giampaolo/psutil/issues/462 # See: https://github.com/giampaolo/psutil/issues/462
self.username_cache = {} self.username_cache = {}
@ -70,6 +75,7 @@ class GlancesProcesses(object):
# Extended stats for top process is enable by default # Extended stats for top process is enable by default
self.disable_extended_tag = False self.disable_extended_tag = False
self.extended_process = None
# Test if the system can grab io_counters # Test if the system can grab io_counters
try: try:
@ -109,6 +115,10 @@ class GlancesProcesses(object):
self._max_values = {} self._max_values = {}
self.reset_max_values() self.reset_max_values()
def set_args(self, args):
"""Set args."""
self.args = args
def reset_processcount(self): def reset_processcount(self):
"""Reset the global process count""" """Reset the global process count"""
self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None} self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None}
@ -247,6 +257,91 @@ class GlancesProcesses(object):
for k in self._max_values_list: for k in self._max_values_list:
self._max_values[k] = 0.0 self._max_values[k] = 0.0
def get_extended_stats(self, proc):
"""Get the extended stats for the given PID."""
# - cpu_affinity (Linux, Windows, FreeBSD)
# - ionice (Linux and Windows > Vista)
# - num_ctx_switches (not available on Illumos/Solaris)
# - num_fds (Unix-like)
# - num_handles (Windows)
# - memory_maps (only swap, Linux)
# https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
# - connections (TCP and UDP)
# - CPU min/max/mean
# Set the extended stats list (OS dependant)
extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches']
if LINUX:
# num_fds only available on Unix system (see issue #1351)
extended_stats += ['num_fds']
if WINDOWS:
extended_stats += ['num_handles']
ret = {}
try:
# Get the extended stats
selected_process = psutil.Process(proc['pid'])
ret = selected_process.as_dict(attrs=extended_stats, ad_value=None)
if LINUX:
try:
ret['memory_swap'] = sum([v.swap for v in selected_process.memory_maps()])
except (psutil.NoSuchProcess, KeyError):
# (KeyError catch for issue #1551)
pass
except (psutil.AccessDenied, NotImplementedError):
# NotImplementedError: /proc/${PID}/smaps file doesn't exist
# on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
# is not enabled (see psutil #533/glances #413).
ret['memory_swap'] = None
try:
ret['tcp'] = len(selected_process.connections(kind="tcp"))
ret['udp'] = len(selected_process.connections(kind="udp"))
except (psutil.AccessDenied, psutil.NoSuchProcess):
# Manage issue1283 (psutil.AccessDenied)
ret['tcp'] = None
ret['udp'] = None
except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
logger.error('Can not grab extended stats ({})'.format(e))
self.extended_process = None
ret['extended_stats'] = False
else:
logger.debug('Grab extended stats for process {}'.format(proc['pid']))
# Compute CPU and MEM min/max/mean
for stat_prefix in ['cpu', 'memory']:
if stat_prefix + '_min' not in self.extended_process:
ret[stat_prefix + '_min'] = proc[stat_prefix + '_percent']
else:
ret[stat_prefix + '_min'] = proc[stat_prefix + '_percent'] if proc[stat_prefix + '_min'] > proc[stat_prefix + '_percent'] else proc[stat_prefix + '_min']
if stat_prefix + '_max' not in self.extended_process:
ret[stat_prefix + '_max'] = proc[stat_prefix + '_percent']
else:
ret[stat_prefix + '_max'] = proc[stat_prefix + '_percent'] if proc[stat_prefix + '_max'] < proc[stat_prefix + '_percent'] else proc[stat_prefix + '_max']
if stat_prefix + '_mean_sum' not in self.extended_process:
ret[stat_prefix + '_mean_sum'] = proc[stat_prefix + '_percent']
else:
ret[stat_prefix + '_mean_sum'] = proc[stat_prefix + '_mean_sum'] + proc[stat_prefix + '_percent']
if stat_prefix + '_mean_counter' not in self.extended_process:
ret[stat_prefix + '_mean_counter'] = 1
else:
ret[stat_prefix + '_mean_counter'] = proc[stat_prefix + '_mean_counter'] + 1
ret[stat_prefix + '_mean'] = ret[stat_prefix + '_mean_sum'] / ret[stat_prefix + '_mean_counter']
ret['extended_stats'] = True
return ret
def is_selected_extended_process(self, position):
"""Return True if the process is the selected one for extended stats."""
return hasattr(self.args, 'programs') and \
not self.args.programs and \
hasattr(self.args, 'enable_process_extended') and \
self.args.enable_process_extended and \
not self.disable_extended_tag and \
hasattr(self.args, 'cursor_position') and \
position == self.args.cursor_position and \
not self.args.disable_cursor
def update(self): def update(self):
"""Update the processes stats.""" """Update the processes stats."""
# Reset the stats # Reset the stats
@ -305,59 +400,25 @@ class GlancesProcesses(object):
# Update the processcount # Update the processcount
self.update_processcount(self.processlist) self.update_processcount(self.processlist)
# Loop over processes and add metadata # Loop over processes and :
first = True # - add extended stats for selected process
for proc in self.processlist: # - add metadata
# Get extended stats, only for top processes (see issue #403). for position, proc in enumerate(self.processlist):
if first and not self.disable_extended_tag: # Extended stats
# - cpu_affinity (Linux, Windows, FreeBSD) ################
# - ionice (Linux and Windows > Vista)
# - num_ctx_switches (not available on Illumos/Solaris)
# - num_fds (Unix-like)
# - num_handles (Windows)
# - memory_maps (only swap, Linux)
# https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
# - connections (TCP and UDP)
extended = {}
try:
top_process = psutil.Process(proc['pid'])
extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches']
if LINUX:
# num_fds only available on Unix system (see issue #1351)
extended_stats += ['num_fds']
if WINDOWS:
extended_stats += ['num_handles']
# Get the extended stats # Get the selected process when the 'e' key is pressed
extended = top_process.as_dict(attrs=extended_stats, ad_value=None) if self.is_selected_extended_process(position):
self.extended_process = proc
if LINUX: # Grab extended stats only for the selected process (see issue #2225)
try: if self.extended_process is not None and \
extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()]) proc['pid'] == self.extended_process['pid']:
except (psutil.NoSuchProcess, KeyError): proc.update(self.get_extended_stats(self.extended_process))
# (KeyError catch for issue #1551) self.extended_process = proc
pass
except (psutil.AccessDenied, NotImplementedError): # Meta data
# NotImplementedError: /proc/${PID}/smaps file doesn't exist ###########
# on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
# is not enabled (see psutil #533/glances #413).
extended['memory_swap'] = None
try:
extended['tcp'] = len(top_process.connections(kind="tcp"))
extended['udp'] = len(top_process.connections(kind="udp"))
except (psutil.AccessDenied, psutil.NoSuchProcess):
# Manage issue1283 (psutil.AccessDenied)
extended['tcp'] = None
extended['udp'] = None
except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
logger.error('Can not grab extended stats ({})'.format(e))
extended['extended_stats'] = False
else:
logger.debug('Grab extended stats for process {}'.format(proc['pid']))
extended['extended_stats'] = True
proc.update(extended)
first = False
# /End of extended stats
# PID is the key # PID is the key
proc['key'] = 'pid' proc['key'] = 'pid'
@ -424,9 +485,14 @@ class GlancesProcesses(object):
"""Get the number of processes.""" """Get the number of processes."""
return self.processcount return self.processcount
def getlist(self, sorted_by=None): def getlist(self, sorted_by=None, as_programs=False):
"""Get the processlist.""" """Get the processlist.
return self.processlist By default, return the list of threads.
If as_programs is True, return the list of programs."""
if as_programs:
return processes_to_programs(self.processlist)
else:
return self.processlist
@property @property
def sort_key(self): def sort_key(self):

View File

@ -50,6 +50,9 @@ class GlancesStandalone(object):
self.display_modules_list() self.display_modules_list()
sys.exit(0) sys.exit(0)
# The args is needed to get the selected process in the process list (Curses mode)
glances_processes.set_args(args)
# If process extended stats is disabled by user # If process extended stats is disabled by user
if not args.enable_process_extended: if not args.enable_process_extended:
logger.debug("Extended stats for top process are disabled") logger.debug("Extended stats for top process are disabled")
@ -193,5 +196,7 @@ class GlancesStandalone(object):
) )
print("You should consider upgrading using: pip install --upgrade glances") print("You should consider upgrading using: pip install --upgrade glances")
print("Disable this warning temporarily using: glances --disable-check-update") print("Disable this warning temporarily using: glances --disable-check-update")
print("To disable it permanently, refer config reference at " print(
"https://glances.readthedocs.io/en/latest/config.html#syntax") "To disable it permanently, refer config reference at "
"https://glances.readthedocs.io/en/latest/config.html#syntax"
)

View File

@ -34,5 +34,5 @@ six
sparklines sparklines
statsd statsd
wifi wifi
zeroconf==0.47.3; python_version < "3.7" zeroconf==0.54.0; python_version < "3.7"
zeroconf>=0.19.1; python_version >= "3.7" zeroconf; python_version >= "3.7"

View File

@ -61,7 +61,7 @@ def get_install_requires():
def get_install_extras_require(): def get_install_extras_require():
extras_require = { extras_require = {
'action': ['chevron'], 'action': ['chevron'],
'browser': ['zeroconf==0.47.3' if PY2 else 'zeroconf>=0.19.1'], 'browser': ['zeroconf==0.54.0' if PY2 else 'zeroconf>=0.19.1'],
'cloud': ['requests'], 'cloud': ['requests'],
'docker': ['docker>=2.0.0', 'python-dateutil', 'six'], 'docker': ['docker>=2.0.0', 'python-dateutil', 'six'],
'export': ['bernhard', 'cassandra-driver', 'couchdb', 'elasticsearch', 'export': ['bernhard', 'cassandra-driver', 'couchdb', 'elasticsearch',

View File

@ -221,6 +221,16 @@ class TestGlances(unittest.TestCase):
self.assertIsInstance(req.json(), dict) self.assertIsInstance(req.json(), dict)
self.assertIsInstance(req.json()['interface_name'], list) self.assertIsInstance(req.json()['interface_name'], list)
def test_012_status(self):
"""Check status endpoint."""
method = "status"
print('INFO: [TEST_012] Status')
print("HTTP RESTful request: %s/%s" % (URL, method))
req = self.http_get("%s/%s" % (URL, method))
self.assertTrue(req.ok)
self.assertEqual(req.text, "Active")
def test_999_stop_server(self): def test_999_stop_server(self):
"""Stop the Glances Web Server.""" """Stop the Glances Web Server."""
print('INFO: [TEST_999] Stop the Glances Web Server') print('INFO: [TEST_999] Stop the Glances Web Server')