mirror of
https://github.com/nicolargo/glances.git
synced 2024-11-24 05:15:47 +03:00
Rename any reference to Bottle in doc and dep file. Ready to refactor the main glances_restful_api.py file.
This commit is contained in:
parent
36ed96b05a
commit
bcf4ce45fc
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-min/bin/python ./unitest-restful.py
|
||||
|
||||
# ===================================================================
|
||||
# Linters and profilers
|
||||
# ===================================================================
|
||||
|
@ -92,11 +92,11 @@ 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)
|
||||
@ -207,10 +207,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 +319,7 @@ Start Termux on your device and enter:
|
||||
$ apt update
|
||||
$ apt upgrade
|
||||
$ apt install clang python
|
||||
$ pip install bottle
|
||||
$ pip install fastapi uvicorn
|
||||
$ pip install glances
|
||||
|
||||
And start Glances:
|
||||
|
@ -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
|
||||
|
||||
|
@ -28,7 +28,7 @@ Available tags (all images are based on both Alpine and Ubuntu Operating System)
|
||||
* - `latest`
|
||||
- Alpine
|
||||
- Latest Release
|
||||
- Minimal + (Bottle & Docker)
|
||||
- Minimal + (FastAPI & Docker)
|
||||
* - `dev`
|
||||
- Alpine
|
||||
- develop
|
||||
@ -40,7 +40,7 @@ Available tags (all images are based on both Alpine and Ubuntu Operating System)
|
||||
* - `ubuntu-latest`
|
||||
- Ubuntu
|
||||
- Latest Release
|
||||
- Minimal + (Bottle & Docker)
|
||||
- Minimal + (FastAPI & Docker)
|
||||
* - `ubuntu-dev`
|
||||
- Ubuntu
|
||||
- develop
|
||||
|
@ -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
|
||||
|
@ -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 and Uvicorn lib 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
|
@ -65,7 +65,7 @@ static
|
||||
|
|
||||
|--- public # path where builds are put
|
||||
|
|
||||
|--- templates (bottle)
|
||||
|--- templates
|
||||
```
|
||||
|
||||
## Data
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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,11 +3,11 @@
|
||||
|
||||
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
|
||||
@ -34,6 +34,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
|
||||
|
7
setup.py
7
setup.py
@ -44,7 +44,8 @@ def get_install_requires():
|
||||
'ujson>=5.4.0',
|
||||
]
|
||||
if sys.platform.startswith('win'):
|
||||
requires.append('bottle')
|
||||
requires.append('fastapi')
|
||||
requires.append('uvicorn')
|
||||
requires.append('requests')
|
||||
|
||||
return requires
|
||||
@ -67,7 +68,7 @@ def get_install_extras_require():
|
||||
'smart': ['pySMART.smartx'],
|
||||
'snmp': ['pysnmp'],
|
||||
'sparklines': ['sparklines'],
|
||||
'web': ['bottle', 'requests'],
|
||||
'web': ['fastapi', 'uvicorn', 'requests'],
|
||||
'wifi': ['wifi']
|
||||
}
|
||||
if sys.platform.startswith('linux'):
|
||||
@ -123,7 +124,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,28 @@ 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
|
||||
|
||||
docker:
|
||||
plugin: python
|
||||
|
3
tox.ini
3
tox.ini
@ -19,7 +19,8 @@ deps =
|
||||
defusedxml
|
||||
packaging
|
||||
ujson
|
||||
bottle
|
||||
fastapi
|
||||
uvicorn
|
||||
requests
|
||||
commands =
|
||||
python unitest.py
|
||||
|
@ -1,4 +1,5 @@
|
||||
# install with base requirements file
|
||||
-r requirements.txt
|
||||
|
||||
bottle
|
||||
fastapi; python_version >= "3.8"
|
||||
uvicorn; python_version >= "3.8"
|
||||
|
Loading…
Reference in New Issue
Block a user