Refactor server list code in order to put all the methodin the servers_list.py file

This commit is contained in:
nicolargo 2024-11-02 16:23:31 +01:00
parent 799dcab034
commit e1dd979b86
3 changed files with 178 additions and 170 deletions

View File

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

View File

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

View File

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