diff options
Diffstat (limited to 'glances')
-rw-r--r-- | glances/__init__.py | 10 | ||||
-rw-r--r-- | glances/autodiscover.py | 5 | ||||
-rw-r--r-- | glances/client.py | 10 | ||||
-rw-r--r-- | glances/client_browser.py | 6 | ||||
-rw-r--r-- | glances/config.py | 15 | ||||
-rw-r--r-- | glances/exports/glances_riemann.py | 92 | ||||
-rw-r--r-- | glances/globals.py | 5 | ||||
-rw-r--r-- | glances/main.py | 11 | ||||
-rw-r--r-- | glances/outputs/glances_bottle.py | 51 | ||||
-rw-r--r-- | glances/outputs/glances_curses.py | 252 | ||||
-rw-r--r-- | glances/outputs/glances_curses_browser.py | 273 | ||||
-rw-r--r-- | glances/password.py | 5 | ||||
-rw-r--r-- | glances/plugins/glances_help.py | 4 | ||||
-rw-r--r-- | glances/plugins/glances_psutilversion.py | 6 | ||||
-rw-r--r-- | glances/plugins/glances_sensors.py | 2 | ||||
-rw-r--r-- | glances/processes.py | 197 | ||||
-rw-r--r-- | glances/processes_tree.py | 217 | ||||
-rw-r--r-- | glances/server.py | 6 | ||||
-rw-r--r-- | glances/static_list.py | 2 | ||||
-rw-r--r-- | glances/stats.py | 173 | ||||
-rw-r--r-- | glances/stats_client.py | 68 | ||||
-rw-r--r-- | glances/stats_client_snmp.py | 109 | ||||
-rw-r--r-- | glances/stats_server.py | 68 |
23 files changed, 904 insertions, 683 deletions
diff --git a/glances/__init__.py b/glances/__init__.py index c937e820..e381f305 100644 --- a/glances/__init__.py +++ b/glances/__init__.py @@ -27,13 +27,13 @@ import sys # Global name __appname__ = 'glances' -__version__ = '2.6' +__version__ = '2.6.1' __author__ = 'Nicolas Hennion <nicolas@nicolargo.com>' __license__ = 'LGPL' # Import psutil try: - from psutil import __version__ as __psutil_version + from psutil import __version__ as psutil_version except ImportError: print('PSutil library not found. Glances cannot start.') sys.exit(1) @@ -62,8 +62,8 @@ if sys.version_info[:2] == (2, 6): # Check PSutil version psutil_min_version = (2, 0, 0) -psutil_version = tuple([int(num) for num in __psutil_version.split('.')]) -if psutil_version < psutil_min_version: +psutil_version_info = tuple([int(num) for num in psutil_version.split('.')]) +if psutil_version_info < psutil_min_version: print('PSutil 2.0 or higher is needed. Glances cannot start.') sys.exit(1) @@ -107,7 +107,7 @@ def main(): logger.info('{0} {1} and PSutil {2} detected'.format( platform.python_implementation(), platform.python_version(), - __psutil_version)) + psutil_version)) # Share global var global core, standalone, client, server, webserver diff --git a/glances/autodiscover.py b/glances/autodiscover.py index fa1414d4..b2f2fcb9 100644 --- a/glances/autodiscover.py +++ b/glances/autodiscover.py @@ -22,7 +22,8 @@ import socket import sys -from glances.globals import appname, BSD +from glances import __appname__ +from glances.globals import BSD from glances.logger import logger try: @@ -46,7 +47,7 @@ if zeroconf_tag: sys.exit(1) # Global var -zeroconf_type = "_%s._tcp." % appname +zeroconf_type = "_%s._tcp." % __appname__ class AutoDiscovered(object): diff --git a/glances/client.py b/glances/client.py index 2dc33bed..a1e68e0b 100644 --- a/glances/client.py +++ b/glances/client.py @@ -23,10 +23,10 @@ import json import socket import sys +from glances import __version__ from glances.compat import Fault, ProtocolError, ServerProxy, Transport -from glances.globals import version from glances.logger import logger -from glances.stats import GlancesStatsClient +from glances.stats_client import GlancesStatsClient from glances.outputs.glances_curses import GlancesCursesClient @@ -122,11 +122,11 @@ class GlancesClient(object): if self.client_mode == 'glances': # Check that both client and server are in the same major version - if version.split('.')[0] == client_version.split('.')[0]: + if __version__.split('.')[0] == client_version.split('.')[0]: # Init stats self.stats = GlancesStatsClient(config=self.config, args=self.args) self.stats.set_plugins(json.loads(self.client.getAllPlugins())) - logger.debug("Client version: {0} / Server version: {1}".format(version, client_version)) + logger.debug("Client version: {0} / Server version: {1}".format(__version__, client_version)) else: self.log_and_exit("Client and server not compatible: \ Client version: {0} / Server version: {1}".format(version, client_version)) @@ -139,7 +139,7 @@ class GlancesClient(object): if self.client_mode == 'snmp': logger.info("Trying to grab stats by SNMP...") - from glances.stats import GlancesStatsClientSNMP + from glances.stats_client_snmp import GlancesStatsClientSNMP # Init stats self.stats = GlancesStatsClientSNMP(config=self.config, args=self.args) diff --git a/glances/client_browser.py b/glances/client_browser.py index cfcf01a2..ef1348d0 100644 --- a/glances/client_browser.py +++ b/glances/client_browser.py @@ -2,7 +2,7 @@ # # This file is part of Glances. # -# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com> +# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com> # # Glances is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by @@ -28,7 +28,7 @@ from glances.client import GlancesClient, GlancesClientTransport from glances.logger import logger from glances.password_list import GlancesPasswordList as GlancesPassword from glances.static_list import GlancesStaticServer -from glances.outputs.glances_curses import GlancesCursesBrowser +from glances.outputs.glances_curses_browser import GlancesCursesBrowser class GlancesClientBrowser(object): @@ -160,7 +160,7 @@ class GlancesClientBrowser(object): "Server list dictionnary change inside the loop (wait next update)") # Update the screen (list or Glances client) - if not self.screen.active_server: + if self.screen.active_server is None: # Display the Glances browser self.screen.update(self.get_servers_list()) else: diff --git a/glances/config.py b/glances/config.py index 3e8570ea..8c53a9ca 100644 --- a/glances/config.py +++ b/glances/config.py @@ -23,8 +23,9 @@ import os import sys from io import open +from glances import __appname__ from glances.compat import ConfigParser, NoOptionError -from glances.globals import appname, BSD, LINUX, OSX, WINDOWS, sys_prefix +from glances.globals import BSD, LINUX, OSX, WINDOWS, sys_prefix from glances.logger import logger @@ -69,22 +70,22 @@ class Config(object): paths.append( os.path.join(os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'), - appname, self.config_filename)) + __appname__, self.config_filename)) if BSD: paths.append( - os.path.join(sys.prefix, 'etc', appname, self.config_filename)) + os.path.join(sys.prefix, 'etc', __appname__, self.config_filename)) else: paths.append( - os.path.join('/etc', appname, self.config_filename)) + os.path.join('/etc', __appname__, self.config_filename)) elif OSX: paths.append( os.path.join(os.path.expanduser('~/Library/Application Support/'), - appname, self.config_filename)) + __appname__, self.config_filename)) paths.append( - os.path.join(sys_prefix, 'etc', appname, self.config_filename)) + os.path.join(sys_prefix, 'etc', __appname__, self.config_filename)) elif WINDOWS: paths.append( - os.path.join(os.environ.get('APPDATA'), appname, self.config_filename)) + os.path.join(os.environ.get('APPDATA'), __appname__, self.config_filename)) return paths diff --git a/glances/exports/glances_riemann.py b/glances/exports/glances_riemann.py new file mode 100644 index 00000000..a6ec1465 --- /dev/null +++ b/glances/exports/glances_riemann.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com> +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Riemann interface class.""" + +import socket +import sys +from numbers import Number + +from glances.compat import NoOptionError, NoSectionError, range +from glances.logger import logger +from glances.exports.glances_export import GlancesExport + +# Import pika for Riemann +import bernhard + + +class Export(GlancesExport): + + """This class manages the Riemann export module.""" + + def __init__(self, config=None, args=None): + """Init the Riemann export IF.""" + super(Export, self).__init__(config=config, args=args) + + # Load the rabbitMQ configuration file + self.riemann_host = None + self.riemann_port = None + self.hostname = socket.gethostname() + self.export_enable = self.load_conf() + if not self.export_enable: + sys.exit(2) + + # Init the rabbitmq client + self.client = self.init() + + def load_conf(self, section="riemann"): + """Load the Riemann configuration in the Glances configuration file.""" + if self.config is None: + return False + try: + self.riemann_host = self.config.get_value(section, 'host') + self.riemann_port = int(self.config.get_value(section, 'port')) + except NoSectionError: + logger.critical("No riemann configuration found") + return False + except NoOptionError as e: + logger.critical("Error in the Riemann configuration (%s)" % e) + return False + else: + logger.debug("Load Riemann from the Glances configuration file") + return True + + def init(self): + """Init the connection to the Riemann server.""" + if not self.export_enable: + return None + try: + client = bernhard.Client(host=self.riemann_host, port=self.riemann_port) + return client + except Exception as e: + logger.critical("Connection to Riemann failed : %s " % e) + return None + + def export(self, name, columns, points): + """Write the points in Riemann.""" + for i in range(len(columns)): + if not isinstance(points[i], Number): + continue + else: + data = {'host': self.hostname, 'service': name + " " + columns[i], 'metric': points[i]} + logger.debug(data) + try: + self.client.send(data) + except Exception as e: + logger.error("Can not export stats to Riemann (%s)" % e) diff --git a/glances/globals.py b/glances/globals.py index 8eba5090..6b952a76 100644 --- a/glances/globals.py +++ b/glances/globals.py @@ -22,11 +22,6 @@ import os import sys -# Global information -appname = 'glances' -version = __import__('glances').__version__ -psutil_version = __import__('glances').__psutil_version - # Operating system flag # Note: Somes libs depends of OS BSD = sys.platform.find('bsd') != -1 diff --git a/glances/main.py b/glances/main.py index a8234295..9be00fcb 100644 --- a/glances/main.py +++ b/glances/main.py @@ -24,9 +24,10 @@ import os import sys import tempfile +from glances import __appname__, __version__, psutil_version from glances.compat import input from glances.config import Config -from glances.globals import appname, LINUX, WINDOWS, psutil_version, version +from glances.globals import LINUX, WINDOWS from glances.logger import logger @@ -86,14 +87,14 @@ Start the client browser (browser mode):\n\ def init_args(self): """Init all the command line arguments.""" - _version = "Glances v" + version + " with psutil v" + psutil_version + version = "Glances v" + __version__ + " with psutil v" + psutil_version parser = argparse.ArgumentParser( - prog=appname, + prog=__appname__, conflict_handler='resolve', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=self.example_of_use) parser.add_argument( - '-V', '--version', action='version', version=_version) + '-V', '--version', action='version', version=version) parser.add_argument('-d', '--debug', action='store_true', default=False, dest='debug', help='enable debug mode') parser.add_argument('-C', '--config', dest='conf_file', @@ -162,6 +163,8 @@ Start the client browser (browser mode):\n\ dest='export_elasticsearch', help='export stats to an ElasticSearch server (elasticsearch lib needed)') parser.add_argument('--export-rabbitmq', action='store_true', default=False, dest='export_rabbitmq', help='export stats to rabbitmq broker (pika lib needed)') + parser.add_argument('--export-riemann', action='store_true', default=False, + dest='export_riemann', help='export stats to riemann broker (bernhard lib needed)') # Client/Server option parser.add_argument('-c', '--client', dest='client', help='connect to a Glances server by IPv4/IPv6 address or hostname') diff --git a/glances/outputs/glances_bottle.py b/glances/outputs/glances_bottle.py index 053a1175..5ea21873 100644 --- a/glances/outputs/glances_bottle.py +++ b/glances/outputs/glances_bottle.py @@ -333,15 +333,8 @@ class GlancesBottle(object): abort(404, "Cannot get views for plugin %s (%s)" % (plugin, str(e))) return ret - def _api_item(self, plugin, item): - """Glances API RESTFul implementation. - - Return the JSON represenation of the couple plugin/item - HTTP/200 if OK - HTTP/400 if plugin is not found - HTTP/404 if others error - - """ + def _api_itemvalue(self, plugin, item, value=None): + """ Father method for _api_item and _api_value""" response.content_type = 'application/json' if plugin not in self.plugins_list: @@ -350,35 +343,39 @@ class GlancesBottle(object): # Update the stat self.stats.update() - plist = self.stats.get_plugin(plugin).get_stats_item(item) + if value is None: + ret = self.stats.get_plugin(plugin).get_stats_item(item) - if plist is None: - abort(404, "Cannot get item %s in plugin %s" % (item, plugin)) + if ret is None: + abort(404, "Cannot get item %s in plugin %s" % (item, plugin)) else: - return plist + ret = self.stats.get_plugin(plugin).get_stats_value(item, value) - def _api_value(self, plugin, item, value): + if ret is None: + abort(404, "Cannot get item(%s)=value(%s) in plugin %s" % (item, value, plugin)) + + return ret + + def _api_item(self, plugin, item): """Glances API RESTFul implementation. - Return the process stats (dict) for the given item=value + Return the JSON represenation of the couple plugin/item HTTP/200 if OK HTTP/400 if plugin is not found HTTP/404 if others error - """ - response.content_type = 'application/json' - - if plugin not in self.plugins_list: - abort(400, "Unknown plugin %s (available plugins: %s)" % (plugin, self.plugins_list)) - # Update the stat - self.stats.update() + """ + return self._api_itemvalue(plugin, item) - pdict = self.stats.get_plugin(plugin).get_stats_value(item, value) + def _api_value(self, plugin, item, value): + """Glances API RESTFul implementation. - if pdict is None: - abort(404, "Cannot get item(%s)=value(%s) in plugin %s" % (item, value, plugin)) - else: - return pdict + 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) def _api_args(self): """Glances API RESTFul implementation. diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 4d5934c8..f86f1175 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -2,7 +2,7 @@ # # This file is part of Glances. # -# Copyright (C) 2015 Nicolargo <nicolas@nicolargo.com> +# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com> # # Glances is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by @@ -846,7 +846,7 @@ class _GlancesCurses(object): else: # Display the popup popup.refresh() - curses.napms(duration * 1000) + self.wait(duration * 1000) return True def display_plugin(self, plugin_stats, @@ -986,10 +986,14 @@ class _GlancesCurses(object): # Redraw display self.flush(stats, cs_status=cs_status) # Wait 100ms... - curses.napms(100) + self.wait() return exitkey + def wait(self, delay=100): + """Wait delay in ms""" + curses.napms(100) + def get_stats_display_width(self, curse_msg, without_option=False): """Return the width of the formatted curses message. @@ -1036,248 +1040,6 @@ class GlancesCursesClient(_GlancesCurses): pass -class GlancesCursesBrowser(_GlancesCurses): - - """Class for the Glances curse client browser.""" - - def __init__(self, args=None): - # Init the father class - super(GlancesCursesBrowser, self).__init__(args=args) - - _colors_list = { - 'UNKNOWN': self.no_color, - 'SNMP': self.default_color2, - 'ONLINE': self.default_color2, - 'OFFLINE': self.ifCRITICAL_color2, - 'PROTECTED': self.ifWARNING_color2, - } - self.colors_list.update(_colors_list) - - # First time scan tag - # Used to display a specific message when the browser is started - self.first_scan = True - - # Init refresh time - self.__refresh_time = args.time - - # Init the cursor position for the client browser - self.cursor_position = 0 - - # Active Glances server number - self._active_server = None - - @property - def active_server(self): - """Return the active server or None if it's the browser list.""" - return self._active_server - - @active_server.setter - def active_server(self, index): - """Set the active server or None if no server selected.""" - self._active_server = index - - @property - def cursor(self): - """Get the cursor position.""" - return self.cursor_position - - @cursor.setter - def cursor(self, position): - """Set the cursor position.""" - self.cursor_position = position - - def cursor_up(self, servers_list): - """Set the cursor to position N-1 in the list.""" - if self.cursor_position > 0: - self.cursor_position -= 1 - else: - self.cursor_position = len(servers_list) - 1 - - def cursor_down(self, servers_list): - """Set the cursor to position N-1 in the list.""" - if self.cursor_position < len(servers_list) - 1: - self.cursor_position += 1 - else: - self.cursor_position = 0 - - def __catch_key(self, servers_list): - # Catch the browser pressed key - self.pressedkey = self.get_key(self.term_window) - - # Actions... - if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'): - # 'ESC'|'q' > Quit - self.end() - logger.info("Stop Glances client browser") - sys.exit(0) - elif self.pressedkey == 10: - # 'ENTER' > Run Glances on the selected server - logger.debug("Server number {0} selected".format(self.cursor + 1)) - self.active_server = self.cursor - elif self.pressedkey == 259: - # 'UP' > Up in the server list - self.cursor_up(servers_list) - elif self.pressedkey == 258: - # 'DOWN' > Down in the server list - self.cursor_down(servers_list) - - # Return the key code - return self.pressedkey - - def update(self, servers_list): - """Update the servers' list screen. - - Wait for __refresh_time sec / catch key every 100 ms. - - servers_list: Dict of dict with servers stats - """ - # Flush display - logger.debug('Servers list: {0}'.format(servers_list)) - self.flush(servers_list) - - # Wait - exitkey = False - countdown = Timer(self.__refresh_time) - while not countdown.finished() and not exitkey: - # Getkey - pressedkey = self.__catch_key(servers_list) - # Is it an exit or select server key ? - exitkey = ( - pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10) - if not exitkey and pressedkey > -1: - # Redraw display - self.flush(servers_list) - # Wait 100ms... - curses.napms(100) - - return self.active_server - - def flush(self, servers_list): - """Update the servers' list screen. - - servers_list: List of dict with servers stats - """ - self.erase() - self.display(servers_list) - - def display(self, servers_list): - """Display the servers list. - - Return: - True if the stats have been displayed - False if the stats have not been displayed (no server available) - """ - # Init the internal line/column for Glances Curses - self.init_line_column() - - # Get the current screen size - screen_x = self.screen.getmaxyx()[1] - screen_y = self.screen.getmaxyx()[0] - - # Init position - x = 0 - y = 0 - - # Display top header - if len(servers_list) == 0: - if self.first_scan and not self.args.disable_autodiscover: - msg = 'Glances is scanning your network. Please wait...' - self.first_scan = False - else: - msg = 'No Glances server available' - elif len(servers_list) == 1: - msg = 'One Glances server available' - else: - msg = '{0} Glances servers available'.format(len(servers_list)) - if self.args.disable_autodiscover: - msg += ' ' + '(auto discover is disabled)' - self.term_window.addnstr(y, x, - msg, - screen_x - x, - self.colors_list['TITLE']) - - if len(servers_list) == 0: - return False - - # Display the Glances server list - # ================================ - - # Table of table - # Item description: [stats_id, column name, column size] - column_def = [ - ['name', 'Name', 16], - ['alias', None, None], - ['load_min5', 'LOAD', 6], - ['cpu_percent', 'CPU%', 5], - ['mem_percent', 'MEM%', 5], - ['status', 'STATUS', 9], - ['ip', 'IP', 15], - # ['port', 'PORT', 5], - ['hr_name', 'OS', 16], - ] - y = 2 - - # Display table header - xc = x + 2 - for cpt, c in enumerate(column_def): - if xc < screen_x and y < screen_y and c[1] is not None: - self.term_window.addnstr(y, xc, - c[1], - screen_x - x, - self.colors_list['BOLD']) - xc += c[2] + self.space_between_column - y += 1 - - # If a servers has been deleted from the list... - # ... and if the cursor is in the latest position - if self.cursor > len(servers_list) - 1: - # Set the cursor position to the latest item - self.cursor = len(servers_list) - 1 - - # Display table - line = 0 - for v in servers_list: - # Get server stats - server_stat = {} - for c in column_def: - try: - server_stat[c[0]] = v[c[0]] - except KeyError as e: - logger.debug( - "Cannot grab stats {0} from server (KeyError: {1})".format(c[0], e)) - server_stat[c[0]] = '?' - # Display alias instead of name - try: - if c[0] == 'alias' and v[c[0]] is not None: - server_stat['name'] = v[c[0]] - except KeyError: - pass - - # Display line for server stats - cpt = 0 - xc = x - - # Is the line selected ? - if line == self.cursor: - # Display cursor - self.term_window.addnstr( - y, xc, ">", screen_x - xc, self.colors_list['BOLD']) - - # Display the line - xc += 2 - for c in column_def: - if xc < screen_x and y < screen_y and c[1] is not None: - # Display server stats - self.term_window.addnstr( - y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']]) - xc += c[2] + self.space_between_column - cpt += 1 - # Next line, next server... - y += 1 - line += 1 - - return True - if not WINDOWS: class GlancesTextbox(Textbox, object): diff --git a/glances/outputs/glances_curses_browser.py b/glances/outputs/glances_curses_browser.py new file mode 100644 index 00000000..11543fb9 --- /dev/null +++ b/glances/outputs/glances_curses_browser.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com> +# +# Glances is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Glances is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Curses browser interface class .""" + +import sys + +from glances.outputs.glances_curses import _GlancesCurses + +from glances.logger import logger +from glances.timer import Timer + + +class GlancesCursesBrowser(_GlancesCurses): + + """Class for the Glances curse client browser.""" + + def __init__(self, args=None): + # Init the father class + super(GlancesCursesBrowser, self).__init__(args=args) + + _colors_list = { + 'UNKNOWN': self.no_color, + 'SNMP': self.default_color2, + 'ONLINE': self.default_color2, + 'OFFLINE': self.ifCRITICAL_color2, + 'PROTECTED': self.ifWARNING_color2, + } + self.colors_list.update(_colors_list) + + # First time scan tag + # Used to display a specific message when the browser is started + self.first_scan = True + + # Init refresh time + self.__refresh_time = args.time + + # Init the cursor position for the client browser + self.cursor_position = 0 + + # Active Glances server number + self._active_server = None + + @property + def active_server(self): + """Return the active server or None if it's the browser list.""" + return self._active_server + + @active_server.setter + def active_server(self, index): + """Set the active server or None if no server selected.""" + self._active_server = index + + @property + def cursor(self): + """Get the cursor position.""" + return self.cursor_position + + @cursor.setter + def cursor(self, position): + """Set the cursor position.""" + self.cursor_position = position + + def cursor_up(self, servers_list): + """Set the cursor to position N-1 in the list.""" + if self.cursor_position > 0: + self.cursor_position -= 1 + else: + self.cursor_position = len(servers_list) - 1 + + def cursor_down(self, servers_list): + """Set the cursor to position N-1 in the list.""" + |