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_extensions import Annotated
from glances.globals import json_dumps
from glances.timer import Timer
from glances.logger import logger
# 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:
from fastapi import FastAPI, Depends, HTTPException, status, APIRouter, Request
from fastapi.security import HTTPBasic, HTTPBasicCredentials
@ -78,7 +69,7 @@ class GlancesRestfulApi(object):
# Load configuration file
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.args.port),
self.url_prefix)
@ -95,7 +86,6 @@ class GlancesRestfulApi(object):
# Set path for WebUI
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._templates = Jinja2Templates(directory=self.TEMPLATE_PATH)
@ -103,7 +93,10 @@ class GlancesRestfulApi(object):
# https://fastapi.tiangolo.com/tutorial/cors/
self._app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
# allow_origins=["*"],
allow_origins=[
self.bind_url
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -140,7 +133,7 @@ class GlancesRestfulApi(object):
# TODO: the password comparaison is not working for the moment.
# 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 ?
def authentication(self, creds: Annotated[HTTPBasicCredentials, Depends(security)]):
"""Check if a username/password combination is valid."""
@ -168,21 +161,24 @@ class GlancesRestfulApi(object):
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/{item}' % self.API_VERSION,
router.add_api_route('/api/%s/config/{section}' % self.API_VERSION,
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,
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/help' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_help)
router.add_api_route('/api/%s/pluginslist' % self.API_VERSION,
response_class=ORJSONResponse,
endpoint=self._api_plugins)
@ -195,6 +191,10 @@ class GlancesRestfulApi(object):
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)
@ -204,7 +204,7 @@ class GlancesRestfulApi(object):
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:int>' % self.API_VERSION,
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,
@ -232,18 +232,13 @@ class GlancesRestfulApi(object):
# WEB UI
if not self.args.disable_webui:
# Template
# Template for the root index.html file
router.add_api_route('/',
response_class=HTMLResponse,
endpoint=self._index)
# TODO: to be migrated to another route
# router.add_api_route('/{refresh_time}',
# endpoint=self._index)
# Statics files
# self._app.mount("/static", StaticFiles(directory=self.STATIC_PATH), name="static")
self._app.mount("/",
self._app.mount("/static",
StaticFiles(directory=self.STATIC_PATH),
name="static")
@ -282,30 +277,27 @@ class GlancesRestfulApi(object):
logger.critical('Error: Can not ran Glances Web server ({})'.format(e))
def end(self):
"""End the bottle."""
"""End 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
# @app.get("/", response_class=HTMLResponse)
# def home(request: Request):
# return templates.TemplateResponse("index.html", {"request": request})
def _index(self, request: Request):
"""Return main index.html (/) file.
Parameters are available through the request object.
Example: http://localhost:61208/?refresh=5
"""
def _index(self, refresh_time=None):
"""Return main index.html (/) file."""
if refresh_time is None or refresh_time < 1:
refresh_time = int(self.args.time)
refresh_time = request.query_params.get('refresh',
default=max(1, int(self.args.time)))
# Update the stat
self.__update__()
# 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):
"""Glances API RESTful implementation.
@ -662,29 +654,61 @@ class GlancesRestfulApi(object):
return ORJSONResponse(args_json)
def _api_config_item(self, item):
def _api_config_section(self, section):
"""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/400 if item is not found
HTTP/404 if others error
"""
config_dict = self.config.as_dict()
if item not in config_dict:
if section not in config_dict:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Unknown configuration item %s" % item)
detail="Unknown configuration item %s" % section)
try:
# Get the RAW value of the config' dict
args_json = config_dict[item]
ret_section = config_dict[section]
except Exception as e:
raise HTTPException(
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):
"""Glances API RESTful implementation.

View File

@ -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;

View File

@ -78,10 +78,10 @@
<div class="table-cell" :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">
{{ $filters.bytes(process.memvirt) }}
</div>
<div class="table-cell hidden-xs hidden-sm">
<div class="table-cell">
{{ $filters.bytes(process.memres) }}
</div>
<div class="table-cell">
@ -159,8 +159,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 = '?';

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 = 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:

View File

@ -59,7 +59,7 @@ class TestGlances(unittest.TestCase):
cmdline = "./venv/bin/python"
else:
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)
args = shlex.split(cmdline)
pid = subprocess.Popen(args)
@ -243,6 +243,19 @@ 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_999_stop_server(self):
"""Stop the Glances Web Server."""
print('INFO: [TEST_999] Stop the Glances Web Server')