Unit tests are ok BUT the WebUI has a lot of issue: plugin disapear, traceback... Perhaps issue when stats are refreched ?

This commit is contained in:
nicolargo 2023-12-10 19:27:02 +01:00
parent 5d054e12e1
commit cbb2facc87
8 changed files with 118 additions and 81 deletions

View File

@ -20,19 +20,10 @@ from urllib.parse import urljoin
# from typing import Annotated # from typing import Annotated
from typing_extensions import Annotated from typing_extensions import Annotated
from glances.globals import json_dumps
from glances.timer import Timer from glances.timer import Timer
from glances.logger import logger from glances.logger import logger
# FastAPI import # FastAPI import
# TODO: not sure import is needed
try:
import jinja2
except ImportError:
logger.critical('Jinja2 import error. Glances cannot start in web server mode.')
sys.exit(2)
try: try:
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Request from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Request
from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.security import HTTPBasic, HTTPBasicCredentials
@ -78,7 +69,7 @@ class GlancesRestfulApi(object):
# Load configuration file # Load configuration file
self.load_config(config) self.load_config(config)
# Set the bind URL (only used for log information purpose) # Set the bind URL
self.bind_url = urljoin('http://{}:{}/'.format(self.args.bind_address, self.bind_url = urljoin('http://{}:{}/'.format(self.args.bind_address,
self.args.port), self.args.port),
self.url_prefix) self.url_prefix)
@ -95,7 +86,6 @@ class GlancesRestfulApi(object):
# Set path for WebUI # Set path for WebUI
self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public') self.STATIC_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/public')
# TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates'))
self.TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates') self.TEMPLATE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static/templates')
self._templates = Jinja2Templates(directory=self.TEMPLATE_PATH) self._templates = Jinja2Templates(directory=self.TEMPLATE_PATH)
@ -103,7 +93,10 @@ class GlancesRestfulApi(object):
# https://fastapi.tiangolo.com/tutorial/cors/ # https://fastapi.tiangolo.com/tutorial/cors/
self._app.add_middleware( self._app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], # allow_origins=["*"],
allow_origins=[
self.bind_url
],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@ -140,7 +133,7 @@ class GlancesRestfulApi(object):
# TODO: the password comparaison is not working for the moment. # TODO: the password comparaison is not working for the moment.
# if the password is wrong, authentication is working... # if the password is wrong, authentication is working...
# Perahps because the password is hashed in the GlancesPassword class # Perhaps because the password is hashed in the GlancesPassword class
# and the one given by creds.password is not hashed ? # and the one given by creds.password is not hashed ?
def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]): def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
"""Check if a username/password combination is valid.""" """Check if a username/password combination is valid."""
@ -168,21 +161,24 @@ class GlancesRestfulApi(object):
status_code=status.HTTP_200_OK, status_code=status.HTTP_200_OK,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_status) endpoint=self._api_status)
router.add_api_route('/api/%s/config' % self.API_VERSION, router.add_api_route('/api/%s/config' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_config) endpoint=self._api_config)
router.add_api_route('/api/%s/config/{item}' % self.API_VERSION, router.add_api_route('/api/%s/config/{section}' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_config_item) 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, router.add_api_route('/api/%s/args' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_args) endpoint=self._api_args)
router.add_api_route('/api/%s/args/{item}' % self.API_VERSION, router.add_api_route('/api/%s/args/{item}' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_args_item) endpoint=self._api_args_item)
router.add_api_route('/api/%s/help' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_help)
router.add_api_route('/api/%s/pluginslist' % self.API_VERSION, router.add_api_route('/api/%s/pluginslist' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_plugins) endpoint=self._api_plugins)
@ -195,6 +191,10 @@ class GlancesRestfulApi(object):
router.add_api_route('/api/%s/all/views' % self.API_VERSION, router.add_api_route('/api/%s/all/views' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_all_views) 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, router.add_api_route('/api/%s/{plugin}' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api) endpoint=self._api)
@ -204,7 +204,7 @@ class GlancesRestfulApi(object):
router.add_api_route('/api/%s/{plugin}/history/{nb}' % self.API_VERSION, router.add_api_route('/api/%s/{plugin}/history/{nb}' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_history) endpoint=self._api_history)
router.add_api_route('/api/%s/{plugin}/top/<nb:int>' % self.API_VERSION, router.add_api_route('/api/%s/{plugin}/top/{nb}' % self.API_VERSION,
response_class=ORJSONResponse, response_class=ORJSONResponse,
endpoint=self._api_top) endpoint=self._api_top)
router.add_api_route('/api/%s/{plugin}/limits' % self.API_VERSION, router.add_api_route('/api/%s/{plugin}/limits' % self.API_VERSION,
@ -232,18 +232,13 @@ class GlancesRestfulApi(object):
# WEB UI # WEB UI
if not self.args.disable_webui: if not self.args.disable_webui:
# Template # Template for the root index.html file
router.add_api_route('/', router.add_api_route('/',
response_class=HTMLResponse, response_class=HTMLResponse,
endpoint=self._index) endpoint=self._index)
# TODO: to be migrated to another route
# router.add_api_route('/{refresh_time}',
# endpoint=self._index)
# Statics files # Statics files
# self._app.mount("/static", StaticFiles(directory=self.STATIC_PATH), name="static") self._app.mount("/static",
self._app.mount("/",
StaticFiles(directory=self.STATIC_PATH), StaticFiles(directory=self.STATIC_PATH),
name="static") name="static")
@ -282,30 +277,27 @@ class GlancesRestfulApi(object):
logger.critical('Error: Can not ran Glances Web server ({})'.format(e)) logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
def end(self): def end(self):
"""End the bottle.""" """End the Web server"""
logger.info("Close the Web server") logger.info("Close the Web server")
# TODO: close FastAPI instance gracefully
# self._app.close()
# if self.url_prefix != '/':
# self.main_app.close()
# Example from FastAPI documentation def _index(self, request: Request):
# @app.get("/", response_class=HTMLResponse) """Return main index.html (/) file.
# def home(request: Request): Parameters are available through the request object.
# return templates.TemplateResponse("index.html", {"request": request}) Example: http://localhost:61208/?refresh=5
"""
def _index(self, refresh_time=None): refresh_time = request.query_params.get('refresh',
"""Return main index.html (/) file.""" default=max(1, int(self.args.time)))
if refresh_time is None or refresh_time < 1:
refresh_time = int(self.args.time)
# Update the stat # Update the stat
self.__update__() self.__update__()
# Display # Display
# return template("index.html", refresh_time=refresh_time) return self._templates.TemplateResponse("index.html",
return self.templates.TemplateResponse("index.html") {
"request": request,
"refresh_time": refresh_time,
})
def _api_status(self): def _api_status(self):
"""Glances API RESTful implementation. """Glances API RESTful implementation.
@ -662,29 +654,61 @@ class GlancesRestfulApi(object):
return ORJSONResponse(args_json) return ORJSONResponse(args_json)
def _api_config_item(self, item): def _api_config_section(self, section):
"""Glances API RESTful implementation. """Glances API RESTful implementation.
Return the JSON representation of the Glances configuration item Return the JSON representation of the Glances configuration section
HTTP/200 if OK HTTP/200 if OK
HTTP/400 if item is not found HTTP/400 if item is not found
HTTP/404 if others error HTTP/404 if others error
""" """
config_dict = self.config.as_dict() config_dict = self.config.as_dict()
if item not in config_dict: if section not in config_dict:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown configuration item %s" % item) detail="Unknown configuration item %s" % section)
try: try:
# Get the RAW value of the config' dict # Get the RAW value of the config' dict
args_json = config_dict[item] ret_section = config_dict[section]
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Cannot get config item (%s)" % str(e)) detail="Cannot get config section %s (%s)" % (section, str(e)))
return ORJSONResponse(args_json) 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): def _api_args(self):
"""Glances API RESTful implementation. """Glances API RESTful implementation.

View File

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

View File

@ -78,10 +78,10 @@
<div class="table-cell" :class="getMemoryPercentAlert(process)"> <div class="table-cell" :class="getMemoryPercentAlert(process)">
{{ process.memory_percent == -1 ? '?' : $filters.number(process.memory_percent, 1) }} {{ process.memory_percent == -1 ? '?' : $filters.number(process.memory_percent, 1) }}
</div> </div>
<div class="table-cell hidden-xs hidden-sm"> <div class="table-cell">
{{ $filters.bytes(process.memvirt) }} {{ $filters.bytes(process.memvirt) }}
</div> </div>
<div class="table-cell hidden-xs hidden-sm"> <div class="table-cell">
{{ $filters.bytes(process.memres) }} {{ $filters.bytes(process.memres) }}
</div> </div>
<div class="table-cell"> <div class="table-cell">
@ -159,8 +159,8 @@ export default {
process.memvirt = '?'; process.memvirt = '?';
process.memres = '?'; process.memres = '?';
if (process.memory_info) { if (process.memory_info) {
process.memvirt = process.memory_info[1]; process.memvirt = process.memory_info.vms;
process.memres = process.memory_info[0]; process.memres = process.memory_info.rss;
} }
process.timeplus = '?'; process.timeplus = '?';

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -443,7 +443,7 @@ class GlancesProcesses(object):
# If io_tag = 0 > Access denied or first time (display "?") # If io_tag = 0 > Access denied or first time (display "?")
# If io_tag = 1 > No access denied (display the IO rate) # If io_tag = 1 > No access denied (display the IO rate)
if 'io_counters' in proc and proc['io_counters'] is not None: 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 # For IO rate computation
# Append saved IO r/w bytes # Append saved IO r/w bytes
try: try:

View File

@ -59,7 +59,7 @@ class TestGlances(unittest.TestCase):
cmdline = "./venv/bin/python" cmdline = "./venv/bin/python"
else: else:
cmdline = "python" cmdline = "python"
cmdline += " -m glances -B 0.0.0.0 -w -p %s --disable-webui" % 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) print("Run the Glances Web Server on port %s" % SERVER_PORT)
args = shlex.split(cmdline) args = shlex.split(cmdline)
pid = subprocess.Popen(args) pid = subprocess.Popen(args)
@ -243,6 +243,19 @@ class TestGlances(unittest.TestCase):
self.assertIsInstance(req.json(), list) self.assertIsInstance(req.json(), list)
self.assertEqual(len(req.json()), 2) 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_999_stop_server(self): def test_999_stop_server(self):
"""Stop the Glances Web Server.""" """Stop the Glances Web Server."""
print('INFO: [TEST_999] Stop the Glances Web Server') print('INFO: [TEST_999] Stop the Glances Web Server')