mirror of
https://github.com/nicolargo/glances.git
synced 2024-12-23 01:01:31 +03:00
Merge branch 'develop' into issue2225
This commit is contained in:
commit
25c0a1c9a7
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -104,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Retrieve Repository Docker metadata
|
- name: Retrieve Repository Docker metadata
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: crazy-max/ghaction-docker-meta@v4.1.1
|
uses: crazy-max/ghaction-docker-meta@v4.3.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.DEFAULT_DOCKER_IMAGE }}
|
images: ${{ env.DEFAULT_DOCKER_IMAGE }}
|
||||||
labels: |
|
labels: |
|
||||||
@ -136,7 +136,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
push: ${{ env.PUSH_BRANCH == 'true' }}
|
push: ${{ env.PUSH_BRANCH == 'true' }}
|
||||||
tags: "${{ env.DEFAULT_DOCKER_IMAGE }}:${{ matrix.os != 'alpine' && format('{0}-', matrix.os) || '' }}${{ matrix.tag.tag }}"
|
tags: "${{ env.DEFAULT_DOCKER_IMAGE }}:${{ matrix.os != 'alpine' && format('{0}-', matrix.os) || '' }}${{ matrix.tag.tag }}"
|
||||||
|
14
Makefile
14
Makefile
@ -137,6 +137,11 @@ docker-alpine: ## Generate local docker images (Alpine)
|
|||||||
docker build --target minimal -f ./docker-files/alpine.Dockerfile -t glances:local-alpine-minimal .
|
docker build --target minimal -f ./docker-files/alpine.Dockerfile -t glances:local-alpine-minimal .
|
||||||
docker build --target dev -f ./docker-files/alpine.Dockerfile -t glances:local-alpine-dev .
|
docker build --target dev -f ./docker-files/alpine.Dockerfile -t glances:local-alpine-dev .
|
||||||
|
|
||||||
|
docker-ubuntu: ## Generate local docker images (Ubuntu)
|
||||||
|
docker build --target full -f ./docker-files/ubuntu.Dockerfile -t glances:local-ubuntu-full .
|
||||||
|
docker build --target minimal -f ./docker-files/ubuntu.Dockerfile -t glances:local-ubuntu-minimal .
|
||||||
|
docker build --target dev -f ./docker-files/ubuntu.Dockerfile -t glances:local-ubuntu-dev .
|
||||||
|
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
# Run
|
# Run
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@ -159,6 +164,15 @@ run-docker-alpine-full: ## Start Glances Alpine Docker full in console mode
|
|||||||
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 -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
|
||||||
|
docker run --rm -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
|
||||||
|
docker run --rm -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
|
||||||
|
docker run --rm -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
|
||||||
|
|
||||||
|
73
NEWS.rst
73
NEWS.rst
@ -2,11 +2,82 @@
|
|||||||
Glances changelog
|
Glances changelog
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
|
===============
|
||||||
|
Version 3.4.0
|
||||||
|
===============
|
||||||
|
|
||||||
|
See roadmap here: https://github.com/nicolargo/glances/issues?q=is%3Aopen+is%3Aissue+milestone%3A%22Glances+3.4.0%22
|
||||||
|
|
||||||
|
===============
|
||||||
|
Version 3.3.1.1
|
||||||
|
===============
|
||||||
|
|
||||||
|
Hard patch on the master branch.
|
||||||
|
|
||||||
|
Bug corrected:
|
||||||
|
|
||||||
|
* "ModuleNotFoundError: No module named 'ujson'" #2246
|
||||||
|
* Remove surrounding quotes for quoted command arguments #2247 (related to #2239)
|
||||||
|
|
||||||
===============
|
===============
|
||||||
Version 3.3.1
|
Version 3.3.1
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Under development, see milestone https://github.com/nicolargo/glances/milestone/61
|
Enhancements:
|
||||||
|
|
||||||
|
* Minor change on the help screen
|
||||||
|
* Refactor some loop in the processes function
|
||||||
|
* Replace json by ujson #2201
|
||||||
|
|
||||||
|
Bug corrected:
|
||||||
|
|
||||||
|
* Unable to see docker related information #2180
|
||||||
|
* CSV export dependent on sort order for docker container cpu #2156
|
||||||
|
* Error when process list is displayed in Programs mode #2209
|
||||||
|
* Console formatting permanently messed up when other text printed #2211
|
||||||
|
* API GET uptime returns formatted string, not seconds as the doc says #2158
|
||||||
|
* Glances UI is breaking for multiline commands #2189
|
||||||
|
|
||||||
|
Documentation and CI:
|
||||||
|
|
||||||
|
* Add unitary test for memory profiling
|
||||||
|
* Update memory profile chart
|
||||||
|
* Add run-docker-ubuntu-* in Makefile
|
||||||
|
* The open-web-browser option was missing dashes #2219
|
||||||
|
* Correct regexp in glances.conf file example
|
||||||
|
* What is CW from network #2222 (related to discussion #2221)
|
||||||
|
* Change Glances repology URL
|
||||||
|
* Add example for the date format
|
||||||
|
* Correct Flake8 configuration file
|
||||||
|
* Drop UT for Python 3.5 and 3.6 (no more available in Ubuntu 22.04)
|
||||||
|
* Correct unitary test with Python 3.5
|
||||||
|
* Update Makefile with comments
|
||||||
|
* Update Python minimal requirement for py3nvlm
|
||||||
|
* Update security policy (user can open private issue directly in Github)
|
||||||
|
* Add a simple run script. Entry point for IDE debuger
|
||||||
|
|
||||||
|
Cyber security update:
|
||||||
|
|
||||||
|
* Security alert on ujson < 5.4
|
||||||
|
* Merge pull request #2243 from nicolargo/renovate/nvidia-cuda-12.x
|
||||||
|
* Merge pull request #2244 from nicolargo/renovate/crazy-max-ghaction-docker-meta-4.x
|
||||||
|
* Merge pull request #2228 from nicolargo/renovate/zeroconf-0.x
|
||||||
|
* Merge pull request #2242 from nicolargo/renovate/crazy-max-ghaction-docker-meta-4.x
|
||||||
|
* Merge pull request #2239 from mfridge/action-command-split
|
||||||
|
* Merge pull request #2165 from nicolargo/renovate/zeroconf-0.x
|
||||||
|
* Merge pull request #2199 from nicolargo/renovate/alpine-3.x
|
||||||
|
* Merge pull request #2202 from chncaption/oscs_fix_cdr0ts8au51t49so8c6g
|
||||||
|
* Bump loader-utils from 2.0.0 to 2.0.3 in /glances/outputs/static #2187 - Update Web lib
|
||||||
|
|
||||||
|
Contributors for this version:
|
||||||
|
|
||||||
|
* Nicolargo
|
||||||
|
* renovate[bot]
|
||||||
|
* chncaption
|
||||||
|
* fkwong
|
||||||
|
* *mfridge
|
||||||
|
|
||||||
|
And also a big thanks to @RazCrimson (https://github.com/RazCrimson) for the support to the Glances community !
|
||||||
|
|
||||||
===============
|
===============
|
||||||
Version 3.3.0.4
|
Version 3.3.0.4
|
||||||
|
21
README.rst
21
README.rst
@ -40,16 +40,22 @@ Glances - An eye on your system
|
|||||||
Summary
|
Summary
|
||||||
=======
|
=======
|
||||||
|
|
||||||
**Glances** is a cross-platform monitoring tool which aims to present a
|
**Glances** is an open-source system cross-platform monitoring tool.
|
||||||
large amount of monitoring information through a curses or Web
|
It allows real-time monitoring of various aspects of your system such as
|
||||||
based interface. The information dynamically adapts depending on the
|
CPU, memory, disk, network usage etc. It also allows monitoring of running processes,
|
||||||
size of the user interface.
|
logged in users, temperatures, voltages, fan speeds etc.
|
||||||
|
It also supports container monitoring, it supports different container management
|
||||||
|
systems such as Docker, LXC. The information is presented in an easy to read dashboard
|
||||||
|
and can also be used for remote monitoring of systems via a web interface or command
|
||||||
|
line interface. It is easy to install and use and can be customized to show only
|
||||||
|
the information that you are interested in.
|
||||||
|
|
||||||
.. image:: https://raw.githubusercontent.com/nicolargo/glances/develop/docs/_static/glances-summary.png
|
.. image:: https://raw.githubusercontent.com/nicolargo/glances/develop/docs/_static/glances-summary.png
|
||||||
|
|
||||||
It can also work in client/server mode. Remote monitoring could be done
|
In client/server mode, remote monitoring could be done via terminal,
|
||||||
via terminal, Web interface or API (XML-RPC and RESTful). Stats can also
|
Web interface or API (XML-RPC and RESTful).
|
||||||
be exported to files or external time/value databases.
|
Stats can also be exported to files or external time/value databases, CSV or direct
|
||||||
|
output to STDOUT.
|
||||||
|
|
||||||
.. image:: https://raw.githubusercontent.com/nicolargo/glances/develop/docs/_static/glances-responsive-webdesign.png
|
.. image:: https://raw.githubusercontent.com/nicolargo/glances/develop/docs/_static/glances-responsive-webdesign.png
|
||||||
|
|
||||||
@ -111,6 +117,7 @@ Optional dependencies:
|
|||||||
- ``py-cpuinfo`` (for the Quicklook CPU info module)
|
- ``py-cpuinfo`` (for the Quicklook CPU info module)
|
||||||
- ``pygal`` (for the graph export module)
|
- ``pygal`` (for the graph export module)
|
||||||
- ``pymdstat`` (for RAID support) [Linux-only]
|
- ``pymdstat`` (for RAID support) [Linux-only]
|
||||||
|
- ``pymongo`` (for the MongoDB export module) [Only for Python >= 3.7]
|
||||||
- ``pysnmp`` (for SNMP support)
|
- ``pysnmp`` (for SNMP support)
|
||||||
- ``pySMART.smartx`` (for HDD Smart support) [Linux-only]
|
- ``pySMART.smartx`` (for HDD Smart support) [Linux-only]
|
||||||
- ``pyzmq`` (for the ZeroMQ export module)
|
- ``pyzmq`` (for the ZeroMQ export module)
|
||||||
|
@ -307,6 +307,7 @@ battery_critical=95
|
|||||||
#core 0_fans_speed_alias=CPU Core 0 fan
|
#core 0_fans_speed_alias=CPU Core 0 fan
|
||||||
#or
|
#or
|
||||||
#core 0_alias=CPU Core 0
|
#core 0_alias=CPU Core 0
|
||||||
|
#core 1_alias=CPU Core 1
|
||||||
|
|
||||||
[processcount]
|
[processcount]
|
||||||
disable=False
|
disable=False
|
||||||
@ -583,6 +584,15 @@ db=glances
|
|||||||
#user=root
|
#user=root
|
||||||
#password=root
|
#password=root
|
||||||
|
|
||||||
|
[mongodb]
|
||||||
|
# Configuration for the --export mongodb option
|
||||||
|
# https://www.mongodb.com
|
||||||
|
host=localhost
|
||||||
|
port=27017
|
||||||
|
db=glances
|
||||||
|
user=root
|
||||||
|
password=example
|
||||||
|
|
||||||
[kafka]
|
[kafka]
|
||||||
# Configuration for the --export kafka option
|
# Configuration for the --export kafka option
|
||||||
# http://kafka.apache.org/
|
# http://kafka.apache.org/
|
||||||
|
@ -9,3 +9,4 @@ codespell
|
|||||||
memory-profiler
|
memory-profiler
|
||||||
matplotlib
|
matplotlib
|
||||||
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
|
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
|
@ -10,8 +10,10 @@ refresh=2
|
|||||||
# Does Glances should check if a newer version is available on PyPI ?
|
# Does Glances should check if a newer version is available on PyPI ?
|
||||||
check_update=false
|
check_update=false
|
||||||
# History size (maximum number of values)
|
# History size (maximum number of values)
|
||||||
# Default is 3600 seconds (1 hour)
|
# Default is 1200 values (~1h with the default refresh rate)
|
||||||
history_size=3600
|
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"
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
# User interface
|
# User interface
|
||||||
@ -212,7 +214,7 @@ critical=-85
|
|||||||
disable=False
|
disable=False
|
||||||
# Define the list of hidden disks (comma-separated regexp)
|
# Define the list of hidden disks (comma-separated regexp)
|
||||||
#hide=sda2,sda5,loop.*
|
#hide=sda2,sda5,loop.*
|
||||||
hide=loop.*,/dev/loop*
|
hide=loop.*,/dev/loop.*
|
||||||
# Define the list of disks to be show (comma-separated)
|
# Define the list of disks to be show (comma-separated)
|
||||||
#show=sda.*
|
#show=sda.*
|
||||||
# Alias for sda1
|
# Alias for sda1
|
||||||
@ -582,6 +584,15 @@ db=glances
|
|||||||
#user=root
|
#user=root
|
||||||
#password=root
|
#password=root
|
||||||
|
|
||||||
|
[mongodb]
|
||||||
|
# Configuration for the --export mongodb option
|
||||||
|
# https://www.mongodb.com
|
||||||
|
host=localhost
|
||||||
|
port=27017
|
||||||
|
db=glances
|
||||||
|
user=root
|
||||||
|
password=example
|
||||||
|
|
||||||
[kafka]
|
[kafka]
|
||||||
# Configuration for the --export kafka option
|
# Configuration for the --export kafka option
|
||||||
# http://kafka.apache.org/
|
# http://kafka.apache.org/
|
||||||
|
@ -26,6 +26,7 @@ RUN apk add --no-cache \
|
|||||||
curl \
|
curl \
|
||||||
lm-sensors \
|
lm-sensors \
|
||||||
wireless-tools \
|
wireless-tools \
|
||||||
|
smartmontools \
|
||||||
iputils
|
iputils
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
@ -89,9 +90,11 @@ RUN apk add --no-cache \
|
|||||||
python3 \
|
python3 \
|
||||||
py3-packaging \
|
py3-packaging \
|
||||||
py3-dateutil \
|
py3-dateutil \
|
||||||
|
py3-requests \
|
||||||
curl \
|
curl \
|
||||||
lm-sensors \
|
lm-sensors \
|
||||||
wireless-tools \
|
wireless-tools \
|
||||||
|
smartmontools \
|
||||||
iputils
|
iputils
|
||||||
|
|
||||||
COPY --from=buildRequirements /root/.local/bin /usr/local/bin/
|
COPY --from=buildRequirements /root/.local/bin /usr/local/bin/
|
||||||
|
146
docker-files/ubuntu.Dockerfile
Normal file
146
docker-files/ubuntu.Dockerfile
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
#
|
||||||
|
# Glances Dockerfile (based on Ubuntu)
|
||||||
|
#
|
||||||
|
# https://github.com/nicolargo/glances
|
||||||
|
#
|
||||||
|
|
||||||
|
# WARNING: the versions should be set.
|
||||||
|
# Ex: Python 3.10 for Ubuntu 22.04
|
||||||
|
# Note: ENV is for future running containers. ARG for building your Docker image.
|
||||||
|
|
||||||
|
ARG IMAGE_VERSION=12.0.1-base-ubuntu22.04
|
||||||
|
ARG PYTHON_VERSION=3.10
|
||||||
|
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||||
|
FROM nvidia/cuda:${IMAGE_VERSION} as build
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
python3 \
|
||||||
|
python3-dev \
|
||||||
|
python3-pip \
|
||||||
|
python3-wheel \
|
||||||
|
musl-dev \
|
||||||
|
build-essential \
|
||||||
|
libzmq5 \
|
||||||
|
curl \
|
||||||
|
lm-sensors \
|
||||||
|
wireless-tools \
|
||||||
|
smartmontools \
|
||||||
|
net-tools \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# Install the dependencies beforehand to make them cacheable
|
||||||
|
|
||||||
|
FROM build as buildRequirements
|
||||||
|
ARG PYTHON_VERSION
|
||||||
|
ARG PIP_MIRROR
|
||||||
|
|
||||||
|
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN python${PYTHON_VERSION} -m pip install --no-cache-dir --user -r requirements.txt -i ${PIP_MIRROR}
|
||||||
|
|
||||||
|
# Minimal means no webui, but it break what is done previously (see #2155)
|
||||||
|
# So install the webui requirements...
|
||||||
|
COPY webui-requirements.txt .
|
||||||
|
RUN python${PYTHON_VERSION} -m pip install --no-cache-dir --user -r webui-requirements.txt -i ${PIP_MIRROR}
|
||||||
|
|
||||||
|
# As minimal image we want to monitor others docker containers
|
||||||
|
RUN python${PYTHON_VERSION} -m pip install --no-cache-dir --user docker -i ${PIP_MIRROR}
|
||||||
|
|
||||||
|
# Force install otherwise it could be cached without rerun
|
||||||
|
ARG CHANGING_ARG
|
||||||
|
RUN python${PYTHON_VERSION} -m pip install --no-cache-dir --user glances -i ${PIP_MIRROR}
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
FROM build as buildOptionalRequirements
|
||||||
|
ARG PYTHON_VERSION
|
||||||
|
ARG PIP_MIRROR
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
COPY optional-requirements.txt .
|
||||||
|
RUN CASS_DRIVER_NO_CYTHON=1 pip3 install --no-cache-dir --user -r optional-requirements.txt -i ${PIP_MIRROR}
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# full image
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
FROM build as full
|
||||||
|
ARG PYTHON_VERSION
|
||||||
|
|
||||||
|
COPY --from=buildRequirements /root/.local/bin /root/.local/bin/
|
||||||
|
COPY --from=buildRequirements /root/.local/lib/python${PYTHON_VERSION}/site-packages /root/.local/lib/python${PYTHON_VERSION}/site-packages/
|
||||||
|
COPY --from=buildOptionalRequirements /root/.local/lib/python${PYTHON_VERSION}/site-packages /root/.local/lib/python${PYTHON_VERSION}/site-packages/
|
||||||
|
COPY ./docker-compose/glances.conf /etc/glances.conf
|
||||||
|
|
||||||
|
# EXPOSE PORT (XMLRPC / WebUI)
|
||||||
|
EXPOSE 61209 61208
|
||||||
|
|
||||||
|
# Define default command.
|
||||||
|
WORKDIR /glances
|
||||||
|
CMD python3 -m glances -C /etc/glances.conf $GLANCES_OPT
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# minimal image
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Create running images without any building dependency
|
||||||
|
FROM nvidia/cuda:${IMAGE_VERSION} as minimal
|
||||||
|
ARG PYTHON_VERSION
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
python3 \
|
||||||
|
python3-packaging \
|
||||||
|
python3-dateutil \
|
||||||
|
python3-requests \
|
||||||
|
curl \
|
||||||
|
lm-sensors \
|
||||||
|
wireless-tools \
|
||||||
|
smartmontools \
|
||||||
|
net-tools \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=buildRequirements /root/.local/bin /root/.local/bin/
|
||||||
|
COPY --from=buildRequirements /root/.local/lib/python${PYTHON_VERSION}/site-packages /root/.local/lib/python${PYTHON_VERSION}/site-packages/
|
||||||
|
COPY ./docker-compose/glances.conf /etc/glances.conf
|
||||||
|
|
||||||
|
# EXPOSE PORT (XMLRPC / WebUI)
|
||||||
|
EXPOSE 61209 61208
|
||||||
|
|
||||||
|
# Define default command.
|
||||||
|
WORKDIR /glances
|
||||||
|
CMD python3 -m glances -C /etc/glances.conf $GLANCES_OPT
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# dev image
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
FROM full as dev
|
||||||
|
ARG PYTHON_VERSION
|
||||||
|
|
||||||
|
COPY --from=buildRequirements /root/.local/bin /root/.local/bin/
|
||||||
|
COPY --from=buildRequirements /root/.local/lib/python${PYTHON_VERSION}/site-packages /root/.local/lib/python${PYTHON_VERSION}/site-packages/
|
||||||
|
COPY --from=buildOptionalRequirements /root/.local/lib/python${PYTHON_VERSION}/site-packages /root/.local/lib/python${PYTHON_VERSION}/site-packages/
|
||||||
|
COPY ./docker-compose/glances.conf /etc/glances.conf
|
||||||
|
|
||||||
|
# Copy the current Glances source code
|
||||||
|
COPY . /glances
|
||||||
|
|
||||||
|
# EXPOSE PORT (XMLRPC / WebUI)
|
||||||
|
EXPOSE 61209 61208
|
||||||
|
|
||||||
|
# Forward access and error logs to Docker's log collector
|
||||||
|
RUN ln -sf /dev/stdout /tmp/glances-root.log \
|
||||||
|
&& ln -sf /dev/stderr /var/log/error.log
|
||||||
|
|
||||||
|
# Define default command.
|
||||||
|
WORKDIR /glances
|
||||||
|
CMD python3 -m glances -C /etc/glances.conf $GLANCES_OPT
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
Binary file not shown.
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
@ -37,3 +37,9 @@ or another example:
|
|||||||
|
|
||||||
[diskio]
|
[diskio]
|
||||||
show=sda.*
|
show=sda.*
|
||||||
|
|
||||||
|
Filtering is based on regular expression. Please be sure that your regular
|
||||||
|
expression works as expected. You can use an online tool like `regex101`_ in
|
||||||
|
order to test your regular expression.
|
||||||
|
|
||||||
|
.. _regex101: https://regex101.com/
|
@ -47,4 +47,9 @@ under the ``[docker]`` section:
|
|||||||
|
|
||||||
You can use all the variables ({{foo}}) available in the Docker plugin.
|
You can use all the variables ({{foo}}) available in the Docker plugin.
|
||||||
|
|
||||||
|
Filtering (for hide or show) is based on regular expression. Please be sure that your regular
|
||||||
|
expression works as expected. You can use an online tool like `regex101`_ in
|
||||||
|
order to test your regular expression.
|
||||||
|
|
||||||
|
.. _regex101: https://regex101.com/
|
||||||
.. _docker-py: https://github.com/docker/docker-py
|
.. _docker-py: https://github.com/docker/docker-py
|
||||||
|
@ -53,3 +53,9 @@ Example to only show /dev/sdb mount points:
|
|||||||
|
|
||||||
[fs]
|
[fs]
|
||||||
show=/dev/sdb.*
|
show=/dev/sdb.*
|
||||||
|
|
||||||
|
Filtering is based on regular expression. Please be sure that your regular
|
||||||
|
expression works as expected. You can use an online tool like `regex101`_ in
|
||||||
|
order to test your regular expression.
|
||||||
|
|
||||||
|
.. _regex101: https://regex101.com/
|
@ -22,7 +22,9 @@ file under the ``[ip]`` section:
|
|||||||
|
|
||||||
|
|
||||||
**NOTE:** Setting low values for `public_refresh_interval` will result in frequent
|
**NOTE:** Setting low values for `public_refresh_interval` will result in frequent
|
||||||
HTTP requests to the IP detection servers. Recommended range: 120-600 seconds
|
HTTP requests to the IP detection servers. Recommended range: 120-600 seconds.
|
||||||
|
Glances uses online services in order to get the IP addresses. Your IP address could be
|
||||||
|
blocked if too many requests are done.
|
||||||
|
|
||||||
If the Censys options are configured, the public IP address is also analysed (with the same interval)
|
If the Censys options are configured, the public IP address is also analysed (with the same interval)
|
||||||
and additional information is displayed.
|
and additional information is displayed.
|
||||||
|
@ -53,3 +53,9 @@ virtual docker interface (docker0, docker1, ...):
|
|||||||
wlan0_tx_warning=900000
|
wlan0_tx_warning=900000
|
||||||
wlan0_tx_critical=1000000
|
wlan0_tx_critical=1000000
|
||||||
wlan0_tx_log=True
|
wlan0_tx_log=True
|
||||||
|
|
||||||
|
Filtering is based on regular expression. Please be sure that your regular
|
||||||
|
expression works as expected. You can use an online tool like `regex101`_ in
|
||||||
|
order to test your regular expression.
|
||||||
|
|
||||||
|
.. _regex101: https://regex101.com/
|
1266
docs/api.rst
1266
docs/api.rst
File diff suppressed because it is too large
Load Diff
@ -9,42 +9,35 @@ following:
|
|||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
[couchdb]
|
[mongodb]
|
||||||
host=localhost
|
host=localhost
|
||||||
port=5984
|
port=27017
|
||||||
user=root
|
|
||||||
password=root
|
|
||||||
db=glances
|
db=glances
|
||||||
|
user=root
|
||||||
|
password=example
|
||||||
|
|
||||||
and run Glances with:
|
and run Glances with:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ glances --export couchdb
|
$ glances --export mongodb
|
||||||
|
|
||||||
Documents are stored in native ``JSON`` format. Glances adds ``"type"``
|
Documents are stored in native the configured database (glances by default)
|
||||||
and ``"time"`` entries:
|
with one collection per plugin.
|
||||||
|
|
||||||
- ``type``: plugin name
|
Example of MongoDB Document for the load stats:
|
||||||
- ``time``: timestamp (format: "2016-09-24T16:39:08.524828Z")
|
|
||||||
|
|
||||||
Example of Couch Document for the load stats:
|
|
||||||
|
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
|
||||||
{
|
{
|
||||||
"_id": "36cbbad81453c53ef08804cb2612d5b6",
|
_id: ObjectId('63d78ffee5528e543ce5af3a'),
|
||||||
"_rev": "1-382400899bec5615cabb99aa34df49fb",
|
min1: 1.46337890625,
|
||||||
"min15": 0.33,
|
min5: 1.09619140625,
|
||||||
"time": "2016-09-24T16:39:08.524828Z",
|
min15: 1.07275390625,
|
||||||
"min5": 0.4,
|
cpucore: 4,
|
||||||
"cpucore": 4,
|
history_size: 1200,
|
||||||
"load_warning": 1,
|
load_disable: 'False',
|
||||||
"min1": 0.5,
|
load_careful: 0.7,
|
||||||
"history_size": 28800,
|
load_warning: 1,
|
||||||
"load_critical": 5,
|
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.
|
|
||||||
|
@ -18,6 +18,7 @@ to providing stats to multiple services (see list below).
|
|||||||
json
|
json
|
||||||
kafka
|
kafka
|
||||||
mqtt
|
mqtt
|
||||||
|
mongodb
|
||||||
opentsdb
|
opentsdb
|
||||||
prometheus
|
prometheus
|
||||||
rabbitmq
|
rabbitmq
|
||||||
|
50
docs/gw/mongodb.rst
Normal file
50
docs/gw/mongodb.rst
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
.. _couchdb:
|
||||||
|
|
||||||
|
MongoDB
|
||||||
|
=======
|
||||||
|
|
||||||
|
You can export statistics to a ``MongoDB`` server.
|
||||||
|
The connection should be defined in the Glances configuration file as
|
||||||
|
following:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[couchdb]
|
||||||
|
host=localhost
|
||||||
|
port=
|
||||||
|
user=root
|
||||||
|
password=example
|
||||||
|
db=glances
|
||||||
|
|
||||||
|
and run Glances with:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ glances --export couchdb
|
||||||
|
|
||||||
|
Documents are stored in native ``JSON`` format. Glances adds ``"type"``
|
||||||
|
and ``"time"`` entries:
|
||||||
|
|
||||||
|
- ``type``: plugin name
|
||||||
|
- ``time``: timestamp (format: "2016-09-24T16:39:08.524828Z")
|
||||||
|
|
||||||
|
Example of Couch 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
|
||||||
|
}
|
||||||
|
|
||||||
|
You can view the result using the CouchDB utils URL: http://127.0.0.1:5984/_utils/database.html?glances.
|
@ -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" "Dec 21, 2022" "3.3.1_beta1" "Glances"
|
.TH "GLANCES" "1" "Jan 30, 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
|
||||||
@ -258,7 +258,7 @@ set the server cache time [default: 1 sec]
|
|||||||
.UNINDENT
|
.UNINDENT
|
||||||
.INDENT 0.0
|
.INDENT 0.0
|
||||||
.TP
|
.TP
|
||||||
.B open\-web\-browser
|
.B \-\-open\-web\-browser
|
||||||
try to open the Web UI in the default Web browser
|
try to open the Web UI in the default Web browser
|
||||||
.UNINDENT
|
.UNINDENT
|
||||||
.INDENT 0.0
|
.INDENT 0.0
|
||||||
@ -732,60 +732,60 @@ format):
|
|||||||
.nf
|
.nf
|
||||||
.ft C
|
.ft C
|
||||||
{
|
{
|
||||||
"version": 1,
|
\(dqversion\(dq: 1,
|
||||||
"disable_existing_loggers": "False",
|
\(dqdisable_existing_loggers\(dq: \(dqFalse\(dq,
|
||||||
"root": {
|
\(dqroot\(dq: {
|
||||||
"level": "INFO",
|
\(dqlevel\(dq: \(dqINFO\(dq,
|
||||||
"handlers": ["file", "console"]
|
\(dqhandlers\(dq: [\(dqfile\(dq, \(dqconsole\(dq]
|
||||||
},
|
},
|
||||||
"formatters": {
|
\(dqformatters\(dq: {
|
||||||
"standard": {
|
\(dqstandard\(dq: {
|
||||||
"format": "%(asctime)s \-\- %(levelname)s \-\- %(message)s"
|
\(dqformat\(dq: \(dq%(asctime)s \-\- %(levelname)s \-\- %(message)s\(dq
|
||||||
},
|
},
|
||||||
"short": {
|
\(dqshort\(dq: {
|
||||||
"format": "%(levelname)s: %(message)s"
|
\(dqformat\(dq: \(dq%(levelname)s: %(message)s\(dq
|
||||||
},
|
},
|
||||||
"free": {
|
\(dqfree\(dq: {
|
||||||
"format": "%(message)s"
|
\(dqformat\(dq: \(dq%(message)s\(dq
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers": {
|
\(dqhandlers\(dq: {
|
||||||
"file": {
|
\(dqfile\(dq: {
|
||||||
"level": "DEBUG",
|
\(dqlevel\(dq: \(dqDEBUG\(dq,
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
\(dqclass\(dq: \(dqlogging.handlers.RotatingFileHandler\(dq,
|
||||||
"formatter": "standard",
|
\(dqformatter\(dq: \(dqstandard\(dq,
|
||||||
"filename": "/var/tmp/glances.log"
|
\(dqfilename\(dq: \(dq/var/tmp/glances.log\(dq
|
||||||
},
|
},
|
||||||
"console": {
|
\(dqconsole\(dq: {
|
||||||
"level": "CRITICAL",
|
\(dqlevel\(dq: \(dqCRITICAL\(dq,
|
||||||
"class": "logging.StreamHandler",
|
\(dqclass\(dq: \(dqlogging.StreamHandler\(dq,
|
||||||
"formatter": "free"
|
\(dqformatter\(dq: \(dqfree\(dq
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loggers": {
|
\(dqloggers\(dq: {
|
||||||
"debug": {
|
\(dqdebug\(dq: {
|
||||||
"handlers": ["file", "console"],
|
\(dqhandlers\(dq: [\(dqfile\(dq, \(dqconsole\(dq],
|
||||||
"level": "DEBUG"
|
\(dqlevel\(dq: \(dqDEBUG\(dq
|
||||||
},
|
},
|
||||||
"verbose": {
|
\(dqverbose\(dq: {
|
||||||
"handlers": ["file", "console"],
|
\(dqhandlers\(dq: [\(dqfile\(dq, \(dqconsole\(dq],
|
||||||
"level": "INFO"
|
\(dqlevel\(dq: \(dqINFO\(dq
|
||||||
},
|
},
|
||||||
"standard": {
|
\(dqstandard\(dq: {
|
||||||
"handlers": ["file"],
|
\(dqhandlers\(dq: [\(dqfile\(dq],
|
||||||
"level": "INFO"
|
\(dqlevel\(dq: \(dqINFO\(dq
|
||||||
},
|
},
|
||||||
"requests": {
|
\(dqrequests\(dq: {
|
||||||
"handlers": ["file", "console"],
|
\(dqhandlers\(dq: [\(dqfile\(dq, \(dqconsole\(dq],
|
||||||
"level": "ERROR"
|
\(dqlevel\(dq: \(dqERROR\(dq
|
||||||
},
|
},
|
||||||
"elasticsearch": {
|
\(dqelasticsearch\(dq: {
|
||||||
"handlers": ["file", "console"],
|
\(dqhandlers\(dq: [\(dqfile\(dq, \(dqconsole\(dq],
|
||||||
"level": "ERROR"
|
\(dqlevel\(dq: \(dqERROR\(dq
|
||||||
},
|
},
|
||||||
"elasticsearch.trace": {
|
\(dqelasticsearch.trace\(dq: {
|
||||||
"handlers": ["file", "console"],
|
\(dqhandlers\(dq: [\(dqfile\(dq, \(dqconsole\(dq],
|
||||||
"level": "ERROR"
|
\(dqlevel\(dq: \(dqERROR\(dq
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -885,6 +885,6 @@ $ glances –browser
|
|||||||
.sp
|
.sp
|
||||||
Nicolas Hennion aka Nicolargo <\fI\%contact@nicolargo.com\fP>
|
Nicolas Hennion aka Nicolargo <\fI\%contact@nicolargo.com\fP>
|
||||||
.SH COPYRIGHT
|
.SH COPYRIGHT
|
||||||
2022, Nicolas Hennion
|
2023, Nicolas Hennion
|
||||||
.\" Generated by docutils manpage writer.
|
.\" Generated by docutils manpage writer.
|
||||||
.
|
.
|
||||||
|
@ -19,7 +19,7 @@ import sys
|
|||||||
# Global name
|
# Global name
|
||||||
# Version should start and end with a numerical char
|
# Version should start and end with a numerical char
|
||||||
# See https://packaging.python.org/specifications/core-metadata/#version
|
# See https://packaging.python.org/specifications/core-metadata/#version
|
||||||
__version__ = '3.3.1_beta1'
|
__version__ = '3.4.0_beta1'
|
||||||
__author__ = 'Nicolas Hennion <nicolas@nicolargo.com>'
|
__author__ = 'Nicolas Hennion <nicolas@nicolargo.com>'
|
||||||
__license__ = 'LGPLv3'
|
__license__ = 'LGPLv3'
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Glances - An eye on your system
|
# Glances - An eye on your system
|
||||||
|
@ -75,7 +75,7 @@ class GlancesClientBrowser(object):
|
|||||||
# Try with the preconfigure password (only if status is PROTECTED)
|
# Try with the preconfigure password (only if status is PROTECTED)
|
||||||
clear_password = self.password.get_password(server['name'])
|
clear_password = self.password.get_password(server['name'])
|
||||||
if clear_password is not None:
|
if clear_password is not None:
|
||||||
server['password'] = self.password.sha256_hash(clear_password)
|
server['password'] = self.password.get_hash(clear_password)
|
||||||
return 'http://{}:{}@{}:{}'.format(server['username'], server['password'], server['ip'], server['port'])
|
return 'http://{}:{}@{}:{}'.format(server['username'], server['password'], server['ip'], server['port'])
|
||||||
else:
|
else:
|
||||||
return 'http://{}:{}'.format(server['ip'], server['port'])
|
return 'http://{}:{}'.format(server['ip'], server['port'])
|
||||||
@ -151,7 +151,7 @@ class GlancesClientBrowser(object):
|
|||||||
)
|
)
|
||||||
# Store the password for the selected server
|
# Store the password for the selected server
|
||||||
if clear_password is not None:
|
if clear_password is not None:
|
||||||
self.set_in_selected('password', self.password.sha256_hash(clear_password))
|
self.set_in_selected('password', self.password.get_hash(clear_password))
|
||||||
|
|
||||||
# Display the Glance client on the selected server
|
# Display the Glance client on the selected server
|
||||||
logger.info("Connect Glances client to the {} server".format(server['key']))
|
logger.info("Connect Glances client to the {} server".format(server['key']))
|
||||||
|
@ -68,6 +68,10 @@ if PY3:
|
|||||||
return s.decode()
|
return s.decode()
|
||||||
return s.encode('ascii', 'ignore').decode()
|
return s.encode('ascii', 'ignore').decode()
|
||||||
|
|
||||||
|
def to_hex(s):
|
||||||
|
"""Convert the bytes string to a hex string"""
|
||||||
|
return s.hex()
|
||||||
|
|
||||||
def listitems(d):
|
def listitems(d):
|
||||||
return list(d.items())
|
return list(d.items())
|
||||||
|
|
||||||
@ -166,6 +170,10 @@ else:
|
|||||||
return s
|
return s
|
||||||
return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore')
|
return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore')
|
||||||
|
|
||||||
|
def to_hex(s):
|
||||||
|
"""Convert the string to a hex string in Python 2"""
|
||||||
|
return s.encode('hex')
|
||||||
|
|
||||||
def listitems(d):
|
def listitems(d):
|
||||||
return d.items()
|
return d.items()
|
||||||
|
|
||||||
|
@ -81,10 +81,10 @@ class Export(GlancesExport):
|
|||||||
# Loop over plugins to export
|
# Loop over plugins to export
|
||||||
for plugin in self.plugins_to_export(stats):
|
for plugin in self.plugins_to_export(stats):
|
||||||
if isinstance(all_stats[plugin], list):
|
if isinstance(all_stats[plugin], list):
|
||||||
for stat in all_stats[plugin]:
|
for stat in sorted(all_stats[plugin], key=lambda x: x['key']):
|
||||||
# First line: header
|
# First line: header
|
||||||
if self.first_line:
|
if self.first_line:
|
||||||
csv_header += ('{}_{}_{}'.format(plugin, self.get_item_key(stat), item) for item in stat)
|
csv_header += ['{}_{}_{}'.format(plugin, self.get_item_key(stat), item) for item in stat]
|
||||||
# Others lines: stats
|
# Others lines: stats
|
||||||
csv_data += itervalues(stat)
|
csv_data += itervalues(stat)
|
||||||
elif isinstance(all_stats[plugin], dict):
|
elif isinstance(all_stats[plugin], dict):
|
||||||
|
81
glances/exports/glances_mongodb.py
Normal file
81
glances/exports/glances_mongodb.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# This file is part of Glances.
|
||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
#
|
||||||
|
|
||||||
|
"""MongoDB interface class."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from glances.logger import logger
|
||||||
|
from glances.exports.glances_export import GlancesExport
|
||||||
|
|
||||||
|
import pymongo
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
|
|
||||||
|
class Export(GlancesExport):
|
||||||
|
|
||||||
|
"""This class manages the MongoDB export module."""
|
||||||
|
|
||||||
|
def __init__(self, config=None, args=None):
|
||||||
|
"""Init the MongoDB 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('mongodb', mandatories=['host', 'port', 'db'], options=['user', 'password'])
|
||||||
|
if not self.export_enable:
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
# Init the CouchDB client
|
||||||
|
self.client = self.init()
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
"""Init the connection to the CouchDB server."""
|
||||||
|
if not self.export_enable:
|
||||||
|
return None
|
||||||
|
|
||||||
|
server_uri = 'mongodb://%s:%s@%s:%s' % (quote_plus(self.user),
|
||||||
|
quote_plus(self.password),
|
||||||
|
self.host,
|
||||||
|
self.port)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = pymongo.MongoClient(server_uri)
|
||||||
|
client.admin.command('ping')
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical("Cannot connect to MongoDB server %s:%s (%s)" % (self.host, self.port, e))
|
||||||
|
sys.exit(2)
|
||||||
|
else:
|
||||||
|
logger.info("Connected to the MongoDB server")
|
||||||
|
|
||||||
|
return client
|
||||||
|
|
||||||
|
def database(self):
|
||||||
|
"""Return the CouchDB database object"""
|
||||||
|
return self.client[self.db]
|
||||||
|
|
||||||
|
def export(self, name, columns, points):
|
||||||
|
"""Write the points to the MongoDB server."""
|
||||||
|
logger.debug("Export {} stats to MongoDB".format(name))
|
||||||
|
|
||||||
|
# Create DB input
|
||||||
|
data = dict(zip(columns, points))
|
||||||
|
|
||||||
|
# Write data to the MongoDB database
|
||||||
|
try:
|
||||||
|
self.database()[name].insert_one(data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Cannot export {} stats to MongoDB ({})".format(name, e))
|
@ -127,8 +127,10 @@ class GlancesBottle(object):
|
|||||||
if username == self.args.username:
|
if username == self.args.username:
|
||||||
from glances.password import GlancesPassword
|
from glances.password import GlancesPassword
|
||||||
|
|
||||||
pwd = GlancesPassword(username=username, config=self.config)
|
pwd = GlancesPassword(username=username,
|
||||||
return pwd.check_password(self.args.password, pwd.sha256_hash(password))
|
config=self.config)
|
||||||
|
return pwd.check_password(self.args.password,
|
||||||
|
pwd.get_hash(password))
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -160,6 +162,7 @@ class GlancesBottle(object):
|
|||||||
'/api/%s/<plugin>/<item>/history/<nb:int>' % self.API_VERSION, method="GET", callback=self._api_item_history
|
'/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>' % 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)
|
bindmsg = 'Glances RESTful API Server started on {}api/{}/'.format(self.bind_url, self.API_VERSION)
|
||||||
logger.info(bindmsg)
|
logger.info(bindmsg)
|
||||||
|
|
||||||
|
@ -123,6 +123,7 @@
|
|||||||
v-if="!args.disable_sensors"
|
v-if="!args.disable_sensors"
|
||||||
:data="data"
|
:data="data"
|
||||||
></glances-plugin-sensors>
|
></glances-plugin-sensors>
|
||||||
|
<glances-plugin-now :data="data"></glances-plugin-now>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-18">
|
<div class="col-sm-18">
|
||||||
@ -138,13 +139,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-24">
|
|
||||||
<glances-plugin-now :data="data"></glances-plugin-now>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
869
glances/outputs/static/package-lock.json
generated
869
glances/outputs/static/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,27 +3,27 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
"favico.js": "^0.3.10",
|
"favico.js": "^0.3.10",
|
||||||
"hotkeys-js": "^3.10.0",
|
"hotkeys-js": "^3.10.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"sanitize-html": "^2.7.2",
|
"sanitize-html": "^2.8.1",
|
||||||
"vue": "^3.2.41"
|
"vue": "^3.2.45"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.3",
|
||||||
"del": "^7.0.0",
|
"del": "^7.0.0",
|
||||||
"eslint": "^8.25.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-plugin-vue": "^9.6.0",
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"less-loader": "^11.1.0",
|
"less-loader": "^11.1.0",
|
||||||
"sass": "^1.55.0",
|
"sass": "^1.57.1",
|
||||||
"sass-loader": "^13.1.0",
|
"sass-loader": "^13.2.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"vue-loader": "^17.0.0",
|
"vue-loader": "^17.0.1",
|
||||||
"webpack": "^5.74.0",
|
"webpack": "^5.75.0",
|
||||||
"webpack-cli": "^4.10.0",
|
"webpack-cli": "^5.0.1",
|
||||||
"webpack-dev-server": "^4.11.1"
|
"webpack-dev-server": "^4.11.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
12
glances/outputs/static/public/glances.js
vendored
12
glances/outputs/static/public/glances.js
vendored
File diff suppressed because one or more lines are too long
@ -16,7 +16,7 @@ import sys
|
|||||||
import uuid
|
import uuid
|
||||||
from io import open
|
from io import open
|
||||||
|
|
||||||
from glances.compat import b, input
|
from glances.compat import b, input, to_hex
|
||||||
from glances.config import user_config_dir
|
from glances.config import user_config_dir
|
||||||
from glances.globals import safe_makedirs
|
from glances.globals import safe_makedirs
|
||||||
from glances.logger import logger
|
from glances.logger import logger
|
||||||
@ -36,25 +36,26 @@ class GlancesPassword(object):
|
|||||||
|
|
||||||
def local_password_path(self):
|
def local_password_path(self):
|
||||||
"""Return the local password path.
|
"""Return the local password path.
|
||||||
Related toissue: Password files in same configuration dir in effect #2143
|
Related to issue: Password files in same configuration dir in effect #2143
|
||||||
"""
|
"""
|
||||||
if self.config is None:
|
if self.config is None:
|
||||||
return user_config_dir()
|
return user_config_dir()
|
||||||
else:
|
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())
|
||||||
|
|
||||||
def sha256_hash(self, plain_password):
|
def get_hash(self, plain_password, salt=''):
|
||||||
"""Return the SHA-256 of the given password."""
|
"""Return the hashed password, salt + pbkdf2_hmac."""
|
||||||
return hashlib.sha256(b(plain_password)).hexdigest()
|
return to_hex(hashlib.pbkdf2_hmac('sha256',
|
||||||
|
plain_password.encode(),
|
||||||
def get_hash(self, salt, plain_password):
|
salt.encode(),
|
||||||
"""Return the hashed password, salt + SHA-256."""
|
100000,
|
||||||
return hashlib.sha256(salt.encode() + plain_password.encode()).hexdigest()
|
dklen=128))
|
||||||
|
|
||||||
def hash_password(self, plain_password):
|
def hash_password(self, plain_password):
|
||||||
"""Hash password with a salt based on UUID (universally unique identifier)."""
|
"""Hash password with a salt based on UUID (universally unique identifier)."""
|
||||||
salt = uuid.uuid4().hex
|
salt = uuid.uuid4().hex
|
||||||
encrypted_password = self.get_hash(salt, plain_password)
|
encrypted_password = self.get_hash(plain_password,
|
||||||
|
salt=salt)
|
||||||
return salt + '$' + encrypted_password
|
return salt + '$' + encrypted_password
|
||||||
|
|
||||||
def check_password(self, hashed_password, plain_password):
|
def check_password(self, hashed_password, plain_password):
|
||||||
@ -63,7 +64,8 @@ class GlancesPassword(object):
|
|||||||
Return the comparison with the encrypted_password.
|
Return the comparison with the encrypted_password.
|
||||||
"""
|
"""
|
||||||
salt, encrypted_password = hashed_password.split('$')
|
salt, encrypted_password = hashed_password.split('$')
|
||||||
re_encrypted_password = self.get_hash(salt, plain_password)
|
re_encrypted_password = self.get_hash(plain_password,
|
||||||
|
salt = salt)
|
||||||
return encrypted_password == re_encrypted_password
|
return encrypted_password == re_encrypted_password
|
||||||
|
|
||||||
def get_password(self, description='', confirm=False, clear=False):
|
def get_password(self, description='', confirm=False, clear=False):
|
||||||
@ -72,11 +74,11 @@ class GlancesPassword(object):
|
|||||||
For Glances server, get the password (confirm=True, clear=False):
|
For Glances server, get the password (confirm=True, clear=False):
|
||||||
1) from the password file (if it exists)
|
1) from the password file (if it exists)
|
||||||
2) from the CLI
|
2) from the CLI
|
||||||
Optionally: save the password to a file (hashed with salt + SHA-256)
|
Optionally: save the password to a file (hashed with salt + SHA-pbkdf2_hmac)
|
||||||
|
|
||||||
For Glances client, get the password (confirm=False, clear=True):
|
For Glances client, get the password (confirm=False, clear=True):
|
||||||
1) from the CLI
|
1) from the CLI
|
||||||
2) the password is hashed with SHA-256 (only SHA string transit
|
2) the password is hashed with SHA-pbkdf2_hmac (only SHA string transit
|
||||||
through the network)
|
through the network)
|
||||||
"""
|
"""
|
||||||
if os.path.exists(self.password_file) and not clear:
|
if os.path.exists(self.password_file) and not clear:
|
||||||
@ -84,21 +86,21 @@ class GlancesPassword(object):
|
|||||||
logger.info("Read password from file {}".format(self.password_file))
|
logger.info("Read password from file {}".format(self.password_file))
|
||||||
password = self.load_password()
|
password = self.load_password()
|
||||||
else:
|
else:
|
||||||
# password_sha256 is the plain SHA-256 password
|
# password_hash is the plain SHA-pbkdf2_hmac password
|
||||||
# password_hashed is the salt + SHA-256 password
|
# password_hashed is the salt + SHA-pbkdf2_hmac password
|
||||||
password_sha256 = self.sha256_hash(getpass.getpass(description))
|
password_hash = self.get_hash(getpass.getpass(description))
|
||||||
password_hashed = self.hash_password(password_sha256)
|
password_hashed = self.hash_password(password_hash)
|
||||||
if confirm:
|
if confirm:
|
||||||
# password_confirm is the clear password (only used to compare)
|
# password_confirm is the clear password (only used to compare)
|
||||||
password_confirm = self.sha256_hash(getpass.getpass('Password (confirm): '))
|
password_confirm = self.get_hash(getpass.getpass('Password (confirm): '))
|
||||||
|
|
||||||
if not self.check_password(password_hashed, password_confirm):
|
if not self.check_password(password_hashed, password_confirm):
|
||||||
logger.critical("Sorry, passwords do not match. Exit.")
|
logger.critical("Sorry, passwords do not match. Exit.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Return the plain SHA-256 or the salted password
|
# Return the plain SHA-pbkdf2_hmac or the salted password
|
||||||
if clear:
|
if clear:
|
||||||
password = password_sha256
|
password = password_hash
|
||||||
else:
|
else:
|
||||||
password = password_hashed
|
password = password_hashed
|
||||||
|
|
||||||
|
@ -29,50 +29,26 @@ import psutil
|
|||||||
# 'key': 'interface_name'}
|
# 'key': 'interface_name'}
|
||||||
# Fields description
|
# Fields description
|
||||||
fields_description = {
|
fields_description = {
|
||||||
'interface_name': {
|
'interface_name': {'description': 'Interface name.', 'unit': 'string'},
|
||||||
'description': 'Interface name.',
|
'alias': {'description': 'Interface alias name (optional).', 'unit': 'string'},
|
||||||
'unit': 'string'
|
'rx': {'description': 'The received/input rate (in bit per second).', 'unit': 'bps'},
|
||||||
},
|
'tx': {'description': 'The sent/output rate (in bit per second).', 'unit': 'bps'},
|
||||||
'alias': {
|
'cx': {'description': 'The cumulative received+sent rate (in bit per second).', 'unit': 'bps'},
|
||||||
'description': 'Interface alias name (optional).',
|
|
||||||
'unit': 'string'
|
|
||||||
},
|
|
||||||
'rx': {
|
|
||||||
'description': 'The received/input rate (in bit per second).',
|
|
||||||
'unit': 'bps'
|
|
||||||
},
|
|
||||||
'tx': {
|
|
||||||
'description': 'The sent/output rate (in bit per second).',
|
|
||||||
'unit': 'bps'
|
|
||||||
},
|
|
||||||
'cx': {
|
|
||||||
'description': 'The cumulative received+sent rate (in bit per second).',
|
|
||||||
'unit': 'bps'
|
|
||||||
},
|
|
||||||
'cumulative_rx': {
|
'cumulative_rx': {
|
||||||
'description': 'The number of bytes received through the interface (cumulative).',
|
'description': 'The number of bytes received through the interface (cumulative).',
|
||||||
'unit': 'bytes',
|
'unit': 'bytes',
|
||||||
},
|
},
|
||||||
'cumulative_tx': {
|
'cumulative_tx': {'description': 'The number of bytes sent through the interface (cumulative).', 'unit': 'bytes'},
|
||||||
'description': 'The number of bytes sent through the interface (cumulative).',
|
|
||||||
'unit': 'bytes'
|
|
||||||
},
|
|
||||||
'cumulative_cx': {
|
'cumulative_cx': {
|
||||||
'description': 'The cumulative number of bytes reveived and sent through the interface (cumulative).',
|
'description': 'The cumulative number of bytes reveived and sent through the interface (cumulative).',
|
||||||
'unit': 'bytes'
|
'unit': 'bytes',
|
||||||
},
|
},
|
||||||
'speed': {
|
'speed': {
|
||||||
'description': 'Maximum interface speed (in bit per second). Can return 0 on some operating-system.',
|
'description': 'Maximum interface speed (in bit per second). Can return 0 on some operating-system.',
|
||||||
'unit': 'bps',
|
'unit': 'bps',
|
||||||
},
|
},
|
||||||
'is_up': {
|
'is_up': {'description': 'Is the interface up ?', 'unit': 'bool'},
|
||||||
'description': 'Is the interface up ?',
|
'time_since_update': {'description': 'Number of seconds since last update.', 'unit': 'seconds'},
|
||||||
'unit': 'bool'
|
|
||||||
},
|
|
||||||
'time_since_update': {
|
|
||||||
'description': 'Number of seconds since last update.',
|
|
||||||
'unit': 'seconds'
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# SNMP OID
|
# SNMP OID
|
||||||
|
@ -383,17 +383,19 @@ class GlancesProcesses(object):
|
|||||||
# Build the processes stats list (it is why we need psutil>=5.3.0)
|
# Build the processes stats list (it is why we need psutil>=5.3.0)
|
||||||
# This is one of the main bottleneck of Glances (see flame graph)
|
# This is one of the main bottleneck of Glances (see flame graph)
|
||||||
# Filter processes
|
# Filter processes
|
||||||
self.processlist = list(filter(lambda p: not (BSD and p.info['name'] == 'idle') and
|
self.processlist = list(
|
||||||
not (WINDOWS and p.info['name'] == 'System Idle Process') and
|
filter(
|
||||||
not (MACOS and p.info['name'] == 'kernel_task') and
|
lambda p: not (BSD and p.info['name'] == 'idle')
|
||||||
not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0),
|
and not (WINDOWS and p.info['name'] == 'System Idle Process')
|
||||||
psutil.process_iter(attrs=sorted_attrs, ad_value=None)))
|
and not (MACOS and p.info['name'] == 'kernel_task')
|
||||||
|
and not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0),
|
||||||
|
psutil.process_iter(attrs=sorted_attrs, ad_value=None),
|
||||||
|
)
|
||||||
|
)
|
||||||
# Only get the info key
|
# Only get the info key
|
||||||
self.processlist = [p.info for p in self.processlist]
|
self.processlist = [p.info for p in self.processlist]
|
||||||
# Sort the processes list by the current sort_key
|
# Sort the processes list by the current sort_key
|
||||||
self.processlist = sort_stats(self.processlist,
|
self.processlist = sort_stats(self.processlist, sorted_by=self.sort_key, reverse=True)
|
||||||
sorted_by=self.sort_key,
|
|
||||||
reverse=True)
|
|
||||||
|
|
||||||
# Update the processcount
|
# Update the processcount
|
||||||
self.update_processcount(self.processlist)
|
self.update_processcount(self.processlist)
|
||||||
@ -470,8 +472,7 @@ class GlancesProcesses(object):
|
|||||||
self.processlist_cache[proc['pid']] = {cached: proc[cached] for cached in cached_attrs}
|
self.processlist_cache[proc['pid']] = {cached: proc[cached] for cached in cached_attrs}
|
||||||
|
|
||||||
# Apply user filter
|
# Apply user filter
|
||||||
self.processlist = list(filter(lambda p: not self._filter.is_filtered(p),
|
self.processlist = list(filter(lambda p: not self._filter.is_filtered(p), self.processlist))
|
||||||
self.processlist))
|
|
||||||
|
|
||||||
# Compute the maximum value for keys in self._max_values_list: CPU, MEM
|
# Compute the maximum value for keys in self._max_values_list: CPU, MEM
|
||||||
# Useful to highlight the processes with maximum values
|
# Useful to highlight the processes with maximum values
|
||||||
|
@ -36,7 +36,7 @@ def processes_to_programs(processes):
|
|||||||
'name': p['name'],
|
'name': p['name'],
|
||||||
'cmdline': [p['name']],
|
'cmdline': [p['name']],
|
||||||
'pid': '_',
|
'pid': '_',
|
||||||
'username': p['username'],
|
'username': p['username'] if 'username' in p else '_',
|
||||||
'nice': p['nice'],
|
'nice': p['nice'],
|
||||||
'status': p['status'],
|
'status': p['status'],
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
from glances.compat import nativestr
|
from glances.compat import nativestr
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
def secure_popen(cmd):
|
def secure_popen(cmd):
|
||||||
@ -48,8 +49,9 @@ def __secure_popen(cmd):
|
|||||||
p_last = None
|
p_last = None
|
||||||
# Split by pipe '|'
|
# Split by pipe '|'
|
||||||
for sub_cmd in cmd.split('|'):
|
for sub_cmd in cmd.split('|'):
|
||||||
# Split by space ' '
|
# Split by space character, but do no split spaces within quotes (remove surrounding quotes, though)
|
||||||
sub_cmd_split = [i for i in sub_cmd.split(' ') if i]
|
tmp_split = [_ for _ in list(filter(None, re.split(r'(\s+)|(".*?"+?)|(\'.*?\'+?)', sub_cmd))) if _ != ' ']
|
||||||
|
sub_cmd_split = [_[1:-1] if (_[0]==_[-1]=='"') or (_[0]==_[-1]=='\'') else _ for _ in tmp_split]
|
||||||
p = Popen(sub_cmd_split, shell=False, stdin=sub_cmd_stdin, stdout=PIPE, stderr=PIPE)
|
p = Popen(sub_cmd_split, shell=False, stdin=sub_cmd_stdin, stdout=PIPE, stderr=PIPE)
|
||||||
if p_last is not None:
|
if p_last is not None:
|
||||||
# Allow p_last to receive a SIGPIPE if p exits.
|
# Allow p_last to receive a SIGPIPE if p exits.
|
||||||
|
@ -118,7 +118,7 @@ class GlancesStats(object):
|
|||||||
if args is not None:
|
if args is not None:
|
||||||
# If the all key is set in the disable_plugin option then look in the enable_plugin option
|
# If the all key is set in the disable_plugin option then look in the enable_plugin option
|
||||||
if getattr(args, 'disable_all', False):
|
if getattr(args, 'disable_all', False):
|
||||||
logger.info('%s => %s', name, getattr(args, 'enable_' + name, False))
|
logger.debug('%s => %s', name, getattr(args, 'enable_' + name, False))
|
||||||
setattr(args, 'disable_' + name, not getattr(args, 'enable_' + name, False))
|
setattr(args, 'disable_' + name, not getattr(args, 'enable_' + name, False))
|
||||||
else:
|
else:
|
||||||
setattr(args, 'disable_' + name, getattr(args, 'disable_' + name, False))
|
setattr(args, 'disable_' + name, getattr(args, 'disable_' + name, False))
|
||||||
|
@ -22,6 +22,7 @@ potsdb
|
|||||||
prometheus_client
|
prometheus_client
|
||||||
pygal
|
pygal
|
||||||
pymdstat
|
pymdstat
|
||||||
|
pymongo; python_version >= "3.7"
|
||||||
pysnmp
|
pysnmp
|
||||||
pySMART.smartx
|
pySMART.smartx
|
||||||
python-dateutil
|
python-dateutil
|
||||||
@ -32,5 +33,5 @@ six
|
|||||||
sparklines
|
sparklines
|
||||||
statsd
|
statsd
|
||||||
wifi
|
wifi
|
||||||
zeroconf==0.47.0; python_version < "3.7"
|
zeroconf==0.47.1; python_version < "3.7"
|
||||||
zeroconf; python_version >= "3.7"
|
zeroconf; python_version >= "3.7"
|
||||||
|
17
setup.py
17
setup.py
@ -41,7 +41,16 @@ def get_data_files():
|
|||||||
|
|
||||||
|
|
||||||
def get_install_requires():
|
def get_install_requires():
|
||||||
requires = ['psutil>=5.3.0', 'defusedxml', 'future', 'packaging']
|
requires = [
|
||||||
|
'psutil>=5.6.7',
|
||||||
|
'defusedxml',
|
||||||
|
'packaging',
|
||||||
|
'future; python_version < "3.0"',
|
||||||
|
'ujson<3; python_version < "3.0"',
|
||||||
|
'ujson<4; python_version >= "3.5" and python_version < "3.6"',
|
||||||
|
'ujson<5; python_version >= "3.6" and python_version < "3.7"',
|
||||||
|
'ujson>=5.4.0; python_version >= "3.7"',
|
||||||
|
]
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
requires.append('bottle')
|
requires.append('bottle')
|
||||||
requires.append('requests')
|
requires.append('requests')
|
||||||
@ -52,12 +61,12 @@ 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.0' if PY2 else 'zeroconf>=0.19.1'],
|
'browser': ['zeroconf==0.47.1' 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',
|
||||||
'graphitesender', 'influxdb>=1.0.0', 'kafka-python', 'pika',
|
'graphitesender', 'influxdb>=1.0.0', 'kafka-python', 'pymongo',
|
||||||
'paho-mqtt', 'potsdb', 'prometheus_client', 'pyzmq',
|
'pika', 'paho-mqtt', 'potsdb', 'prometheus_client', 'pyzmq',
|
||||||
'statsd'],
|
'statsd'],
|
||||||
'folders': ['scandir'], # python_version<"3.5"
|
'folders': ['scandir'], # python_version<"3.5"
|
||||||
'graph': ['pygal'],
|
'graph': ['pygal'],
|
||||||
|
Loading…
Reference in New Issue
Block a user