diff --git a/glances/client_browser.py b/glances/client_browser.py index 9043c380..304c61ab 100644 --- a/glances/client_browser.py +++ b/glances/client_browser.py @@ -8,31 +8,13 @@ """Manage the Glances client browser (list of Glances server).""" -import threading import webbrowser -from defusedxml import xmlrpc - -from glances import __apiversion__ -from glances.client import GlancesClient, GlancesClientTransport -from glances.globals import json_loads +from glances.client import GlancesClient from glances.logger import LOG_FILENAME, logger from glances.outputs.glances_curses_browser import GlancesCursesBrowser -from glances.password_list import GlancesPasswordList as GlancesPassword from glances.servers_list import GlancesServersList -try: - import requests -except ImportError as e: - import_requests_error_tag = True - # Display debug message if import error - logger.warning(f"Missing Python Lib ({e}), Client browser will not grab stats from Glances REST server") -else: - import_requests_error_tag = False - -# Correct issue #1025 by monkey path the xmlrpc lib -xmlrpc.monkey_patch() - class GlancesClientBrowser: """This class creates and manages the TCP client browser (servers list).""" @@ -42,127 +24,12 @@ class GlancesClientBrowser: self.args = args self.config = config - # Load the configuration file - self.password = None - self.load() - # Init the server list self.servers_list = GlancesServersList(config=config, args=args) # Init screen self.screen = GlancesCursesBrowser(args=self.args) - def load(self): - # Init the password list (if defined) - self.password = GlancesPassword(config=self.config) - - def get_servers_list(self): - """Return the current server list (list of dict). - - Merge of static + autodiscover servers list. - """ - return self.servers_list.get_servers_list() - - def __get_uri(self, server): - """Return the URI for the given server dict.""" - # Select the connection mode (with or without password) - if server['password'] != "": - if server['status'] == 'PROTECTED': - # Try with the preconfigure password (only if status is PROTECTED) - clear_password = self.password.get_password(server['name']) - if clear_password is not None: - server['password'] = self.password.get_hash(clear_password) - uri = 'http://{}:{}@{}:{}'.format(server['username'], server['password'], server['ip'], server['port']) - else: - uri = 'http://{}:{}'.format(server['ip'], server['port']) - return uri - - def __get_key(self, column): - server_key = column.get('plugin') + '_' + column.get('field') - if 'key' in column: - server_key += '_' + column.get('key') - return server_key - - def __update_stats_rpc(self, uri, server): - # Try to connect to the server - t = GlancesClientTransport() - t.set_timeout(3) - - # Get common stats from Glances server - try: - s = xmlrpc.xmlrpc_client.ServerProxy(uri, transport=t) - except Exception as e: - logger.warning(f"Client browser couldn't create socket ({e})") - return server - - # Get the stats - for column in self.servers_list.get_columns(): - server_key = self.__get_key(column) - try: - # Value - v_json = json_loads(s.getPlugin(column['plugin'])) - if 'key' in column: - v_json = [i for i in v_json if i[i['key']].lower() == column['key'].lower()][0] - server[server_key] = v_json[column['field']] - # Decoration - d_json = json_loads(s.getPluginView(column['plugin'])) - if 'key' in column: - d_json = d_json.get(column['key']) - server[server_key + '_decoration'] = d_json[column['field']]['decoration'] - except (KeyError, IndexError, xmlrpc.xmlrpc_client.Fault) as e: - logger.debug(f"Error while grabbing stats form server ({e})") - except OSError as e: - logger.debug(f"Error while grabbing stats form server ({e})") - server['status'] = 'OFFLINE' - except xmlrpc.xmlrpc_client.ProtocolError as e: - if e.errcode == 401: - # Error 401 (Authentication failed) - # Password is not the good one... - server['password'] = None - server['status'] = 'PROTECTED' - else: - server['status'] = 'OFFLINE' - logger.debug(f"Cannot grab stats from server ({e.errcode} {e.errmsg})") - else: - # Status - server['status'] = 'ONLINE' - - return server - - def __update_stats_rest(self, uri, server): - try: - requests.get(f'{uri}/status', timeout=3) - except requests.exceptions.RequestException as e: - logger.debug(f"Error while grabbing stats form server ({e})") - server['status'] = 'OFFLINE' - return server - else: - server['status'] = 'ONLINE' - - for column in self.servers_list.get_columns(): - server_key = self.__get_key(column) - try: - r = requests.get(f'{uri}/{column['plugin']}/{column['field']}', timeout=3) - except requests.exceptions.RequestException as e: - logger.debug(f"Error while grabbing stats form server ({e})") - return server - else: - server[server_key] = r.json()[column['field']] - - return server - - def __update_stats(self, server): - """Update stats for the given server (picked from the server list)""" - # Get the server URI - uri = self.__get_uri(server) - - if server['protocol'].lower() == 'rpc': - self.__update_stats_rpc(uri, server) - elif server['protocol'].lower() == 'rest' and not import_requests_error_tag: - self.__update_stats_rest(f'{uri}/api/{__apiversion__}', server) - - return server - def __display_server(self, server): """Connect and display the given server""" # Display the Glances client for the selected server @@ -174,7 +41,7 @@ class GlancesClientBrowser: 'Open the WebUI {}:{} in a Web Browser'.format(server['name'], server['port']), duration=1 ) # Try to open a Webbrowser - webbrowser.open(self.__get_uri(server), new=2, autoraise=1) + webbrowser.open(self.servers_list.get_uri(server), new=2, autoraise=1) self.screen.active_server = None return @@ -185,8 +52,11 @@ class GlancesClientBrowser: # A password is needed to access to the server's stats if server['password'] is None: # First of all, check if a password is available in the [passwords] section - clear_password = self.password.get_password(server['name']) - if clear_password is None or self.get_servers_list()[self.screen.active_server]['status'] == 'PROTECTED': + clear_password = self.servers_list.password.get_password(server['name']) + if ( + clear_password is None + or self.servers_list.get_servers_list()[self.screen.active_server]['status'] == 'PROTECTED' + ): # Else, the password should be enter by the user # Display a popup to enter password clear_password = self.screen.display_popup( @@ -194,7 +64,7 @@ class GlancesClientBrowser: ) # Store the password for the selected server if clear_password is not None: - self.set_in_selected('password', self.password.get_hash(clear_password)) + self.set_in_selected('password', self.servers_list.password.get_hash(clear_password)) # Display the Glance client on the selected server logger.info("Connect Glances client to the {} server".format(server['key'])) @@ -240,31 +110,16 @@ class GlancesClientBrowser: def __serve_forever(self): """Main client loop.""" # No need to update the server list - # It's done by the GlancesAutoDiscoverListener class (autodiscover.py) - # Or define statically in the configuration file (module static_list.py) - # For each server in the list, grab elementary stats (CPU, LOAD, MEM, OS...) - thread_list = {} while not self.screen.is_end: - logger.debug(f"Iter through the following server list: {self.get_servers_list()}") - for v in self.get_servers_list(): - key = v["key"] - thread = thread_list.get(key, None) - if thread is None or thread.is_alive() is False: - thread = threading.Thread(target=self.__update_stats, args=[v]) - thread_list[key] = thread - thread.start() + # Update the stats in the servers list + self.servers_list.update_servers_stats() - # Update the screen (list or Glances client) if self.screen.active_server is None: - # Display the Glances browser - self.screen.update(self.get_servers_list()) + # Display Glances browser (servers list) + self.screen.update(self.servers_list.get_servers_list()) else: - # Display the active server - self.__display_server(self.get_servers_list()[self.screen.active_server]) - - # exit key pressed - for thread in thread_list.values(): - thread.join() + # Display selected Glances server + self.__display_server(self.servers_list.get_servers_list()[self.screen.active_server]) def serve_forever(self): """Wrapper to the serve_forever function. diff --git a/glances/outputs/glances_restful_api.py b/glances/outputs/glances_restful_api.py index 554938dc..0e2c3461 100644 --- a/glances/outputs/glances_restful_api.py +++ b/glances/outputs/glances_restful_api.py @@ -170,7 +170,13 @@ class GlancesRestfulApi: self.url_prefix = self.url_prefix.rstrip('/') logger.debug(f'URL prefix: {self.url_prefix}') - def __update__(self): + def __update_stats(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 __update_servers_list(self): # Never update more than 1 time per cached_time if self.timer.finished(): self.stats.update() @@ -210,6 +216,7 @@ class GlancesRestfulApi: f'{base_path}/all/limits': self._api_all_limits, f'{base_path}/all/views': self._api_all_views, f'{base_path}/pluginslist': self._api_plugins, + f'{base_path}/serverslist': self._api_servers_list, f'{plugin_path}': self._api, f'{plugin_path}/history': self._api_history, f'{plugin_path}/history/{{nb}}': self._api_history, @@ -310,7 +317,7 @@ class GlancesRestfulApi: refresh_time = request.query_params.get('refresh', default=max(1, int(self.args.time))) # Update the stat - self.__update__() + self.__update_stats() # Display return self._templates.TemplateResponse("index.html", {"request": request, "refresh_time": refresh_time}) @@ -375,7 +382,7 @@ class GlancesRestfulApi: HTTP/1.1 404 Not Found """ # Update the stat - self.__update__() + self.__update_stats() try: plist = self.plugins_list @@ -384,6 +391,14 @@ class GlancesRestfulApi: return GlancesJSONResponse(plist) + def _api_servers_list(self): + """Glances API RESTful implementation. + + Return the JSON representation of the servers list (for browser mode) + HTTP/200 if OK + """ + self.__update_servers_list() + def _api_all(self): """Glances API RESTful implementation. @@ -401,7 +416,7 @@ class GlancesRestfulApi: logger.debug(f"Debug file ({fname}) not found") # Update the stat - self.__update__() + self.__update_stats() try: # Get the RAW value of the stat ID @@ -454,7 +469,7 @@ class GlancesRestfulApi: self._check_if_plugin_available(plugin) # Update the stat - self.__update__() + self.__update_stats() try: # Get the RAW value of the stat ID @@ -485,7 +500,7 @@ class GlancesRestfulApi: self._check_if_plugin_available(plugin) # Update the stat - self.__update__() + self.__update_stats() try: # Get the RAW value of the stat ID @@ -512,7 +527,7 @@ class GlancesRestfulApi: self._check_if_plugin_available(plugin) # Update the stat - self.__update__() + self.__update_stats() try: # Get the RAW value of the stat ID @@ -572,7 +587,7 @@ class GlancesRestfulApi: self._check_if_plugin_available(plugin) # Update the stat - self.__update__() + self.__update_stats() try: # Get the RAW value of the stat views @@ -597,7 +612,7 @@ class GlancesRestfulApi: self._check_if_plugin_available(plugin) # Update the stat - self.__update__() + self.__update_stats() try: # Get the RAW value of the stat history @@ -656,7 +671,7 @@ class GlancesRestfulApi: self._check_if_plugin_available(plugin) # Update the stat - self.__update__() + self.__update_stats() try: # Get the RAW value diff --git a/glances/servers_list.py b/glances/servers_list.py index 54d079cd..48679a03 100644 --- a/glances/servers_list.py +++ b/glances/servers_list.py @@ -8,9 +8,30 @@ """Manage the servers list used in TUI and WEBUI Central Browser mode""" +import threading + +from defusedxml import xmlrpc + +from glances import __apiversion__ +from glances.client import GlancesClientTransport +from glances.globals import json_loads +from glances.logger import logger +from glances.password_list import GlancesPasswordList as GlancesPassword from glances.servers_list_dynamic import GlancesAutoDiscoverServer from glances.servers_list_static import GlancesStaticServer +try: + import requests +except ImportError as e: + import_requests_error_tag = True + # Display debug message if import error + logger.warning(f"Missing Python Lib ({e}), Client browser will not grab stats from Glances REST server") +else: + import_requests_error_tag = False + +# Correct issue #1025 by monkey path the xmlrpc lib +xmlrpc.monkey_patch() + class GlancesServersList: def __init__(self, config=None, args=None): @@ -18,8 +39,9 @@ class GlancesServersList: self.args = args self.config = config - # Init the servers list defined in the Glances configuration file + # Init the servers and passwords list defined in the Glances configuration file self.static_server = None + self.password = None self.load() # Init the dynamic servers list by starting a Zeroconf listener @@ -27,10 +49,16 @@ class GlancesServersList: if not self.args.disable_autodiscover: self.autodiscover_server = GlancesAutoDiscoverServer() + # Stats are updated in thread + # Create a dict of threads + self.threads_list = {} + def load(self): """Load server and password list from the configuration file.""" # Init the static server list self.static_server = GlancesStaticServer(config=self.config) + # Init the password list (if defined) + self.password = GlancesPassword(config=self.config) def get_servers_list(self): """Return the current server list (list of dict). @@ -46,9 +74,33 @@ class GlancesServersList: return ret + def update_servers_stats(self): + """For each server in the servers list, update the stats""" + for v in self.get_servers_list(): + key = v["key"] + thread = self.threads_list.get(key, None) + if thread is None or thread.is_alive() is False: + thread = threading.Thread(target=self.__update_stats, args=[v]) + self.threads_list[key] = thread + thread.start() + def get_columns(self): return self.static_server.get_columns() + def get_uri(self, server): + """Return the URI for the given server dict.""" + # Select the connection mode (with or without password) + if server['password'] != "": + if server['status'] == 'PROTECTED': + # Try with the preconfigure password (only if status is PROTECTED) + clear_password = self.password.get_password(server['name']) + if clear_password is not None: + server['password'] = self.password.get_hash(clear_password) + uri = 'http://{}:{}@{}:{}'.format(server['username'], server['password'], server['ip'], server['port']) + else: + uri = 'http://{}:{}'.format(server['ip'], server['port']) + return uri + def set_in_selected(self, selected, key, value): """Set the (key, value) for the selected server in the list.""" # Static list then dynamic one @@ -56,3 +108,89 @@ class GlancesServersList: self.autodiscover_server.set_server(selected - len(self.static_server.get_servers_list()), key, value) else: self.static_server.set_server(selected, key, value) + + def __update_stats(self, server): + """Update stats for the given server (picked from the server list)""" + # Get the server URI + uri = self.get_uri(server) + + if server['protocol'].lower() == 'rpc': + self.__update_stats_rpc(uri, server) + elif server['protocol'].lower() == 'rest' and not import_requests_error_tag: + self.__update_stats_rest(f'{uri}/api/{__apiversion__}', server) + + return server + + def __update_stats_rpc(self, uri, server): + # Try to connect to the server + t = GlancesClientTransport() + t.set_timeout(3) + + # Get common stats from Glances server + try: + s = xmlrpc.xmlrpc_client.ServerProxy(uri, transport=t) + except Exception as e: + logger.warning(f"Client browser couldn't create socket ({e})") + return server + + # Get the stats + for column in self.static_server.get_columns(): + server_key = self.__get_key(column) + try: + # Value + v_json = json_loads(s.getPlugin(column['plugin'])) + if 'key' in column: + v_json = [i for i in v_json if i[i['key']].lower() == column['key'].lower()][0] + server[server_key] = v_json[column['field']] + # Decoration + d_json = json_loads(s.getPluginView(column['plugin'])) + if 'key' in column: + d_json = d_json.get(column['key']) + server[server_key + '_decoration'] = d_json[column['field']]['decoration'] + except (KeyError, IndexError, xmlrpc.xmlrpc_client.Fault) as e: + logger.debug(f"Error while grabbing stats form server ({e})") + except OSError as e: + logger.debug(f"Error while grabbing stats form server ({e})") + server['status'] = 'OFFLINE' + except xmlrpc.xmlrpc_client.ProtocolError as e: + if e.errcode == 401: + # Error 401 (Authentication failed) + # Password is not the good one... + server['password'] = None + server['status'] = 'PROTECTED' + else: + server['status'] = 'OFFLINE' + logger.debug(f"Cannot grab stats from server ({e.errcode} {e.errmsg})") + else: + # Status + server['status'] = 'ONLINE' + + return server + + def __update_stats_rest(self, uri, server): + try: + requests.get(f'{uri}/status', timeout=3) + except requests.exceptions.RequestException as e: + logger.debug(f"Error while grabbing stats form server ({e})") + server['status'] = 'OFFLINE' + return server + else: + server['status'] = 'ONLINE' + + for column in self.static_server.get_columns(): + server_key = self.__get_key(column) + try: + r = requests.get(f'{uri}/{column['plugin']}/{column['field']}', timeout=3) + except requests.exceptions.RequestException as e: + logger.debug(f"Error while grabbing stats form server ({e})") + return server + else: + server[server_key] = r.json()[column['field']] + + return server + + def __get_key(self, column): + server_key = column.get('plugin') + '_' + column.get('field') + if 'key' in column: + server_key += '_' + column.get('key') + return server_key