mirror of
https://github.com/nicolargo/glances.git
synced 2024-09-19 16:47:30 +03:00
Merge branch 'issue2181' into develop
This commit is contained in:
commit
32b33b5883
3
Makefile
3
Makefile
@ -80,6 +80,9 @@ test-min: ## Run unit tests in minimal environment
|
||||
test-min-with-upgrade: venv-min-upgrade ## Upgrade deps and run unit tests in minimal environment
|
||||
./venv-min/bin/python ./unitest.py
|
||||
|
||||
test-restful-api: ## Run unit tests of the RESTful API
|
||||
./venv/bin/python ./unitest-restful.py
|
||||
|
||||
# ===================================================================
|
||||
# Linters, profilers and cyber security
|
||||
# ===================================================================
|
||||
|
10
README.rst
10
README.rst
@ -92,17 +92,19 @@ Optional dependencies:
|
||||
|
||||
- ``batinfo`` (for battery monitoring)
|
||||
- ``bernhard`` (for the Riemann export module)
|
||||
- ``bottle`` (for Web server mode)
|
||||
- ``cassandra-driver`` (for the Cassandra export module)
|
||||
- ``chevron`` (for the action script feature)
|
||||
- ``docker`` (for the Containers Docker monitoring support)
|
||||
- ``elasticsearch`` (for the Elastic Search export module)
|
||||
- ``FastAPI`` and ``Uvicorn`` (for Web server mode)
|
||||
- ``graphitesender`` (For the Graphite export module)
|
||||
- ``hddtemp`` (for HDD temperature monitoring support) [Linux-only]
|
||||
- ``influxdb`` (for the InfluxDB version 1 export module)
|
||||
- ``influxdb-client`` (for the InfluxDB version 2 export module)
|
||||
- ``jinja2`` (for templating, used under the hood by FastAPI)
|
||||
- ``kafka-python`` (for the Kafka export module)
|
||||
- ``netifaces`` (for the IP plugin)
|
||||
- ``orjson`` (fast JSON library, used under the hood by FastAPI)
|
||||
- ``py3nvml`` (for the GPU plugin)
|
||||
- ``pycouchdb`` (for the CouchDB export module)
|
||||
- ``pika`` (for the RabbitMQ/ActiveMQ export module)
|
||||
@ -207,10 +209,10 @@ Get the Glances container:
|
||||
The following tags are availables:
|
||||
|
||||
- *latest-full* for a full Alpine Glances image (latest release) with all dependencies
|
||||
- *latest* for a basic Alpine Glances (latest release) version with minimal dependencies (Bottle and Docker)
|
||||
- *latest* for a basic Alpine Glances (latest release) version with minimal dependencies (FastAPI and Docker)
|
||||
- *dev* for a basic Alpine Glances image (based on development branch) with all dependencies (Warning: may be instable)
|
||||
- *ubuntu-latest-full* for a full Ubuntu Glances image (latest release) with all dependencies
|
||||
- *ubuntu-latest* for a basic Ubuntu Glances (latest release) version with minimal dependencies (Bottle and Docker)
|
||||
- *ubuntu-latest* for a basic Ubuntu Glances (latest release) version with minimal dependencies (FastAPI and Docker)
|
||||
- *ubuntu-dev* for a basic Ubuntu Glances image (based on development branch) with all dependencies (Warning: may be instable)
|
||||
|
||||
Run last version of Glances container in *console mode*:
|
||||
@ -319,7 +321,7 @@ Start Termux on your device and enter:
|
||||
$ apt update
|
||||
$ apt upgrade
|
||||
$ apt install clang python
|
||||
$ pip install bottle
|
||||
$ pip install fastapi uvicorn orjson jinja2
|
||||
$ pip install glances
|
||||
|
||||
And start Glances:
|
||||
|
@ -23,7 +23,7 @@ history_size=1200
|
||||
# Theme name for the Curses interface: black or white
|
||||
curse_theme=black
|
||||
# Limit the number of processes to display (for the WebUI)
|
||||
max_processes_display=30
|
||||
max_processes_display=25
|
||||
# Set the URL prefix (for the WebUI and the API)
|
||||
# Example: url_prefix=/glances/ => http://localhost/glances/
|
||||
# The final / is mandatory
|
||||
|
@ -23,7 +23,7 @@ history_size=1200
|
||||
# Theme name for the Curses interface: black or white
|
||||
curse_theme=black
|
||||
# Limit the number of processes to display (for the WebUI)
|
||||
max_processes_display=30
|
||||
max_processes_display=25
|
||||
# Set the URL prefix (for the WebUI and the API)
|
||||
# Example: url_prefix=/glances/ => http://localhost/glances/
|
||||
# The final / is mandatory
|
||||
|
530
docs/api.rst
530
docs/api.rst
File diff suppressed because it is too large
Load Diff
@ -172,7 +172,7 @@ Command-Line Options
|
||||
|
||||
.. option:: -w, --webserver
|
||||
|
||||
run Glances in web server mode (bottle lib needed)
|
||||
run Glances in web server mode (FastAPI lib needed)
|
||||
|
||||
.. option:: --cached-time CACHED_TIME
|
||||
|
||||
|
@ -30,7 +30,7 @@ Available tags (all images are based on both Alpine and Ubuntu Operating Systems
|
||||
* - `latest`
|
||||
- Alpine
|
||||
- Latest Release
|
||||
- Minimal + (Bottle & Docker)
|
||||
- Minimal + (FastAPI & Docker)
|
||||
* - `dev`
|
||||
- Alpine
|
||||
- develop
|
||||
@ -42,7 +42,7 @@ Available tags (all images are based on both Alpine and Ubuntu Operating Systems
|
||||
* - `ubuntu-latest`
|
||||
- Ubuntu
|
||||
- Latest Release
|
||||
- Minimal + (Bottle & Docker)
|
||||
- Minimal + (FastAPI & Docker)
|
||||
* - `ubuntu-dev`
|
||||
- Ubuntu
|
||||
- develop
|
||||
|
@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "GLANCES" "1" "Dec 10, 2023" "4.0.0_beta01" "Glances"
|
||||
.TH "GLANCES" "1" "Dec 16, 2023" "4.0.0_beta01" "Glances"
|
||||
.SH NAME
|
||||
glances \- An eye on your system
|
||||
.SH SYNOPSIS
|
||||
@ -254,7 +254,7 @@ set refresh time in seconds [default: 3 sec]
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-w, \-\-webserver
|
||||
run Glances in web server mode (bottle lib needed)
|
||||
run Glances in web server mode (FastAPI lib needed)
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
|
@ -12,7 +12,7 @@ globals.py Share variables upon modules
|
||||
main.py Main script to rule them up...
|
||||
client.py Glances client
|
||||
server.py Glances server
|
||||
webserver.py Glances web server (Bottle-based)
|
||||
webserver.py Glances web server (Based on FastAPI)
|
||||
autodiscover.py Glances autodiscover module (via zeroconf)
|
||||
standalone.py Glances standalone (curses interface)
|
||||
password.py Manage password for Glances client/server
|
||||
@ -27,7 +27,7 @@ plugins
|
||||
outputs
|
||||
=> Glances UI
|
||||
glances_curses.py The curses interface
|
||||
glances_bottle.py The web interface
|
||||
glances_restful-api.py The HTTP/API & Web based interface
|
||||
...
|
||||
exports
|
||||
=> Glances exports
|
||||
|
@ -36,6 +36,7 @@ class GlancesExport(object):
|
||||
'processlist',
|
||||
'psutilversion',
|
||||
'quicklook',
|
||||
'version'
|
||||
]
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
|
@ -25,6 +25,8 @@ import subprocess
|
||||
from datetime import datetime
|
||||
import re
|
||||
import base64
|
||||
import functools
|
||||
import weakref
|
||||
|
||||
import queue
|
||||
from configparser import ConfigParser, NoOptionError, NoSectionError
|
||||
@ -315,10 +317,10 @@ def json_dumps(data):
|
||||
return ujson.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
def json_dumps_dictlist(data, item):
|
||||
def dictlist(data, item):
|
||||
if isinstance(data, dict):
|
||||
try:
|
||||
return json_dumps({item: data[item]})
|
||||
return {item: data[item]}
|
||||
except (TypeError, IndexError, KeyError):
|
||||
return None
|
||||
elif isinstance(data, list):
|
||||
@ -326,13 +328,21 @@ def json_dumps_dictlist(data, item):
|
||||
# Source:
|
||||
# http://stackoverflow.com/questions/4573875/python-get-index-of-dictionary-item-in-list
|
||||
# But https://github.com/nicolargo/glances/issues/1401
|
||||
return json_dumps({item: list(map(itemgetter(item), data))})
|
||||
return {item: list(map(itemgetter(item), data))}
|
||||
except (TypeError, IndexError, KeyError):
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def json_dumps_dictlist(data, item):
|
||||
dl = dictlist(data, item)
|
||||
if dl is None:
|
||||
return None
|
||||
else:
|
||||
return json_dumps(dl)
|
||||
|
||||
|
||||
def string_value_to_float(s):
|
||||
"""Convert a string with a value and an unit to a float.
|
||||
Example:
|
||||
@ -398,3 +408,21 @@ def folder_size(path, errno=0):
|
||||
except OSError as e:
|
||||
ret_err = e.errno
|
||||
return ret_size, ret_err
|
||||
|
||||
|
||||
def weak_lru_cache(maxsize=128, typed=False):
|
||||
"""LRU Cache decorator that keeps a weak reference to self
|
||||
Source: https://stackoverflow.com/a/55990799"""
|
||||
def wrapper(func):
|
||||
|
||||
@functools.lru_cache(maxsize, typed)
|
||||
def _func(_self, *args, **kwargs):
|
||||
return func(_self(), *args, **kwargs)
|
||||
|
||||
@functools.wraps(func)
|
||||
def inner(self, *args, **kwargs):
|
||||
return _func(weakref.ref(self), *args, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
return wrapper
|
||||
|
@ -363,7 +363,7 @@ Examples of use:
|
||||
action='store_true',
|
||||
default=False,
|
||||
dest='webserver',
|
||||
help='run Glances in web server mode (bottle needed)',
|
||||
help='run Glances in web server mode (FastAPI, Uvicorn, Jinja2 and OrJsonLib needed)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--cached-time',
|
||||
|
@ -1,668 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""RestFull API interface class."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from io import open
|
||||
import webbrowser
|
||||
import zlib
|
||||
import socket
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from glances.globals import b, json_dumps
|
||||
from glances.timer import Timer
|
||||
from glances.logger import logger
|
||||
|
||||
try:
|
||||
from bottle import Bottle, static_file, abort, response, request, auth_basic, template, TEMPLATE_PATH
|
||||
except ImportError:
|
||||
logger.critical('Bottle module not found. Glances cannot start in web server mode.')
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def compress(func):
|
||||
"""Compress result with deflate algorithm if the client ask for it."""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
"""Wrapper that take one function and return the compressed result."""
|
||||
ret = func(*args, **kwargs)
|
||||
logger.debug(
|
||||
'Receive {} {} request with header: {}'.format(
|
||||
request.method,
|
||||
request.url,
|
||||
['{}: {}'.format(h, request.headers.get(h)) for h in request.headers.keys()],
|
||||
)
|
||||
)
|
||||
if 'deflate' in request.headers.get('Accept-Encoding', ''):
|
||||
response.headers['Content-Encoding'] = 'deflate'
|
||||
ret = deflate_compress(ret)
|
||||
else:
|
||||
response.headers['Content-Encoding'] = 'identity'
|
||||
return ret
|
||||
|
||||
def deflate_compress(data, compress_level=6):
|
||||
"""Compress given data using the DEFLATE algorithm"""
|
||||
# Init compression
|
||||
zobj = zlib.compressobj(
|
||||
compress_level, zlib.DEFLATED, zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, zlib.Z_DEFAULT_STRATEGY
|
||||
)
|
||||
|
||||
# Return compressed object
|
||||
return zobj.compress(b(data)) + zobj.flush()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class GlancesBottle(object):
|
||||
"""This class manages the Bottle Web server."""
|
||||
|
||||
API_VERSION = '3'
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
# Init config
|
||||
self.config = config
|
||||
|
||||
# Init args
|
||||
self.args = args
|
||||
|
||||
# Init stats
|
||||
# Will be updated within Bottle route
|
||||
self.stats = None
|
||||
|
||||
# cached_time is the minimum time interval between stats updates
|
||||
# i.e. HTTP/RESTful calls will not retrieve updated info until the time
|
||||
# since last update is passed (will retrieve old cached info instead)
|
||||
self.timer = Timer(0)
|
||||
|
||||
# Load configuration file
|
||||
self.load_config(config)
|
||||
|
||||
# Set the bind URL (only used for log information purpose)
|
||||
self.bind_url = urljoin('http://{}:{}/'.format(self.args.bind_address, self.args.port), self.url_prefix)
|
||||
|
||||
# Init Bottle
|
||||
self._app = Bottle()
|
||||
# Enable CORS (issue #479)
|
||||
self._app.install(EnableCors())
|
||||
# Password
|
||||
if args.password != '':
|
||||
self._app.install(auth_basic(self.check_auth))
|
||||
# Define routes
|
||||
self._route()
|
||||
|
||||
# Path where the statics files are stored
|
||||
self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public')
|
||||
|
||||
# Paths for templates
|
||||
TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates'))
|
||||
|
||||
def load_config(self, config):
|
||||
"""Load the outputs section of the configuration file."""
|
||||
# Limit the number of processes to display in the WebUI
|
||||
self.url_prefix = '/'
|
||||
if config is not None and config.has_section('outputs'):
|
||||
n = config.get_value('outputs', 'max_processes_display', default=None)
|
||||
logger.debug('Number of processes to display in the WebUI: {}'.format(n))
|
||||
self.url_prefix = config.get_value('outputs', 'url_prefix', default='/')
|
||||
logger.debug('URL prefix: {}'.format(self.url_prefix))
|
||||
|
||||
def __update__(self):
|
||||
# Never update more than 1 time per cached_time
|
||||
if self.timer.finished():
|
||||
self.stats.update()
|
||||
self.timer = Timer(self.args.cached_time)
|
||||
|
||||
def app(self):
|
||||
return self._app()
|
||||
|
||||
def check_auth(self, username, password):
|
||||
"""Check if a username/password combination is valid."""
|
||||
if username == self.args.username:
|
||||
from glances.password import GlancesPassword
|
||||
|
||||
pwd = GlancesPassword(username=username, config=self.config)
|
||||
return pwd.check_password(self.args.password, pwd.get_hash(password))
|
||||
else:
|
||||
return False
|
||||
|
||||
def _route(self):
|
||||
"""Define route."""
|
||||
# REST API
|
||||
self._app.route('/api/%s/status' % self.API_VERSION, method="GET", callback=self._api_status)
|
||||
self._app.route('/api/%s/config' % self.API_VERSION, method="GET", callback=self._api_config)
|
||||
self._app.route('/api/%s/config/<item>' % self.API_VERSION, method="GET", callback=self._api_config_item)
|
||||
self._app.route('/api/%s/args' % self.API_VERSION, method="GET", callback=self._api_args)
|
||||
self._app.route('/api/%s/args/<item>' % self.API_VERSION, method="GET", callback=self._api_args_item)
|
||||
self._app.route('/api/%s/help' % self.API_VERSION, method="GET", callback=self._api_help)
|
||||
self._app.route('/api/%s/pluginslist' % self.API_VERSION, method="GET", callback=self._api_plugins)
|
||||
self._app.route('/api/%s/all' % self.API_VERSION, method="GET", callback=self._api_all)
|
||||
self._app.route('/api/%s/all/limits' % self.API_VERSION, method="GET", callback=self._api_all_limits)
|
||||
self._app.route('/api/%s/all/views' % self.API_VERSION, method="GET", callback=self._api_all_views)
|
||||
self._app.route('/api/%s/<plugin>' % self.API_VERSION, method="GET", callback=self._api)
|
||||
self._app.route('/api/%s/<plugin>/history' % self.API_VERSION, method="GET", callback=self._api_history)
|
||||
self._app.route(
|
||||
'/api/%s/<plugin>/history/<nb:int>' % self.API_VERSION, method="GET", callback=self._api_history
|
||||
)
|
||||
self._app.route('/api/%s/<plugin>/top/<nb:int>' % self.API_VERSION, method="GET", callback=self._api_top)
|
||||
self._app.route('/api/%s/<plugin>/limits' % self.API_VERSION, method="GET", callback=self._api_limits)
|
||||
self._app.route('/api/%s/<plugin>/views' % self.API_VERSION, method="GET", callback=self._api_views)
|
||||
self._app.route('/api/%s/<plugin>/<item>' % self.API_VERSION, method="GET", callback=self._api_item)
|
||||
self._app.route(
|
||||
'/api/%s/<plugin>/<item>/history' % self.API_VERSION, method="GET", callback=self._api_item_history
|
||||
)
|
||||
self._app.route(
|
||||
'/api/%s/<plugin>/<item>/history/<nb:int>' % self.API_VERSION, method="GET", callback=self._api_item_history
|
||||
)
|
||||
self._app.route('/api/%s/<plugin>/<item>/<value>' % self.API_VERSION, method="GET", callback=self._api_value)
|
||||
self._app.route(
|
||||
'/api/%s/<plugin>/<item>/<value:path>' % self.API_VERSION, method="GET", callback=self._api_value
|
||||
)
|
||||
bindmsg = 'Glances RESTful API Server started on {}api/{}'.format(self.bind_url, self.API_VERSION)
|
||||
logger.info(bindmsg)
|
||||
|
||||
# WEB UI
|
||||
if not self.args.disable_webui:
|
||||
self._app.route('/', method="GET", callback=self._index)
|
||||
self._app.route('/<refresh_time:int>', method=["GET"], callback=self._index)
|
||||
self._app.route('/<filepath:path>', method="GET", callback=self._resource)
|
||||
bindmsg = 'Glances Web User Interface started on {}'.format(self.bind_url)
|
||||
else:
|
||||
bindmsg = 'The WebUI is disable (--disable-webui)'
|
||||
|
||||
logger.info(bindmsg)
|
||||
print(bindmsg)
|
||||
|
||||
def start(self, stats):
|
||||
"""Start the bottle."""
|
||||
# Init stats
|
||||
self.stats = stats
|
||||
|
||||
# Init plugin list
|
||||
self.plugins_list = self.stats.getPluginsList()
|
||||
|
||||
# Bind the Bottle TCP address/port
|
||||
if self.args.open_web_browser:
|
||||
# Implementation of the issue #946
|
||||
# Try to open the Glances Web UI in the default Web browser if:
|
||||
# 1) --open-web-browser option is used
|
||||
# 2) Glances standalone mode is running on Windows OS
|
||||
webbrowser.open(self.bind_url, new=2, autoraise=1)
|
||||
|
||||
# Run the Web application
|
||||
if self.url_prefix != '/':
|
||||
# Create an outer Bottle class instance to manage url_prefix
|
||||
self.main_app = Bottle()
|
||||
self.main_app.mount(self.url_prefix, self._app)
|
||||
try:
|
||||
self.main_app.run(host=self.args.bind_address, port=self.args.port, quiet=not self.args.debug)
|
||||
except socket.error as e:
|
||||
logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
|
||||
else:
|
||||
try:
|
||||
self._app.run(host=self.args.bind_address, port=self.args.port, quiet=not self.args.debug)
|
||||
except socket.error as e:
|
||||
logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
|
||||
|
||||
def end(self):
|
||||
"""End the bottle."""
|
||||
logger.info("Close the Web server")
|
||||
self._app.close()
|
||||
if self.url_prefix != '/':
|
||||
self.main_app.close()
|
||||
|
||||
def _index(self, refresh_time=None):
|
||||
"""Bottle callback for index.html (/) file."""
|
||||
|
||||
if refresh_time is None or refresh_time < 1:
|
||||
refresh_time = int(self.args.time)
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
# Display
|
||||
return template("index.html", refresh_time=refresh_time)
|
||||
|
||||
def _resource(self, filepath):
|
||||
"""Bottle callback for resources files."""
|
||||
# Return the static file
|
||||
return static_file(filepath, root=self.STATIC_PATH)
|
||||
|
||||
@compress
|
||||
def _api_status(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return a 200 status code.
|
||||
This entry point should be used to check the API health.
|
||||
|
||||
See related issue: Web server health check endpoint #1988
|
||||
"""
|
||||
response.status = 200
|
||||
|
||||
return "Active"
|
||||
|
||||
@compress
|
||||
def _api_help(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the help data or 404 error.
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
# Update the stat
|
||||
view_data = self.stats.get_plugin("help").get_view_data()
|
||||
try:
|
||||
plist = json_dumps(view_data)
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get help view data (%s)" % str(e))
|
||||
return plist
|
||||
|
||||
@compress
|
||||
def _api_plugins(self):
|
||||
"""Glances API RESTFul implementation.
|
||||
|
||||
@api {get} /api/%s/pluginslist Get plugins list
|
||||
@apiVersion 2.0
|
||||
@apiName pluginslist
|
||||
@apiGroup plugin
|
||||
|
||||
@apiSuccess {String[]} Plugins list.
|
||||
|
||||
@apiSuccessExample Success-Response:
|
||||
HTTP/1.1 200 OK
|
||||
[
|
||||
"load",
|
||||
"help",
|
||||
"ip",
|
||||
"memswap",
|
||||
"processlist",
|
||||
...
|
||||
]
|
||||
|
||||
@apiError Cannot get plugin list.
|
||||
|
||||
@apiErrorExample Error-Response:
|
||||
HTTP/1.1 404 Not Found
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
plist = json_dumps(self.plugins_list)
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get plugin list (%s)" % str(e))
|
||||
return plist
|
||||
|
||||
@compress
|
||||
def _api_all(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of all the plugins
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if self.args.debug:
|
||||
fname = os.path.join(tempfile.gettempdir(), 'glances-debug.json')
|
||||
try:
|
||||
with open(fname) as f:
|
||||
return f.read()
|
||||
except IOError:
|
||||
logger.debug("Debug file (%s) not found" % fname)
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the JSON value of the stat ID
|
||||
statval = json_dumps(self.stats.getAllAsDict())
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get stats (%s)" % str(e))
|
||||
|
||||
return statval
|
||||
|
||||
@compress
|
||||
def _api_all_limits(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of all the plugins limits
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
try:
|
||||
# Get the JSON value of the stat limits
|
||||
limits = json_dumps(self.stats.getAllLimitsAsDict())
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get limits (%s)" % (str(e)))
|
||||
return limits
|
||||
|
||||
@compress
|
||||
def _api_all_views(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of all the plugins views
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
try:
|
||||
# Get the JSON value of the stat view
|
||||
limits = json_dumps(self.stats.getAllViewsAsDict())
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get views (%s)" % (str(e)))
|
||||
return limits
|
||||
|
||||
@compress
|
||||
def _api(self, plugin):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of a given plugin
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the JSON value of the stat ID
|
||||
statval = self.stats.get_plugin(plugin).get_stats()
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get plugin %s (%s)" % (plugin, str(e)))
|
||||
|
||||
return statval
|
||||
|
||||
@compress
|
||||
def _api_top(self, plugin, nb=0):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of a given plugin limited to the top nb items.
|
||||
It is used to reduce the payload of the HTTP response (example: processlist).
|
||||
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the value of the stat ID
|
||||
statval = self.stats.get_plugin(plugin).get_export()
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get plugin %s (%s)" % (plugin, str(e)))
|
||||
|
||||
if isinstance(statval, list):
|
||||
return json_dumps(statval[:nb])
|
||||
else:
|
||||
return json_dumps(statval)
|
||||
|
||||
@compress
|
||||
def _api_history(self, plugin, nb=0):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of a given plugin history
|
||||
Limit to the last nb items (all if nb=0)
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the JSON value of the stat ID
|
||||
statval = self.stats.get_plugin(plugin).get_stats_history(nb=int(nb))
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get plugin history %s (%s)" % (plugin, str(e)))
|
||||
return statval
|
||||
|
||||
@compress
|
||||
def _api_limits(self, plugin):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON limits of a given plugin
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
# self.__update__()
|
||||
|
||||
try:
|
||||
# Get the JSON value of the stat limits
|
||||
ret = self.stats.get_plugin(plugin).limits
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get limits for plugin %s (%s)" % (plugin, str(e)))
|
||||
return ret
|
||||
|
||||
@compress
|
||||
def _api_views(self, plugin):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON views of a given plugin
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
# self.__update__()
|
||||
|
||||
try:
|
||||
# Get the JSON value of the stat views
|
||||
ret = self.stats.get_plugin(plugin).get_views()
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get views for plugin %s (%s)" % (plugin, str(e)))
|
||||
return ret
|
||||
|
||||
# No compression see issue #1228
|
||||
# @compress
|
||||
def _api_itemvalue(self, plugin, item, value=None, history=False, nb=0):
|
||||
"""Father method for _api_item and _api_value."""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if plugin not in self.plugins_list:
|
||||
abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
if value is None:
|
||||
if history:
|
||||
ret = self.stats.get_plugin(plugin).get_stats_history(item, nb=int(nb))
|
||||
else:
|
||||
ret = self.stats.get_plugin(plugin).get_stats_item(item)
|
||||
|
||||
if ret is None:
|
||||
abort(404, "Cannot get item %s%s in plugin %s" % (item, 'history ' if history else '', plugin))
|
||||
else:
|
||||
if history:
|
||||
# Not available
|
||||
ret = None
|
||||
else:
|
||||
ret = self.stats.get_plugin(plugin).get_stats_value(item, value)
|
||||
|
||||
if ret is None:
|
||||
abort(
|
||||
404, "Cannot get item %s(%s=%s) in plugin %s" % ('history ' if history else '', item, value, plugin)
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
@compress
|
||||
def _api_item(self, plugin, item):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the couple plugin/item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
|
||||
"""
|
||||
return self._api_itemvalue(plugin, item)
|
||||
|
||||
@compress
|
||||
def _api_item_history(self, plugin, item, nb=0):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the couple plugin/history of item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
|
||||
"""
|
||||
return self._api_itemvalue(plugin, item, history=True, nb=int(nb))
|
||||
|
||||
@compress
|
||||
def _api_value(self, plugin, item, value):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the process stats (dict) for the given item=value
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
return self._api_itemvalue(plugin, item, value)
|
||||
|
||||
@compress
|
||||
def _api_config(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances configuration file
|
||||
HTTP/200 if OK
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
try:
|
||||
# Get the JSON value of the config' dict
|
||||
args_json = json_dumps(self.config.as_dict())
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get config (%s)" % str(e))
|
||||
return args_json
|
||||
|
||||
@compress
|
||||
def _api_config_item(self, item):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances configuration item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if item is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
config_dict = self.config.as_dict()
|
||||
if item not in config_dict:
|
||||
abort(400, "Unknown configuration item %s" % item)
|
||||
|
||||
try:
|
||||
# Get the JSON value of the config' dict
|
||||
args_json = json_dumps(config_dict[item])
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get config item (%s)" % str(e))
|
||||
return args_json
|
||||
|
||||
@compress
|
||||
def _api_args(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances command line arguments
|
||||
HTTP/200 if OK
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
try:
|
||||
# Get the JSON value of the args' dict
|
||||
# Use vars to convert namespace to dict
|
||||
# Source: https://docs.python.org/%s/library/functions.html#vars
|
||||
args_json = json_dumps(vars(self.args))
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get args (%s)" % str(e))
|
||||
return args_json
|
||||
|
||||
@compress
|
||||
def _api_args_item(self, item):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances command line arguments item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if item is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
response.content_type = 'application/json; charset=utf-8'
|
||||
|
||||
if item not in self.args:
|
||||
abort(400, "Unknown argument item %s" % item)
|
||||
|
||||
try:
|
||||
# Get the JSON value of the args' dict
|
||||
# Use vars to convert namespace to dict
|
||||
# Source: https://docs.python.org/%s/library/functions.html#vars
|
||||
args_json = json_dumps(vars(self.args)[item])
|
||||
except Exception as e:
|
||||
abort(404, "Cannot get args item (%s)" % str(e))
|
||||
return args_json
|
||||
|
||||
|
||||
class EnableCors(object):
|
||||
name = 'enable_cors'
|
||||
api = 2
|
||||
|
||||
def apply(self, fn, context):
|
||||
def _enable_cors(*args, **kwargs):
|
||||
# set CORS headers
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
|
||||
response.headers[
|
||||
'Access-Control-Allow-Headers'
|
||||
] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
|
||||
|
||||
if request.method != 'OPTIONS':
|
||||
# actual request; reply with the actual response
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return _enable_cors
|
754
glances/outputs/glances_restful_api.py
Normal file
754
glances/outputs/glances_restful_api.py
Normal file
@ -0,0 +1,754 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""RestFull API interface class."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from io import open
|
||||
import webbrowser
|
||||
import socket
|
||||
from urllib.parse import urljoin
|
||||
# Replace typing_extensions by typing when Python 3.8 support will be dropped
|
||||
# from typing import Annotated
|
||||
from typing_extensions import Annotated
|
||||
|
||||
from glances import __version__
|
||||
from glances.password import GlancesPassword
|
||||
from glances.timer import Timer
|
||||
from glances.logger import logger
|
||||
|
||||
# FastAPI import
|
||||
try:
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Request
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import HTMLResponse, ORJSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
except ImportError:
|
||||
logger.critical('FastAPI import error. Glances cannot start in web server mode.')
|
||||
sys.exit(2)
|
||||
|
||||
try:
|
||||
import uvicorn
|
||||
except ImportError:
|
||||
logger.critical('Uvicorn import error. Glances cannot start in web server mode.')
|
||||
sys.exit(2)
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
class GlancesRestfulApi(object):
|
||||
"""This class manages the Restful API server."""
|
||||
|
||||
API_VERSION = '4'
|
||||
|
||||
def __init__(self, config=None, args=None):
|
||||
# Init config
|
||||
self.config = config
|
||||
|
||||
# Init args
|
||||
self.args = args
|
||||
|
||||
# Init stats
|
||||
# Will be updated within Bottle route
|
||||
self.stats = None
|
||||
|
||||
# cached_time is the minimum time interval between stats updates
|
||||
# i.e. HTTP/RESTful calls will not retrieve updated info until the time
|
||||
# since last update is passed (will retrieve old cached info instead)
|
||||
self.timer = Timer(0)
|
||||
|
||||
# Load configuration file
|
||||
self.load_config(config)
|
||||
|
||||
# Set the bind URL
|
||||
self.bind_url = urljoin('http://{}:{}/'.format(self.args.bind_address,
|
||||
self.args.port),
|
||||
self.url_prefix)
|
||||
|
||||
# FastAPI Init
|
||||
if self.args.password:
|
||||
self._app = FastAPI(dependencies=[Depends(self.authentication)])
|
||||
self._password = GlancesPassword(username=args.username,
|
||||
config=config)
|
||||
|
||||
else:
|
||||
self._app = FastAPI()
|
||||
self._password = None
|
||||
|
||||
# Change the default root path
|
||||
if self.url_prefix != '/':
|
||||
self._app.include_router(APIRouter(prefix=self.url_prefix))
|
||||
|
||||
# Set path for WebUI
|
||||
self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public')
|
||||
self.TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates')
|
||||
self._templates = Jinja2Templates(directory=self.TEMPLATE_PATH)
|
||||
|
||||
# FastAPI Enable CORS
|
||||
# https://fastapi.tiangolo.com/tutorial/cors/
|
||||
self._app.add_middleware(
|
||||
CORSMiddleware,
|
||||
# allow_origins=["*"],
|
||||
allow_origins=[
|
||||
self.bind_url
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# FastAPI Enable GZIP compression
|
||||
# https://fastapi.tiangolo.com/advanced/middleware/
|
||||
self._app.add_middleware(GZipMiddleware,
|
||||
minimum_size=1000)
|
||||
|
||||
# FastAPI Define routes
|
||||
self._app.include_router(self._router())
|
||||
|
||||
def load_config(self, config):
|
||||
"""Load the outputs section of the configuration file."""
|
||||
# Limit the number of processes to display in the WebUI
|
||||
self.url_prefix = '/'
|
||||
if config is not None and config.has_section('outputs'):
|
||||
n = config.get_value('outputs', 'max_processes_display', default=None)
|
||||
logger.debug('Number of processes to display in the WebUI: {}'.format(n))
|
||||
self.url_prefix = config.get_value('outputs', 'url_prefix', default='/')
|
||||
logger.debug('URL prefix: {}'.format(self.url_prefix))
|
||||
|
||||
def __update__(self):
|
||||
# Never update more than 1 time per cached_time
|
||||
if self.timer.finished():
|
||||
self.stats.update()
|
||||
self.timer = Timer(self.args.cached_time)
|
||||
|
||||
def app(self):
|
||||
return self._app()
|
||||
|
||||
def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
|
||||
"""Check if a username/password combination is valid."""
|
||||
if creds.username == self.args.username:
|
||||
# check_password and get_hash are (lru) cached to optimize the requests
|
||||
if self._password.check_password(self.args.password,
|
||||
self._password.get_hash(creds.password)):
|
||||
return creds.username
|
||||
|
||||
# If the username/password combination is invalid, return an HTTP 401
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
|
||||
def _router(self):
|
||||
"""Define a custom router for Glances path."""
|
||||
router = APIRouter()
|
||||
|
||||
# REST API
|
||||
router.add_api_route('/api/%s/status' % self.API_VERSION,
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_status)
|
||||
|
||||
router.add_api_route('/api/%s/config' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_config)
|
||||
router.add_api_route('/api/%s/config/{section}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_config_section)
|
||||
router.add_api_route('/api/%s/config/{section}/{item}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_config_section_item)
|
||||
|
||||
router.add_api_route('/api/%s/args' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_args)
|
||||
router.add_api_route('/api/%s/args/{item}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_args_item)
|
||||
|
||||
router.add_api_route('/api/%s/pluginslist' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_plugins)
|
||||
router.add_api_route('/api/%s/all' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_all)
|
||||
router.add_api_route('/api/%s/all/limits' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_all_limits)
|
||||
router.add_api_route('/api/%s/all/views' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_all_views)
|
||||
|
||||
router.add_api_route('/api/%s/help' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_help)
|
||||
router.add_api_route('/api/%s/{plugin}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api)
|
||||
router.add_api_route('/api/%s/{plugin}/history' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_history)
|
||||
router.add_api_route('/api/%s/{plugin}/history/{nb}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_history)
|
||||
router.add_api_route('/api/%s/{plugin}/top/{nb}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_top)
|
||||
router.add_api_route('/api/%s/{plugin}/limits' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_limits)
|
||||
router.add_api_route('/api/%s/{plugin}/views' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_views)
|
||||
router.add_api_route('/api/%s/{plugin}/{item}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_item)
|
||||
router.add_api_route('/api/%s/{plugin}/{item}/history' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_item_history)
|
||||
router.add_api_route('/api/%s/{plugin}/{item}/history/{nb}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_item_history)
|
||||
router.add_api_route('/api/%s/{plugin}/{item}/{value}' % self.API_VERSION,
|
||||
response_class=ORJSONResponse,
|
||||
endpoint=self._api_value)
|
||||
|
||||
# Restful API
|
||||
bindmsg = 'Glances RESTful API Server started on {}api/{}'.format(self.bind_url, self.API_VERSION)
|
||||
logger.info(bindmsg)
|
||||
|
||||
# WEB UI
|
||||
if not self.args.disable_webui:
|
||||
# Template for the root index.html file
|
||||
router.add_api_route('/',
|
||||
response_class=HTMLResponse,
|
||||
endpoint=self._index)
|
||||
|
||||
# Statics files
|
||||
self._app.mount("/static",
|
||||
StaticFiles(directory=self.STATIC_PATH),
|
||||
name="static")
|
||||
|
||||
bindmsg = 'Glances Web User Interface started on {}'.format(self.bind_url)
|
||||
else:
|
||||
bindmsg = 'The WebUI is disable (--disable-webui)'
|
||||
|
||||
logger.info(bindmsg)
|
||||
print(bindmsg)
|
||||
|
||||
return router
|
||||
|
||||
def start(self, stats):
|
||||
"""Start the bottle."""
|
||||
# Init stats
|
||||
self.stats = stats
|
||||
|
||||
# Init plugin list
|
||||
self.plugins_list = self.stats.getPluginsList()
|
||||
|
||||
# Bind the Bottle TCP address/port
|
||||
if self.args.open_web_browser:
|
||||
# Implementation of the issue #946
|
||||
# Try to open the Glances Web UI in the default Web browser if:
|
||||
# 1) --open-web-browser option is used
|
||||
# 2) Glances standalone mode is running on Windows OS
|
||||
webbrowser.open(self.bind_url, new=2, autoraise=1)
|
||||
|
||||
# Run the Web application
|
||||
try:
|
||||
uvicorn.run(self._app,
|
||||
host=self.args.bind_address,
|
||||
port=self.args.port,
|
||||
access_log=self.args.debug)
|
||||
except socket.error as e:
|
||||
logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
|
||||
|
||||
def end(self):
|
||||
"""End the Web server"""
|
||||
logger.info("Close the Web server")
|
||||
|
||||
def _index(self, request: Request):
|
||||
"""Return main index.html (/) file.
|
||||
|
||||
Parameters are available through the request object.
|
||||
Example: http://localhost:61208/?refresh=5
|
||||
|
||||
Note: This function is only called the first time the page is loaded.
|
||||
"""
|
||||
refresh_time = request.query_params.get('refresh',
|
||||
default=max(1, int(self.args.time)))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
# Display
|
||||
return self._templates.TemplateResponse("index.html",
|
||||
{
|
||||
"request": request,
|
||||
"refresh_time": refresh_time,
|
||||
})
|
||||
|
||||
def _api_status(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return a 200 status code.
|
||||
This entry point should be used to check the API health.
|
||||
|
||||
See related issue: Web server health check endpoint #1988
|
||||
"""
|
||||
|
||||
return ORJSONResponse({'version': __version__})
|
||||
|
||||
def _api_help(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the help data or 404 error.
|
||||
"""
|
||||
try:
|
||||
plist = self.stats.get_plugin("help").get_view_data()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get help view data (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(plist)
|
||||
|
||||
def _api_plugins(self):
|
||||
"""Glances API RESTFul implementation.
|
||||
|
||||
@api {get} /api/%s/pluginslist Get plugins list
|
||||
@apiVersion 2.0
|
||||
@apiName pluginslist
|
||||
@apiGroup plugin
|
||||
|
||||
@apiSuccess {String[]} Plugins list.
|
||||
|
||||
@apiSuccessExample Success-Response:
|
||||
HTTP/1.1 200 OK
|
||||
[
|
||||
"load",
|
||||
"help",
|
||||
"ip",
|
||||
"memswap",
|
||||
"processlist",
|
||||
...
|
||||
]
|
||||
|
||||
@apiError Cannot get plugin list.
|
||||
|
||||
@apiErrorExample Error-Response:
|
||||
HTTP/1.1 404 Not Found
|
||||
"""
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
plist = self.plugins_list
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get plugin list (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(plist)
|
||||
|
||||
def _api_all(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of all the plugins
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if self.args.debug:
|
||||
fname = os.path.join(tempfile.gettempdir(), 'glances-debug.json')
|
||||
try:
|
||||
with open(fname) as f:
|
||||
return f.read()
|
||||
except IOError:
|
||||
logger.debug("Debug file (%s) not found" % fname)
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat ID
|
||||
statval = self.stats.getAllAsDict()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get stats (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(statval)
|
||||
|
||||
def _api_all_limits(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of all the plugins limits
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
try:
|
||||
# Get the RAW value of the stat limits
|
||||
limits = self.stats.getAllLimitsAsDict()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get limits (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(limits)
|
||||
|
||||
def _api_all_views(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of all the plugins views
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
try:
|
||||
# Get the RAW value of the stat view
|
||||
limits = self.stats.getAllViewsAsDict()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get views (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(limits)
|
||||
|
||||
def _api(self, plugin):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of a given plugin
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat ID
|
||||
statval = self.stats.get_plugin(plugin).get_raw()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get plugin %s (%s)" % (plugin, str(e)))
|
||||
|
||||
return ORJSONResponse(statval)
|
||||
|
||||
def _api_top(self, plugin, nb: int = 0):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of a given plugin limited to the top nb items.
|
||||
It is used to reduce the payload of the HTTP response (example: processlist).
|
||||
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat ID
|
||||
statval = self.stats.get_plugin(plugin).get_export()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get plugin %s (%s)" % (plugin, str(e)))
|
||||
|
||||
if isinstance(statval, list):
|
||||
statval = statval[:nb]
|
||||
|
||||
return ORJSONResponse(statval)
|
||||
|
||||
def _api_history(self, plugin, nb: int = 0):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of a given plugin history
|
||||
Limit to the last nb items (all if nb=0)
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat ID
|
||||
statval = self.stats.get_plugin(plugin).get_raw_history(nb=int(nb))
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get plugin history %s (%s)" % (plugin, str(e)))
|
||||
|
||||
return statval
|
||||
|
||||
def _api_limits(self, plugin):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON limits of a given plugin
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat limits
|
||||
ret = self.stats.get_plugin(plugin).limits
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get limits for plugin %s (%s)" % (plugin, str(e)))
|
||||
|
||||
return ORJSONResponse(ret)
|
||||
|
||||
def _api_views(self, plugin):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON views of a given plugin
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat views
|
||||
ret = self.stats.get_plugin(plugin).get_views()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get views for plugin %s (%s)" % (plugin, str(e)))
|
||||
|
||||
return ORJSONResponse(ret)
|
||||
|
||||
def _api_item(self, plugin, item):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the couple plugin/item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat views
|
||||
ret = self.stats.get_plugin(plugin).get_raw_stats_item(item)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get item %s in plugin %s (%s)" % (item, plugin, str(e)))
|
||||
|
||||
return ORJSONResponse(ret)
|
||||
|
||||
def _api_item_history(self, plugin, item, nb: int = 0):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the couple plugin/history of item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the RAW value of the stat history
|
||||
ret = self.stats.get_plugin(plugin).get_raw_history(item, nb=nb)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get history for plugin %s (%s)" % (plugin, str(e)))
|
||||
|
||||
return ORJSONResponse(ret)
|
||||
|
||||
def _api_value(self, plugin, item, value):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the process stats (dict) for the given item=value
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if plugin is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if plugin not in self.plugins_list:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list))
|
||||
|
||||
# Update the stat
|
||||
self.__update__()
|
||||
|
||||
try:
|
||||
# Get the RAW value
|
||||
ret = self.stats.get_plugin(plugin).get_raw_stats_value(item, value)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get %s = %s for plugin %s (%s)" % (item, value, plugin, str(e)))
|
||||
|
||||
return ORJSONResponse(ret)
|
||||
|
||||
def _api_config(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances configuration file
|
||||
HTTP/200 if OK
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
try:
|
||||
# Get the RAW value of the config' dict
|
||||
args_json = self.config.as_dict()
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get config (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(args_json)
|
||||
|
||||
def _api_config_section(self, section):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances configuration section
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if item is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
config_dict = self.config.as_dict()
|
||||
if section not in config_dict:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown configuration item %s" % section)
|
||||
|
||||
try:
|
||||
# Get the RAW value of the config' dict
|
||||
ret_section = config_dict[section]
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get config section %s (%s)" % (section, str(e)))
|
||||
|
||||
return ORJSONResponse(ret_section)
|
||||
|
||||
def _api_config_section_item(self, section, item):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances configuration section/item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if item is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
config_dict = self.config.as_dict()
|
||||
if section not in config_dict:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown configuration item %s" % section)
|
||||
|
||||
try:
|
||||
# Get the RAW value of the config' dict section
|
||||
ret_section = config_dict[section]
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get config section %s (%s)" % (section, str(e)))
|
||||
|
||||
try:
|
||||
# Get the RAW value of the config' dict item
|
||||
ret_item = ret_section[item]
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get item %s in config section %s (%s)" % (item, section, str(e)))
|
||||
|
||||
return ORJSONResponse(ret_item)
|
||||
|
||||
def _api_args(self):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances command line arguments
|
||||
HTTP/200 if OK
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
try:
|
||||
# Get the RAW value of the args' dict
|
||||
# Use vars to convert namespace to dict
|
||||
# Source: https://docs.python.org/%s/library/functions.html#vars
|
||||
args_json = vars(self.args)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get args (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(args_json)
|
||||
|
||||
def _api_args_item(self, item):
|
||||
"""Glances API RESTful implementation.
|
||||
|
||||
Return the JSON representation of the Glances command line arguments item
|
||||
HTTP/200 if OK
|
||||
HTTP/400 if item is not found
|
||||
HTTP/404 if others error
|
||||
"""
|
||||
if item not in self.args:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Unknown argument item %s" % item)
|
||||
|
||||
try:
|
||||
# Get the RAW value of the args' dict
|
||||
# Use vars to convert namespace to dict
|
||||
# Source: https://docs.python.org/%s/library/functions.html#vars
|
||||
args_json = vars(self.args)[item]
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Cannot get args item (%s)" % str(e))
|
||||
|
||||
return ORJSONResponse(args_json)
|
@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
@ -13,10 +13,12 @@ from pprint import pformat
|
||||
import json
|
||||
import time
|
||||
|
||||
from glances.outputs.glances_restful_api import GlancesRestfulApi
|
||||
from glances.logger import logger
|
||||
from glances.globals import iteritems
|
||||
|
||||
API_URL = "http://localhost:61208/api/3"
|
||||
|
||||
API_URL = "http://localhost:61208/api/{api_version}".format(api_version=GlancesRestfulApi.API_VERSION)
|
||||
|
||||
APIDOC_HEADER = """\
|
||||
.. _api:
|
||||
@ -33,12 +35,14 @@ The Glances Restfull/API server could be ran using the following command line:
|
||||
API URL
|
||||
-------
|
||||
|
||||
The default root API URL is ``http://localhost:61208/api/3``.
|
||||
The default root API URL is ``http://localhost:61208/api/{api_version}``.
|
||||
|
||||
The bind address and port could be changed using the ``--bind`` and ``--port`` command line options.
|
||||
|
||||
It is also possible to define an URL prefix using the ``url_prefix`` option from the [outputs] section
|
||||
of the Glances configuration file. The url_prefix should always end with a slash (``/``).
|
||||
of the Glances configuration file.
|
||||
|
||||
Note: The url_prefix should always end with a slash (``/``).
|
||||
|
||||
For example:
|
||||
|
||||
@ -46,10 +50,21 @@ For example:
|
||||
[outputs]
|
||||
url_prefix = /glances/
|
||||
|
||||
will change the root API URL to ``http://localhost:61208/glances/api/3`` and the Web UI URL to
|
||||
will change the root API URL to ``http://localhost:61208/glances/api/{api_version}`` and the Web UI URL to
|
||||
``http://localhost:61208/glances/``
|
||||
|
||||
"""
|
||||
API documentation
|
||||
-----------------
|
||||
|
||||
The API documentation is available at the following URL: ``http://localhost:61208/docs#/``.
|
||||
|
||||
WebUI refresh
|
||||
-------------
|
||||
|
||||
It is possible to change the Web UI refresh rate (default is 2 seconds) using the following option in the URL:
|
||||
``http://localhost:61208/glances/?refresh=5``
|
||||
|
||||
""".format(api_version=GlancesRestfulApi.API_VERSION)
|
||||
|
||||
|
||||
def indent_stat(stat, indent=' '):
|
||||
@ -67,7 +82,7 @@ def print_api_status():
|
||||
print('-' * len(sub_title))
|
||||
print('')
|
||||
print('This entry point should be used to check the API status.')
|
||||
print('It will return nothing but a 200 return code if everything is OK.')
|
||||
print('It will the Glances version and a 200 return code if everything is OK.')
|
||||
print('')
|
||||
print('Get the Rest API status::')
|
||||
print('')
|
||||
|
@ -65,7 +65,7 @@ static
|
||||
|
|
||||
|--- public # path where builds are put
|
||||
|
|
||||
|--- templates (bottle)
|
||||
|--- templates
|
||||
```
|
||||
|
||||
## Data
|
||||
|
@ -19,6 +19,15 @@ body {
|
||||
display: table-cell;
|
||||
text-align: right;
|
||||
}
|
||||
.width-50 {
|
||||
width: 50px;
|
||||
}
|
||||
.width-75 {
|
||||
width: 75px;
|
||||
}
|
||||
.width-100 {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.plugin {
|
||||
margin-bottom: 20px;
|
||||
@ -116,6 +125,7 @@ body {
|
||||
}
|
||||
|
||||
/* Plugins */
|
||||
|
||||
#processlist-plugin .table-cell {
|
||||
padding: 0px 5px 0px 5px;
|
||||
white-space: nowrap;
|
||||
|
@ -266,7 +266,7 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
fetch('api/3/help', { method: 'GET' })
|
||||
fetch('api/4/help', { method: 'GET' })
|
||||
.then((response) => response.json())
|
||||
.then((response) => (this.help = response));
|
||||
}
|
||||
|
@ -64,13 +64,13 @@ export default {
|
||||
}
|
||||
function getColumnLabel(value) {
|
||||
const labels = {
|
||||
io_counters: 'disk IO',
|
||||
cpu_percent: 'CPU consumption',
|
||||
memory_percent: 'memory consumption',
|
||||
cpu_times: 'process time',
|
||||
username: 'user name',
|
||||
name: 'process name',
|
||||
timemillis: 'process time',
|
||||
cpu_times: 'process time',
|
||||
io_counters: 'disk IO',
|
||||
name: 'process name',
|
||||
None: 'None'
|
||||
};
|
||||
return labels[value] || value;
|
||||
|
@ -3,112 +3,88 @@
|
||||
<section id="processlist-plugin" class="plugin">
|
||||
<div class="table">
|
||||
<div class="table-row">
|
||||
<div
|
||||
class="table-cell"
|
||||
:class="['sortable', sorter.column === 'cpu_percent' && 'sort']"
|
||||
@click="$emit('update:sorter', 'cpu_percent')"
|
||||
>
|
||||
<div class="table-cell width-50" :class="['sortable', sorter.column === 'cpu_percent' && 'sort']"
|
||||
@click="$emit('update:sorter', 'cpu_percent')">
|
||||
CPU%
|
||||
</div>
|
||||
<div
|
||||
class="table-cell"
|
||||
:class="['sortable', sorter.column === 'memory_percent' && 'sort']"
|
||||
@click="$emit('update:sorter', 'memory_percent')"
|
||||
>
|
||||
<div class="table-cell width-50" :class="['sortable', sorter.column === 'memory_percent' && 'sort']"
|
||||
@click="$emit('update:sorter', 'memory_percent')">
|
||||
MEM%
|
||||
</div>
|
||||
<div class="table-cell hidden-xs hidden-sm">VIRT</div>
|
||||
<div class="table-cell hidden-xs hidden-sm">RES</div>
|
||||
<div class="table-cell">PID</div>
|
||||
<div
|
||||
class="table-cell text-left"
|
||||
:class="['sortable', sorter.column === 'username' && 'sort']"
|
||||
@click="$emit('update:sorter', 'username')"
|
||||
>
|
||||
<div class="table-cell width-75 hidden-xs hidden-sm">VIRT</div>
|
||||
<div class="table-cell width-75 hidden-xs hidden-sm">RES</div>
|
||||
<div class="table-cell width-75">PID</div>
|
||||
<div class="table-cell width-100 text-left" :class="['sortable', sorter.column === 'username' && 'sort']"
|
||||
@click="$emit('update:sorter', 'username')">
|
||||
USER
|
||||
</div>
|
||||
<div
|
||||
class="table-cell hidden-xs hidden-sm"
|
||||
<div class="table-cell width-100 hidden-xs hidden-sm"
|
||||
:class="['sortable', sorter.column === 'timemillis' && 'sort']"
|
||||
@click="$emit('update:sorter', 'timemillis')"
|
||||
>
|
||||
@click="$emit('update:sorter', 'timemillis')">
|
||||
TIME+
|
||||
</div>
|
||||
<div
|
||||
class="table-cell text-left hidden-xs hidden-sm"
|
||||
<div class="table-cell width-75 text-left hidden-xs hidden-sm"
|
||||
:class="['sortable', sorter.column === 'num_threads' && 'sort']"
|
||||
@click="$emit('update:sorter', 'num_threads')"
|
||||
>
|
||||
@click="$emit('update:sorter', 'num_threads')">
|
||||
THR
|
||||
</div>
|
||||
<div class="table-cell">NI</div>
|
||||
<div class="table-cell">S</div>
|
||||
<div
|
||||
v-show="ioReadWritePresent"
|
||||
class="table-cell hidden-xs hidden-sm"
|
||||
<div class="table-cell width-50">NI</div>
|
||||
<div class="table-cell width-50">S</div>
|
||||
<div v-show="ioReadWritePresent" class="table-cell width-75 hidden-xs hidden-sm"
|
||||
:class="['sortable', sorter.column === 'io_counters' && 'sort']"
|
||||
@click="$emit('update:sorter', 'io_counters')"
|
||||
>
|
||||
@click="$emit('update:sorter', 'io_counters')">
|
||||
IOR/s
|
||||
</div>
|
||||
<div
|
||||
v-show="ioReadWritePresent"
|
||||
class="table-cell text-left hidden-xs hidden-sm"
|
||||
<div v-show="ioReadWritePresent" class="table-cell width-75 text-left hidden-xs hidden-sm"
|
||||
:class="['sortable', sorter.column === 'io_counters' && 'sort']"
|
||||
@click="$emit('update:sorter', 'io_counters')"
|
||||
>
|
||||
@click="$emit('update:sorter', 'io_counters')">
|
||||
IOW/s
|
||||
</div>
|
||||
<div
|
||||
class="table-cell text-left"
|
||||
:class="['sortable', sorter.column === 'name' && 'sort']"
|
||||
@click="$emit('update:sorter', 'name')"
|
||||
>
|
||||
<div class="table-cell text-left" :class="['sortable', sorter.column === 'name' && 'sort']"
|
||||
@click="$emit('update:sorter', 'name')">
|
||||
Command
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="table-row"
|
||||
v-for="(process, processId) in processes"
|
||||
:key="processId"
|
||||
>
|
||||
<div class="table-cell" :class="getCpuPercentAlert(process)">
|
||||
<div class="table-row" v-for="(process, processId) in processes" :key="processId">
|
||||
<div class="table-cell width-50" :class="getCpuPercentAlert(process)">
|
||||
{{ process.cpu_percent == -1 ? '?' : $filters.number(process.cpu_percent, 1) }}
|
||||
</div>
|
||||
<div class="table-cell" :class="getMemoryPercentAlert(process)">
|
||||
<div class="table-cell width-50" :class="getMemoryPercentAlert(process)">
|
||||
{{ process.memory_percent == -1 ? '?' : $filters.number(process.memory_percent, 1) }}
|
||||
</div>
|
||||
<div class="table-cell hidden-xs hidden-sm">
|
||||
<div class="table-cell width-75">
|
||||
{{ $filters.bytes(process.memvirt) }}
|
||||
</div>
|
||||
<div class="table-cell hidden-xs hidden-sm">
|
||||
<div class="table-cell width-75">
|
||||
{{ $filters.bytes(process.memres) }}
|
||||
</div>
|
||||
<div class="table-cell">
|
||||
<div class="table-cell width-75">
|
||||
{{ process.pid }}
|
||||
</div>
|
||||
<div class="table-cell text-left">
|
||||
<div class="table-cell width-100 text-left">
|
||||
{{ process.username }}
|
||||
</div>
|
||||
<div class="table-cell hidden-xs hidden-sm" v-if="process.timeplus != '?'">
|
||||
<div class="table-cell width-100 hidden-xs hidden-sm" v-if="process.timeplus != '?'">
|
||||
<span v-show="process.timeplus.hours > 0" class="highlight">{{ process.timeplus.hours }}h</span>
|
||||
{{ $filters.leftPad(process.timeplus.minutes, 2, '0') }}:{{ $filters.leftPad(process.timeplus.seconds, 2, '0') }}
|
||||
<span v-show="process.timeplus.hours <= 0">.{{ $filters.leftPad(process.timeplus.milliseconds, 2, '0') }}</span>
|
||||
{{ $filters.leftPad(process.timeplus.minutes, 2, '0') }}:{{ $filters.leftPad(process.timeplus.seconds,
|
||||
2, '0') }}
|
||||
<span v-show="process.timeplus.hours <= 0">.{{ $filters.leftPad(process.timeplus.milliseconds, 2, '0')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="table-cell hidden-xs hidden-sm" v-if="process.timeplus == '?'">?</div>
|
||||
<div class="table-cell text-left hidden-xs hidden-sm">
|
||||
<div class="table-cell width-75 hidden-xs hidden-sm" v-if="process.timeplus == '?'">?</div>
|
||||
<div class="table-cell width-75 text-left hidden-xs hidden-sm">
|
||||
{{ process.num_threads == -1 ? '?' : process.num_threads }}
|
||||
</div>
|
||||
<div class="table-cell" :class="{ nice: process.isNice }">
|
||||
<div class="table-cell width-50" :class="{ nice: process.isNice }">
|
||||
{{ $filters.exclamation(process.nice) }}
|
||||
</div>
|
||||
<div class="table-cell" :class="{ status: process.status == 'R' }">
|
||||
<div class="table-cell width-50" :class="{ status: process.status == 'R' }">
|
||||
{{ process.status }}
|
||||
</div>
|
||||
<div class="table-cell hidden-xs hidden-sm" v-show="ioReadWritePresent">
|
||||
<div class="table-cell width-75 hidden-xs hidden-sm" v-show="ioReadWritePresent">
|
||||
{{ $filters.bytes(process.io_read) }}
|
||||
</div>
|
||||
<div class="table-cell text-left hidden-xs hidden-sm" v-show="ioReadWritePresent">
|
||||
<div class="table-cell width-75 text-left hidden-xs hidden-sm" v-show="ioReadWritePresent">
|
||||
{{ $filters.bytes(process.io_write) }}
|
||||
</div>
|
||||
<div class="table-cell text-left" v-show="args.process_short_name">
|
||||
@ -159,8 +135,8 @@ export default {
|
||||
process.memvirt = '?';
|
||||
process.memres = '?';
|
||||
if (process.memory_info) {
|
||||
process.memvirt = process.memory_info[1];
|
||||
process.memres = process.memory_info[0];
|
||||
process.memvirt = process.memory_info.vms;
|
||||
process.memres = process.memory_info.rss;
|
||||
}
|
||||
|
||||
process.timeplus = '?';
|
||||
|
@ -2,15 +2,15 @@ import { store } from './store.js';
|
||||
import Favico from 'favico.js';
|
||||
|
||||
// prettier-ignore
|
||||
const fetchAll = () => fetch('api/3/all', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchAll = () => fetch('api/4/all', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchAllViews = () => fetch('api/3/all/views', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchAllViews = () => fetch('api/4/all/views', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchAllLimits = () => fetch('api/3/all/limits', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchAllLimits = () => fetch('api/4/all/limits', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchArgs = () => fetch('api/3/args', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchArgs = () => fetch('api/4/args', { method: 'GET' }).then((response) => response.json());
|
||||
// prettier-ignore
|
||||
const fetchConfig = () => fetch('api/3/config', { method: 'GET' }).then((response) => response.json());
|
||||
const fetchConfig = () => fetch('api/4/config', { method: 'GET' }).then((response) => response.json());
|
||||
|
||||
class GlancesHelperService {
|
||||
limits = {};
|
||||
|
BIN
glances/outputs/static/public/glances.js
vendored
BIN
glances/outputs/static/public/glances.js
vendored
Binary file not shown.
22
glances/outputs/static/templates/index.html
Normal file
22
glances/outputs/static/templates/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Glances</title>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="static/favicon.ico" />
|
||||
<script>
|
||||
window.__GLANCES__ = {
|
||||
'refresh-time': '{{ refresh_time }}'
|
||||
}
|
||||
</script>
|
||||
<script src="static/glances.js" defer></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Glances</title>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<script>
|
||||
window.__GLANCES__ = {
|
||||
'refresh-time': '{{ refresh_time }}'
|
||||
}
|
||||
</script>
|
||||
<script src="glances.js" defer></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
@ -16,7 +16,7 @@ import sys
|
||||
import uuid
|
||||
from io import open
|
||||
|
||||
from glances.globals import b, safe_makedirs
|
||||
from glances.globals import b, safe_makedirs, weak_lru_cache
|
||||
from glances.config import user_config_dir
|
||||
from glances.logger import logger
|
||||
|
||||
@ -42,21 +42,25 @@ class GlancesPassword(object):
|
||||
else:
|
||||
return self.config.get_value('passwords', 'local_password_path', default=user_config_dir())
|
||||
|
||||
@weak_lru_cache(maxsize=32)
|
||||
def get_hash(self, plain_password, salt=''):
|
||||
"""Return the hashed password, salt + pbkdf2_hmac."""
|
||||
return hashlib.pbkdf2_hmac('sha256', plain_password.encode(), salt.encode(), 100000, dklen=128).hex()
|
||||
|
||||
@weak_lru_cache(maxsize=32)
|
||||
def hash_password(self, plain_password):
|
||||
"""Hash password with a salt based on UUID (universally unique identifier)."""
|
||||
salt = uuid.uuid4().hex
|
||||
encrypted_password = self.get_hash(plain_password, salt=salt)
|
||||
return salt + '$' + encrypted_password
|
||||
|
||||
@weak_lru_cache(maxsize=32)
|
||||
def check_password(self, hashed_password, plain_password):
|
||||
"""Encode the plain_password with the salt of the hashed_password.
|
||||
|
||||
Return the comparison with the encrypted_password.
|
||||
"""
|
||||
logger.info("Check password")
|
||||
salt, encrypted_password = hashed_password.split('$')
|
||||
re_encrypted_password = self.get_hash(plain_password, salt=salt)
|
||||
return encrypted_password == re_encrypted_password
|
||||
|
@ -16,7 +16,7 @@ I am your father...
|
||||
import re
|
||||
import copy
|
||||
|
||||
from glances.globals import iterkeys, itervalues, listkeys, mean, nativestr, json_dumps, json_dumps_dictlist
|
||||
from glances.globals import iterkeys, itervalues, listkeys, mean, nativestr, json_dumps, json_dumps_dictlist, dictlist
|
||||
from glances.actions import GlancesActions
|
||||
from glances.history import GlancesHistory
|
||||
from glances.logger import logger
|
||||
@ -395,6 +395,13 @@ class GlancesPluginModel(object):
|
||||
"""Return the stats object in JSON format."""
|
||||
return self.get_stats()
|
||||
|
||||
def get_raw_stats_item(self, item):
|
||||
"""Return the stats object for a specific item in RAW format.
|
||||
|
||||
Stats should be a list of dict (processlist, network...)
|
||||
"""
|
||||
return dictlist(self.stats, item)
|
||||
|
||||
def get_stats_item(self, item):
|
||||
"""Return the stats object for a specific item in JSON format.
|
||||
|
||||
@ -402,8 +409,8 @@ class GlancesPluginModel(object):
|
||||
"""
|
||||
return json_dumps_dictlist(self.stats, item)
|
||||
|
||||
def get_stats_value(self, item, value):
|
||||
"""Return the stats object for a specific item=value in JSON format.
|
||||
def get_raw_stats_value(self, item, value):
|
||||
"""Return the stats object for a specific item=value.
|
||||
|
||||
Stats should be a list of dict (processlist, network...)
|
||||
"""
|
||||
@ -413,11 +420,22 @@ class GlancesPluginModel(object):
|
||||
if not isinstance(value, int) and value.isdigit():
|
||||
value = int(value)
|
||||
try:
|
||||
return json_dumps({value: [i for i in self.stats if i[item] == value]})
|
||||
return {value: [i for i in self.stats if i[item] == value]}
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.error("Cannot get item({})=value({}) ({})".format(item, value, e))
|
||||
return None
|
||||
|
||||
def get_stats_value(self, item, value):
|
||||
"""Return the stats object for a specific item=value in JSON format.
|
||||
|
||||
Stats should be a list of dict (processlist, network...)
|
||||
"""
|
||||
rsv = self.get_raw_stats_value(item, value)
|
||||
if rsv is None:
|
||||
return None
|
||||
else:
|
||||
return json_dumps(rsv)
|
||||
|
||||
def update_views_hidden(self):
|
||||
"""Update the hidden views
|
||||
|
||||
|
@ -153,7 +153,7 @@ class PluginModel(GlancesPluginModel):
|
||||
# No SNMP grab for processes
|
||||
pass
|
||||
|
||||
# Update the stats
|
||||
# Update the stats and transform all namedtuples to dict
|
||||
self.stats = stats
|
||||
|
||||
# Get the max values (dict)
|
||||
@ -220,8 +220,8 @@ class PluginModel(GlancesPluginModel):
|
||||
|
||||
def _get_process_curses_vms(self, p, selected, args):
|
||||
"""Return process VMS curses"""
|
||||
if key_exist_value_not_none_not_v('memory_info', p, '', 1):
|
||||
msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info'][1], low_precision=False))
|
||||
if key_exist_value_not_none_not_v('memory_info', p, '', 1) and 'vms' in p['memory_info']:
|
||||
msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info']['vms'], low_precision=False))
|
||||
ret = self.curse_add_line(msg, optional=True)
|
||||
else:
|
||||
msg = self.layout_header['virt'].format('?')
|
||||
@ -230,8 +230,8 @@ class PluginModel(GlancesPluginModel):
|
||||
|
||||
def _get_process_curses_rss(self, p, selected, args):
|
||||
"""Return process RSS curses"""
|
||||
if key_exist_value_not_none_not_v('memory_info', p, '', 0):
|
||||
msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info'][0], low_precision=False))
|
||||
if key_exist_value_not_none_not_v('memory_info', p, '', 0) and 'rss' in p['memory_info']:
|
||||
msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info']['rss'], low_precision=False))
|
||||
ret = self.curse_add_line(msg, optional=True)
|
||||
else:
|
||||
msg = self.layout_header['res'].format('?')
|
||||
@ -254,7 +254,7 @@ class PluginModel(GlancesPluginModel):
|
||||
"""Return process time curses"""
|
||||
try:
|
||||
# Sum user and system time
|
||||
user_system_time = p['cpu_times'][0] + p['cpu_times'][1]
|
||||
user_system_time = p['cpu_times']['user'] + p['cpu_times']['system']
|
||||
except (OverflowError, TypeError):
|
||||
# Catch OverflowError on some Amazon EC2 server
|
||||
# See https://github.com/nicolargo/glances/issues/87
|
||||
@ -470,7 +470,7 @@ class PluginModel(GlancesPluginModel):
|
||||
# Process list
|
||||
# Loop over processes (sorted by the sort key previously compute)
|
||||
# This is a Glances bottleneck (see flame graph),
|
||||
# get_process_curses_data should be optimzed
|
||||
# TODO: get_process_curses_data should be optimzed
|
||||
for position, process in enumerate(processes_list_sorted):
|
||||
ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args))
|
||||
|
||||
@ -493,16 +493,17 @@ class PluginModel(GlancesPluginModel):
|
||||
|
||||
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),
|
||||
'memory_info': {'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),
|
||||
'gids': {'real': 1000, 'effective': 1000, 'saved': 1000},
|
||||
'cpu_times': {'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,
|
||||
@ -579,10 +580,11 @@ class PluginModel(GlancesPluginModel):
|
||||
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():
|
||||
for k, v in p['memory_info'].items():
|
||||
ret.append(
|
||||
self.curse_add_line(
|
||||
self.auto_unit(p['memory_info']._asdict()[k], low_precision=False),
|
||||
self.auto_unit(v,
|
||||
low_precision=False),
|
||||
decoration='INFO',
|
||||
splittable=True,
|
||||
)
|
||||
@ -591,7 +593,10 @@ class PluginModel(GlancesPluginModel):
|
||||
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
|
||||
self.auto_unit(p['memory_swap'],
|
||||
low_precision=False),
|
||||
decoration='INFO',
|
||||
splittable=True
|
||||
)
|
||||
)
|
||||
ret.append(self.curse_add_line(' swap ', splittable=True))
|
||||
@ -690,12 +695,12 @@ class PluginModel(GlancesPluginModel):
|
||||
):
|
||||
# VMS
|
||||
msg = self.layout_stat['virt'].format(
|
||||
self.auto_unit(self.__sum_stats('memory_info', indice=1, mmm=mmm), low_precision=False)
|
||||
self.auto_unit(self.__sum_stats('memory_info', sub_key='vms', mmm=mmm), low_precision=False)
|
||||
)
|
||||
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True))
|
||||
# RSS
|
||||
msg = self.layout_stat['res'].format(
|
||||
self.auto_unit(self.__sum_stats('memory_info', indice=0, mmm=mmm), low_precision=False)
|
||||
self.auto_unit(self.__sum_stats('memory_info', sub_key='rss', mmm=mmm), low_precision=False)
|
||||
)
|
||||
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True))
|
||||
else:
|
||||
@ -725,7 +730,7 @@ class PluginModel(GlancesPluginModel):
|
||||
if 'io_counters' in self.stats[0] and mmm is None:
|
||||
# IO read
|
||||
io_rs = int(
|
||||
(self.__sum_stats('io_counters', 0) - self.__sum_stats('io_counters', indice=2, mmm=mmm))
|
||||
(self.__sum_stats('io_counters', 0) - self.__sum_stats('io_counters', sub_key=2, mmm=mmm))
|
||||
/ self.stats[0]['time_since_update']
|
||||
)
|
||||
if io_rs == 0:
|
||||
@ -735,7 +740,7 @@ class PluginModel(GlancesPluginModel):
|
||||
ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True, additional=True))
|
||||
# IO write
|
||||
io_ws = int(
|
||||
(self.__sum_stats('io_counters', 1) - self.__sum_stats('io_counters', indice=3, mmm=mmm))
|
||||
(self.__sum_stats('io_counters', 1) - self.__sum_stats('io_counters', sub_key=3, mmm=mmm))
|
||||
/ self.stats[0]['time_since_update']
|
||||
)
|
||||
if io_ws == 0:
|
||||
@ -769,10 +774,10 @@ class PluginModel(GlancesPluginModel):
|
||||
self.mmm_min = {}
|
||||
self.mmm_max = {}
|
||||
|
||||
def __sum_stats(self, key, indice=None, mmm=None):
|
||||
def __sum_stats(self, key, sub_key=None, mmm=None):
|
||||
"""Return the sum of the stats value for the given key.
|
||||
|
||||
:param indice: If indice is set, get the p[key][indice]
|
||||
:param sub_key: If sub_key is set, get the p[key][sub_key]
|
||||
:param mmm: display min, max, mean or current (if mmm=None)
|
||||
"""
|
||||
# Compute stats summary
|
||||
@ -784,13 +789,13 @@ class PluginModel(GlancesPluginModel):
|
||||
if p[key] is None:
|
||||
# Correct https://github.com/nicolargo/glances/issues/1105#issuecomment-363553788
|
||||
continue
|
||||
if indice is None:
|
||||
if sub_key is None:
|
||||
ret += p[key]
|
||||
else:
|
||||
ret += p[key][indice]
|
||||
ret += p[key][sub_key]
|
||||
|
||||
# Manage Min/Max/Mean
|
||||
mmm_key = self.__mmm_key(key, indice)
|
||||
mmm_key = self.__mmm_key(key, sub_key)
|
||||
if mmm == 'min':
|
||||
try:
|
||||
if self.mmm_min[mmm_key] > ret:
|
||||
@ -814,10 +819,10 @@ class PluginModel(GlancesPluginModel):
|
||||
|
||||
return ret
|
||||
|
||||
def __mmm_key(self, key, indice):
|
||||
def __mmm_key(self, key, sub_key):
|
||||
ret = key
|
||||
if indice is not None:
|
||||
ret += str(indice)
|
||||
if sub_key is not None:
|
||||
ret += str(sub_key)
|
||||
return ret
|
||||
|
||||
def __sort_stats(self, sorted_by=None):
|
||||
|
@ -7,16 +7,17 @@
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""psutil plugin."""
|
||||
"""psutil plugin.
|
||||
Just a simple plugin to get the Psutil version."""
|
||||
|
||||
from glances import psutil_version_info
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Get the psutil version for client/server purposes.
|
||||
"""Get the Psutil version.
|
||||
|
||||
stats is a tuple
|
||||
stats is a string
|
||||
"""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
@ -40,7 +41,7 @@ class PluginModel(GlancesPluginModel):
|
||||
if self.input_method == 'local':
|
||||
# psutil version only available in local
|
||||
try:
|
||||
self.stats = psutil_version_info
|
||||
self.stats = '.'.join([str(i) for i in psutil_version_info])
|
||||
except NameError:
|
||||
pass
|
||||
else:
|
||||
|
51
glances/plugins/version/__init__.py
Normal file
51
glances/plugins/version/__init__.py
Normal file
@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""version plugin.
|
||||
Just a simple plugin to get the Glances version.
|
||||
"""
|
||||
|
||||
from glances import __version__ as glances_version
|
||||
from glances.plugins.plugin.model import GlancesPluginModel
|
||||
|
||||
|
||||
class PluginModel(GlancesPluginModel):
|
||||
"""Get the Glances versions.
|
||||
|
||||
stats is a string
|
||||
"""
|
||||
|
||||
def __init__(self, args=None, config=None):
|
||||
"""Init the plugin."""
|
||||
super(PluginModel, self).__init__(args=args, config=config)
|
||||
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
"""Reset/init the stats."""
|
||||
self.stats = None
|
||||
|
||||
@GlancesPluginModel._check_decorator
|
||||
@GlancesPluginModel._log_result_decorator
|
||||
def update(self):
|
||||
"""Update the stats."""
|
||||
# Reset stats
|
||||
self.reset()
|
||||
|
||||
# Return psutil version as a tuple
|
||||
if self.input_method == 'local':
|
||||
# psutil version only available in local
|
||||
try:
|
||||
self.stats = glances_version
|
||||
except NameError:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
return self.stats
|
@ -353,9 +353,8 @@ class GlancesProcesses(object):
|
||||
|
||||
def update(self):
|
||||
"""Update the processes stats."""
|
||||
# Reset the stats
|
||||
self.processlist = []
|
||||
self.reset_processcount()
|
||||
# Init new processes stats
|
||||
processlist = []
|
||||
|
||||
# Do not process if disable tag is set
|
||||
if self.disable_tag:
|
||||
@ -392,7 +391,7 @@ class GlancesProcesses(object):
|
||||
# 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)
|
||||
# Filter processes
|
||||
self.processlist = list(
|
||||
processlist = list(
|
||||
filter(
|
||||
lambda p: not (BSD and p.info['name'] == 'idle')
|
||||
and not (WINDOWS and p.info['name'] == 'System Idle Process')
|
||||
@ -402,17 +401,17 @@ class GlancesProcesses(object):
|
||||
)
|
||||
)
|
||||
# Only get the info key
|
||||
self.processlist = [p.info for p in self.processlist]
|
||||
processlist = [p.info for p in processlist]
|
||||
# Sort the processes list by the current sort_key
|
||||
self.processlist = sort_stats(self.processlist, sorted_by=self.sort_key, reverse=True)
|
||||
processlist = sort_stats(processlist, sorted_by=self.sort_key, reverse=True)
|
||||
|
||||
# Update the processcount
|
||||
self.update_processcount(self.processlist)
|
||||
self.update_processcount(processlist)
|
||||
|
||||
# Loop over processes and :
|
||||
# - add extended stats for selected process
|
||||
# - add metadata
|
||||
for position, proc in enumerate(self.processlist):
|
||||
for position, proc in enumerate(processlist):
|
||||
# Extended stats
|
||||
################
|
||||
|
||||
@ -443,7 +442,7 @@ class GlancesProcesses(object):
|
||||
# If io_tag = 0 > Access denied or first time (display "?")
|
||||
# If io_tag = 1 > No access denied (display the IO rate)
|
||||
if 'io_counters' in proc and proc['io_counters'] is not None:
|
||||
io_new = [proc['io_counters'].read_bytes, proc['io_counters'].write_bytes]
|
||||
io_new = [proc['io_counters'][2], proc['io_counters'][3]]
|
||||
# For IO rate computation
|
||||
# Append saved IO r/w bytes
|
||||
try:
|
||||
@ -480,7 +479,11 @@ class GlancesProcesses(object):
|
||||
self.processlist_cache[proc['pid']] = {cached: proc[cached] for cached in cached_attrs}
|
||||
|
||||
# Apply user filter
|
||||
self.processlist = list(filter(lambda p: not self._filter.is_filtered(p), self.processlist))
|
||||
processlist = list(filter(lambda p: not self._filter.is_filtered(p), processlist))
|
||||
|
||||
# Save the new processlist and transform all namedtuples to dict
|
||||
self.processlist = [{k: (v._asdict() if hasattr(v, '_asdict') else v)
|
||||
for k, v in p.items()} for p in processlist]
|
||||
|
||||
# Compute the maximum value for keys in self._max_values_list: CPU, MEM
|
||||
# Useful to highlight the processes with maximum values
|
||||
|
@ -7,6 +7,8 @@
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
from collections import Counter
|
||||
|
||||
# from glances.logger import logger
|
||||
|
||||
# This constant defines the list of available processes sort key
|
||||
@ -21,9 +23,9 @@ def create_program_dict(p):
|
||||
'num_threads': p['num_threads'] or 0,
|
||||
'cpu_percent': p['cpu_percent'] or 0,
|
||||
'memory_percent': p['memory_percent'] or 0,
|
||||
'cpu_times': p['cpu_times'] or (),
|
||||
'memory_info': p['memory_info'] or (),
|
||||
'io_counters': p['io_counters'] or (),
|
||||
'cpu_times': p['cpu_times'] or {},
|
||||
'memory_info': p['memory_info'] or {},
|
||||
'io_counters': p['io_counters'] or {},
|
||||
'childrens': [p['pid']],
|
||||
# Others keys are not used
|
||||
# but should be set to be compliant with the existing process_list
|
||||
@ -35,14 +37,17 @@ def create_program_dict(p):
|
||||
'status': p['status'],
|
||||
}
|
||||
|
||||
|
||||
def update_program_dict(program, p):
|
||||
"""Update an existing entry in the dict (existing program)"""
|
||||
# some values can be None, e.g. macOS system processes
|
||||
program['num_threads'] += p['num_threads'] or 0
|
||||
program['cpu_percent'] += p['cpu_percent'] or 0
|
||||
program['memory_percent'] += p['memory_percent'] or 0
|
||||
program['cpu_times'] += p['cpu_times'] or ()
|
||||
program['memory_info'] += p['memory_info'] or ()
|
||||
program['cpu_times'] = dict(Counter(program['cpu_times'] or {}) +
|
||||
Counter(p['cpu_times'] or {}))
|
||||
program['memory_info'] = dict(Counter(program['memory_info'] or {}) +
|
||||
Counter(p['memory_info'] or {}))
|
||||
|
||||
program['io_counters'] += p['io_counters']
|
||||
program['childrens'].append(p['pid'])
|
||||
@ -51,6 +56,7 @@ def update_program_dict(program, p):
|
||||
program['nice'] = p['nice'] if p['nice'] == program['nice'] else '_'
|
||||
program['status'] = p['status'] if p['status'] == program['status'] else '_'
|
||||
|
||||
|
||||
def processes_to_programs(processes):
|
||||
"""Convert a list of processes to a list of programs."""
|
||||
# Start to build a dict of programs (key is program name)
|
||||
|
@ -136,7 +136,7 @@ class GlancesStats(object):
|
||||
|
||||
# Log plugins list
|
||||
logger.debug("Active plugins list: {}".format(self.getPluginsList()))
|
||||
|
||||
|
||||
def load_additional_plugins(self, args=None, config=None):
|
||||
""" Load additional plugins if defined """
|
||||
def get_addl_plugins(self, plugin_path):
|
||||
@ -162,7 +162,7 @@ class GlancesStats(object):
|
||||
|
||||
if args and 'plugin_dir' in args and args.plugin_dir:
|
||||
path = args.plugin_dir
|
||||
|
||||
|
||||
if path:
|
||||
# Get list before starting the counter
|
||||
_sys_path = sys.path
|
||||
@ -190,7 +190,7 @@ class GlancesStats(object):
|
||||
sys.path = _sys_path
|
||||
# Log plugins list
|
||||
logger.debug("Active additional plugins list: {}".format(self.getPluginsList()))
|
||||
|
||||
|
||||
def load_exports(self, args=None):
|
||||
"""Load all exporters in the 'exports' folder."""
|
||||
start_duration = Counter()
|
||||
|
@ -2,17 +2,17 @@
|
||||
#
|
||||
# This file is part of Glances.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-3.0-only
|
||||
#
|
||||
|
||||
"""Glances Web Interface (Bottle based)."""
|
||||
"""Glances Restful/API and Web based interface."""
|
||||
|
||||
from glances.globals import WINDOWS
|
||||
from glances.processes import glances_processes
|
||||
from glances.stats import GlancesStats
|
||||
from glances.outputs.glances_bottle import GlancesBottle
|
||||
from glances.outputs.glances_restful_api import GlancesRestfulApi
|
||||
|
||||
|
||||
class GlancesWebServer(object):
|
||||
@ -30,8 +30,8 @@ class GlancesWebServer(object):
|
||||
# Initial system information update
|
||||
self.stats.update()
|
||||
|
||||
# Init the Bottle Web server
|
||||
self.web = GlancesBottle(config=config, args=args)
|
||||
# Init the Web server
|
||||
self.web = GlancesRestfulApi(config=config, args=args)
|
||||
|
||||
def serve_forever(self):
|
||||
"""Main loop for the Web server."""
|
||||
|
@ -3,17 +3,19 @@
|
||||
|
||||
batinfo
|
||||
bernhard
|
||||
bottle
|
||||
cassandra-driver
|
||||
chevron
|
||||
docker>=6.1.1
|
||||
elasticsearch
|
||||
fastapi; python_version >= "3.8"
|
||||
graphitesender
|
||||
hddtemp
|
||||
influxdb>=1.0.0 # For InfluxDB < 1.8
|
||||
influxdb-client; python_version >= "3.7" # For InfluxDB >= 1.8
|
||||
jinja2
|
||||
kafka-python
|
||||
netifaces
|
||||
orjson; python_version >= "3.8"
|
||||
packaging; python_version >= "3.7"
|
||||
paho-mqtt
|
||||
pika
|
||||
@ -34,6 +36,7 @@ scandir; python_version < "3.5"
|
||||
six
|
||||
sparklines
|
||||
statsd
|
||||
uvicorn; python_version >= "3.8"
|
||||
wifi
|
||||
zeroconf==0.112.0; python_version < "3.7"
|
||||
zeroconf; python_version >= "3.7"
|
||||
|
@ -1,7 +1,5 @@
|
||||
psutil>=5.6.7
|
||||
defusedxml
|
||||
packaging
|
||||
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"
|
||||
ujson>=5.4.0
|
||||
pytz
|
||||
|
9
setup.py
9
setup.py
@ -44,7 +44,10 @@ def get_install_requires():
|
||||
'ujson>=5.4.0',
|
||||
]
|
||||
if sys.platform.startswith('win'):
|
||||
requires.append('bottle')
|
||||
requires.append('fastapi')
|
||||
requires.append('uvicorn')
|
||||
requires.append('orjson')
|
||||
requires.append('jinja2')
|
||||
requires.append('requests')
|
||||
|
||||
return requires
|
||||
@ -67,7 +70,7 @@ def get_install_extras_require():
|
||||
'smart': ['pySMART.smartx'],
|
||||
'snmp': ['pysnmp'],
|
||||
'sparklines': ['sparklines'],
|
||||
'web': ['bottle', 'requests'],
|
||||
'web': ['fastapi', 'uvicorn', 'jinja2', 'orjson', 'requests'],
|
||||
'wifi': ['wifi']
|
||||
}
|
||||
if sys.platform.startswith('linux'):
|
||||
@ -123,7 +126,7 @@ setup(
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console :: Curses',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Bottle',
|
||||
'Framework :: FastAPI',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'Intended Audience :: System Administrators',
|
||||
|
@ -53,16 +53,50 @@ parts:
|
||||
override-pull: |
|
||||
snapcraftctl pull
|
||||
"$SNAPCRAFT_STAGE"/scriptlets/selective-checkout
|
||||
bottle:
|
||||
|
||||
fastapi:
|
||||
plugin: python
|
||||
source: https://github.com/bottlepy/bottle.git
|
||||
source-branch: release-0.12
|
||||
source: https://github.com/tiangolo/fastapi.git
|
||||
source-tag: '0.104.1'
|
||||
source-depth: 1
|
||||
override-build: |
|
||||
mkdir -p $SNAPCRAFT_PART_BUILD/dist
|
||||
cp -r $SNAPCRAFT_PART_BUILD/dist $SNAPCRAFT_PART_INSTALL/bottle-dist
|
||||
cp -r $SNAPCRAFT_PART_BUILD/dist $SNAPCRAFT_PART_INSTALL/fastapi-dist
|
||||
organize:
|
||||
bottle-dist: bottle/dist
|
||||
fastapi-dist: fastapi/dist
|
||||
|
||||
uvicorn:
|
||||
plugin: python
|
||||
source: https://github.com/encode/uvicorn.git
|
||||
source-tag: '0.24.0.post1'
|
||||
source-depth: 1
|
||||
override-build: |
|
||||
mkdir -p $SNAPCRAFT_PART_BUILD/dist
|
||||
cp -r $SNAPCRAFT_PART_BUILD/dist $SNAPCRAFT_PART_INSTALL/uvicorn-dist
|
||||
organize:
|
||||
uvicorn-dist: uvicorn/dist
|
||||
|
||||
orjson:
|
||||
plugin: python
|
||||
source: https://github.com/ijl/orjson.git
|
||||
source-tag: '3.9.10'
|
||||
source-depth: 1
|
||||
override-build: |
|
||||
mkdir -p $SNAPCRAFT_PART_BUILD/dist
|
||||
cp -r $SNAPCRAFT_PART_BUILD/dist $SNAPCRAFT_PART_INSTALL/orjson-dist
|
||||
organize:
|
||||
orjson-dist: orjson/dist
|
||||
|
||||
jinja2:
|
||||
plugin: python
|
||||
source: https://github.com/pallets/jinja.git
|
||||
source-tag: '3.1.2'
|
||||
source-depth: 1
|
||||
override-build: |
|
||||
mkdir -p $SNAPCRAFT_PART_BUILD/dist
|
||||
cp -r $SNAPCRAFT_PART_BUILD/dist $SNAPCRAFT_PART_INSTALL/jinja2-dist
|
||||
organize:
|
||||
jinja2-dist: jinja2/dist
|
||||
|
||||
docker:
|
||||
plugin: python
|
||||
|
5
tox.ini
5
tox.ini
@ -19,7 +19,10 @@ deps =
|
||||
defusedxml
|
||||
packaging
|
||||
ujson
|
||||
bottle
|
||||
fastapi
|
||||
uvicorn
|
||||
orjson
|
||||
jinja2
|
||||
requests
|
||||
commands =
|
||||
python unitest.py
|
||||
|
@ -17,13 +17,14 @@ import time
|
||||
import numbers
|
||||
import unittest
|
||||
|
||||
from glances.outputs.glances_restful_api import GlancesRestfulApi
|
||||
from glances import __version__
|
||||
from glances.globals import text_type
|
||||
|
||||
import requests
|
||||
|
||||
SERVER_PORT = 61234
|
||||
API_VERSION = 3
|
||||
API_VERSION = GlancesRestfulApi.API_VERSION
|
||||
URL = "http://localhost:{}/api/{}".format(SERVER_PORT, API_VERSION)
|
||||
pid = None
|
||||
|
||||
@ -39,12 +40,12 @@ class TestGlances(unittest.TestCase):
|
||||
"""The function is called *every time* before test_*."""
|
||||
print('\n' + '=' * 78)
|
||||
|
||||
def http_get(self, url, deflate=False):
|
||||
def http_get(self, url, gzip=False):
|
||||
"""Make the request"""
|
||||
if deflate:
|
||||
if gzip:
|
||||
ret = requests.get(url,
|
||||
stream=True,
|
||||
headers={'Accept-encoding': 'deflate'})
|
||||
headers={'Accept-encoding': 'gzip'})
|
||||
else:
|
||||
ret = requests.get(url,
|
||||
headers={'Accept-encoding': 'identity'})
|
||||
@ -54,12 +55,12 @@ class TestGlances(unittest.TestCase):
|
||||
"""Start the Glances Web Server."""
|
||||
global pid
|
||||
|
||||
print('INFO: [TEST_000] Start the Glances Web Server')
|
||||
print('INFO: [TEST_000] Start the Glances Web Server API')
|
||||
if os.path.isfile('./venv/bin/python'):
|
||||
cmdline = "./venv/bin/python"
|
||||
else:
|
||||
cmdline = "python"
|
||||
cmdline += " -m glances -B localhost -w -p %s" % SERVER_PORT
|
||||
cmdline += " -m glances -B 0.0.0.0 -w -p %s --disable-webui -C ./conf/glances.conf" % SERVER_PORT
|
||||
print("Run the Glances Web Server on port %s" % SERVER_PORT)
|
||||
args = shlex.split(cmdline)
|
||||
pid = subprocess.Popen(args)
|
||||
@ -76,16 +77,7 @@ class TestGlances(unittest.TestCase):
|
||||
req = self.http_get("%s/%s" % (URL, method))
|
||||
|
||||
self.assertTrue(req.ok)
|
||||
|
||||
def test_001a_all_deflate(self):
|
||||
"""All."""
|
||||
method = "all"
|
||||
print('INFO: [TEST_001a] Get all stats (with Deflate compression)')
|
||||
print("HTTP RESTful request: %s/%s" % (URL, method))
|
||||
req = self.http_get("%s/%s" % (URL, method), deflate=True)
|
||||
|
||||
self.assertTrue(req.ok)
|
||||
self.assertTrue(req.headers['Content-Encoding'] == 'deflate')
|
||||
self.assertTrue(req.json(), dict)
|
||||
|
||||
def test_002_pluginslist(self):
|
||||
"""Plugins list."""
|
||||
@ -108,14 +100,12 @@ class TestGlances(unittest.TestCase):
|
||||
print("HTTP RESTful request: %s/%s" % (URL, p))
|
||||
req = self.http_get("%s/%s" % (URL, p))
|
||||
self.assertTrue(req.ok)
|
||||
if p in ('uptime', 'now'):
|
||||
if p in ('uptime', 'now', 'version', 'psutilversion'):
|
||||
self.assertIsInstance(req.json(), text_type)
|
||||
elif p in ('fs', 'percpu', 'sensors', 'alert', 'processlist', 'diskio',
|
||||
'hddtemp', 'batpercent', 'network', 'folders', 'amps', 'ports',
|
||||
'irq', 'wifi', 'gpu'):
|
||||
self.assertIsInstance(req.json(), list)
|
||||
elif p in ('psutilversion', 'help'):
|
||||
pass
|
||||
else:
|
||||
self.assertIsInstance(req.json(), dict)
|
||||
|
||||
@ -203,14 +193,12 @@ class TestGlances(unittest.TestCase):
|
||||
self.assertTrue(len(req.json()['user']) > 1)
|
||||
print("HTTP RESTful request: %s/cpu/system/%s" % (URL, method))
|
||||
req = self.http_get("%s/cpu/system/%s" % (URL, method))
|
||||
self.assertIsInstance(req.json(), dict)
|
||||
self.assertIsInstance(req.json()['system'], list)
|
||||
self.assertTrue(len(req.json()['system']) > 0)
|
||||
self.assertIsInstance(req.json(), list)
|
||||
self.assertIsInstance(req.json()[0], list)
|
||||
print("HTTP RESTful request: %s/cpu/system/%s/3" % (URL, method))
|
||||
req = self.http_get("%s/cpu/system/%s/3" % (URL, method))
|
||||
self.assertIsInstance(req.json(), dict)
|
||||
self.assertIsInstance(req.json()['system'], list)
|
||||
self.assertTrue(len(req.json()['system']) > 1)
|
||||
self.assertIsInstance(req.json(), list)
|
||||
self.assertIsInstance(req.json()[0], list)
|
||||
|
||||
def test_011_issue1401(self):
|
||||
"""Check issue #1401."""
|
||||
@ -229,7 +217,7 @@ class TestGlances(unittest.TestCase):
|
||||
req = self.http_get("%s/%s" % (URL, method))
|
||||
|
||||
self.assertTrue(req.ok)
|
||||
self.assertEqual(req.text, "Active")
|
||||
self.assertEqual(req.json()['version'], __version__)
|
||||
|
||||
def test_013_top(self):
|
||||
"""Values."""
|
||||
@ -243,6 +231,30 @@ class TestGlances(unittest.TestCase):
|
||||
self.assertIsInstance(req.json(), list)
|
||||
self.assertEqual(len(req.json()), 2)
|
||||
|
||||
def test_014_config(self):
|
||||
"""Test API request to get Glances configuration."""
|
||||
method = "config"
|
||||
print('INFO: [TEST_014] Get config')
|
||||
|
||||
req = self.http_get("%s/%s" % (URL, method))
|
||||
self.assertTrue(req.ok)
|
||||
self.assertIsInstance(req.json(), dict)
|
||||
|
||||
req = self.http_get("%s/%s/global/refresh" % (URL, method))
|
||||
self.assertTrue(req.ok)
|
||||
self.assertEqual(req.json(), "2")
|
||||
|
||||
def test_015_all_gzip(self):
|
||||
"""All with Gzip."""
|
||||
method = "all"
|
||||
print('INFO: [TEST_015] Get all stats (with GZip compression)')
|
||||
print("HTTP RESTful request: %s/%s" % (URL, method))
|
||||
req = self.http_get("%s/%s" % (URL, method), gzip=True)
|
||||
|
||||
self.assertTrue(req.ok)
|
||||
self.assertTrue(req.headers['Content-Encoding'] == 'gzip')
|
||||
self.assertTrue(req.json(), dict)
|
||||
|
||||
def test_999_stop_server(self):
|
||||
"""Stop the Glances Web Server."""
|
||||
print('INFO: [TEST_999] Stop the Glances Web Server')
|
||||
|
@ -280,10 +280,8 @@ class TestGlances(unittest.TestCase):
|
||||
# stats_to_check = [ ]
|
||||
print('INFO: [TEST_017] Check PROGRAM stats')
|
||||
stats_grab = processes_to_programs(stats.get_plugin('processlist').get_raw())
|
||||
self.assertTrue(type(stats_grab) is list, msg='Programs stats is not a list')
|
||||
print('INFO: PROGRAM list stats: %s items in the list' % len(stats_grab))
|
||||
# Check if number of processes in the list equal counter
|
||||
# self.assertEqual(total, len(stats_grab))
|
||||
self.assertIsInstance(stats_grab, list, msg='Programs stats list is not a list')
|
||||
self.assertIsInstance(stats_grab[0], dict, msg='First item should be a dict')
|
||||
|
||||
def test_018_string_value_to_float(self):
|
||||
"""Check string_value_to_float function"""
|
||||
|
@ -1,4 +1,7 @@
|
||||
# install with base requirements file
|
||||
-r requirements.txt
|
||||
|
||||
bottle
|
||||
fastapi; python_version >= "3.8"
|
||||
uvicorn; python_version >= "3.8"
|
||||
orjson; python_version >= "3.8"
|
||||
jinja2
|
Loading…
Reference in New Issue
Block a user