mirror of
https://github.com/nicolargo/glances.git
synced 2024-11-28 05:42:57 +03:00
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:
parent
5d054e12e1
commit
cbb2facc87
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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 = '?';
|
||||||
|
2
glances/outputs/static/public/glances.js
vendored
2
glances/outputs/static/public/glances.js
vendored
File diff suppressed because one or more lines are too long
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>
|
|
@ -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:
|
||||||
|
@ -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')
|
||||||
|
Loading…
Reference in New Issue
Block a user