From bcf4ce45fc0394ea64d35062c97a5e5dfdedd29b Mon Sep 17 00:00:00 2001 From: nicolargo Date: Sun, 26 Nov 2023 10:02:30 +0100 Subject: Rename any reference to Bottle in doc and dep file. Ready to refactor the main glances_restful_api.py file. --- Makefile | 3 + README.rst | 8 +- docs/cmds.rst | 2 +- docs/docker.rst | 4 +- docs/man/glances.1 | 2 +- glances/README.txt | 4 +- glances/main.py | 2 +- glances/outputs/glances_bottle.py | 668 -------------------------------- glances/outputs/static/README.md | 2 +- glances/plugins/processlist/__init__.py | 2 +- glances/webserver.py | 10 +- optional-requirements.txt | 3 +- requirements.txt | 4 +- setup.py | 7 +- snap/snapcraft.yaml | 22 +- tox.ini | 3 +- webui-requirements.txt | 3 +- 17 files changed, 49 insertions(+), 700 deletions(-) delete mode 100644 glances/outputs/glances_bottle.py diff --git a/Makefile b/Makefile index 4ffc82b9..fd0cfae9 100644 --- a/Makefile +++ b/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 # =================================================================== diff --git a/README.rst b/README.rst index 3af03fe4..50c9c0f3 100644 --- a/README.rst +++ b/README.rst @@ -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: diff --git a/docs/cmds.rst b/docs/cmds.rst index cffeb153..91a8ed53 100644 --- a/docs/cmds.rst +++ b/docs/cmds.rst @@ -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 diff --git a/docs/docker.rst b/docs/docker.rst index ad91fd3d..9f4aa63e 100644 --- a/docs/docker.rst +++ b/docs/docker.rst @@ -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 diff --git a/docs/man/glances.1 b/docs/man/glances.1 index 87e52bf9..b2888dd8 100644 --- a/docs/man/glances.1 +++ b/docs/man/glances.1 @@ -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 diff --git a/glances/README.txt b/glances/README.txt index c5fd13eb..3e6de7eb 100644 --- a/glances/README.txt +++ b/glances/README.txt @@ -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 diff --git a/glances/main.py b/glances/main.py index 03acfd7a..85db14aa 100644 --- a/glances/main.py +++ b/glances/main.py @@ -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', diff --git a/glances/outputs/glances_bottle.py b/glances/outputs/glances_bottle.py deleted file mode 100644 index df6cfc71..00000000 --- a/glances/outputs/glances_bottle.py +++ /dev/null @@ -1,668 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Glances. -# -# SPDX-FileCopyrightText: 2023 Nicolas Hennion -# -# 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/' % 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/' % 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/' % self.API_VERSION, method="GET", callback=self._api) - self._app.route('/api/%s//history' % self.API_VERSION, method="GET", callback=self._api_history) - self._app.route( - '/api/%s//history/' % self.API_VERSION, method="GET", callback=self._api_history - ) - self._app.route('/api/%s//top/' % self.API_VERSION, method="GET", callback=self._api_top) - self._app.route('/api/%s//limits' % self.API_VERSION, method="GET", callback=self._api_limits) - self._app.route('/api/%s//views' % self.API_VERSION, method="GET", callback=self._api_views) - self._app.route('/api/%s//' % self.API_VERSION, method="GET", callback=self._api_item) - self._app.route( - '/api/%s///history' % self.API_VERSION, method="GET", callback=self._api_item_history - ) - self._app.route( - '/api/%s///history/' % self.API_VERSION, method="GET", callback=self._api_item_history - ) - self._app.route('/api/%s///' % self.API_VERSION, method="GET", callback=self._api_value) - self._app.route( - '/api/%s///' % 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('/', method=["GET"], callback=self._index) - self._app.route('/', 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 diff --git a/glances/outputs/static/README.md b/glances/outputs/static/README.md index bac2decb..5e8e7838 100644 --- a/glances/outputs/static/README.md +++ b/glances/outputs/static/README.md @@ -65,7 +65,7 @@ static | |--- public # path where builds are put | -|--- templates (bottle) +|--- templates ``` ## Data diff --git a/glances/plugins/processlist/__init__.py b/glances/plugins/processlist/__init__.py index 99219fcb..ac6ade93 100644 --- a/glances/plugins/processlist/__init__.py +++ b/glances/plugins/processlist/__init__.py @@ -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)) diff --git a/glances/webserver.py b/glances/webserver.py index c6da8cba..44c24ec2 100644 --- a/glances/webserver.py +++ b/glances/webserver.py @@ -2,17 +2,17 @@ # # This file is part of Glances. # -# SPDX-FileCopyrightText: 2022 Nicolas Hennion +# SPDX-FileCopyrightText: 2023 Nicolas Hennion # # 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.""" diff --git a/optional-requirements.txt b/optional-requirements.txt index 0de8809c..ef1cb735 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -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" diff --git a/requirements.txt b/requirements.txt index 355233dc..7e241a84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index 092a3a82..6049b513 100755 --- a/setup.py +++ b/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', diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 4af96e96..7185846c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -53,16 +53,28 @@ parts: override-pull: | snapcraftctl pull "$SNAPCRAFT_STAGE"/scriptlets/selective-checkout - bottle: + + fastapi: + plugin: python + 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/fastapi-dist + organize: + fastapi-dist: fastapi/dist + + uvicorn: plugin: python - source: https://github.com/bottlepy/bottle.git - source-branch: release-0.12 + 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/bottle-dist + cp -r $SNAPCRAFT_PART_BUILD/dist $SNAPCRAFT_PART_INSTALL/uvicorn-dist organize: - bottle-dist: bottle/dist + uvicorn-dist: uvicorn/dist docker: plugin: python diff --git a/tox.ini b/tox.ini index 6c9d454c..30a67b06 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,8 @@ deps = defusedxml packaging ujson - bottle + fastapi + uvicorn requests commands = python unitest.py diff --git a/webui-requirements.txt b/webui-requirements.txt index 465b985d..309e06cf 100644 --- a/webui-requirements.txt +++ b/webui-requirements.txt @@ -1,4 +1,5 @@ # install with base requirements file -r requirements.txt -bottle +fastapi; python_version >= "3.8" +uvicorn; python_version >= "3.8" -- cgit v1.2.3