1 | # -*- coding: utf-8 -*- |
||
2 | # |
||
3 | # This file is part of Glances. |
||
4 | # |
||
5 | # Copyright (C) 2019 Nicolargo <[email protected]> |
||
6 | # |
||
7 | # Glances is free software; you can redistribute it and/or modify |
||
8 | # it under the terms of the GNU Lesser General Public License as published by |
||
9 | # the Free Software Foundation, either version 3 of the License, or |
||
10 | # (at your option) any later version. |
||
11 | # |
||
12 | # Glances is distributed in the hope that it will be useful, |
||
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
15 | # GNU Lesser General Public License for more details. |
||
16 | # |
||
17 | # You should have received a copy of the GNU Lesser General Public License |
||
18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
19 | |||
20 | """Curses browser interface class .""" |
||
21 | |||
22 | import sys |
||
23 | import math |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
24 | import curses |
||
25 | from glances.outputs.glances_curses import _GlancesCurses |
||
26 | |||
27 | from glances.logger import logger |
||
28 | from glances.timer import Timer |
||
29 | |||
30 | |||
31 | class GlancesCursesBrowser(_GlancesCurses): |
||
32 | """Class for the Glances curse client browser.""" |
||
33 | |||
34 | def __init__(self, args=None): |
||
35 | """Init the father class.""" |
||
36 | super(GlancesCursesBrowser, self).__init__(args=args) |
||
37 | |||
38 | _colors_list = { |
||
39 | 'UNKNOWN': self.no_color, |
||
40 | 'SNMP': self.default_color2, |
||
41 | 'ONLINE': self.default_color2, |
||
42 | 'OFFLINE': self.ifCRITICAL_color2, |
||
43 | 'PROTECTED': self.ifWARNING_color2, |
||
44 | } |
||
45 | self.colors_list.update(_colors_list) |
||
46 | |||
47 | # First time scan tag |
||
48 | # Used to display a specific message when the browser is started |
||
49 | self.first_scan = True |
||
50 | |||
51 | # Init refresh time |
||
52 | self.__refresh_time = args.time |
||
53 | |||
54 | # Init the cursor position for the client browser |
||
55 | self.cursor_position = 0 |
||
56 | |||
57 | # Active Glances server number |
||
58 | self._active_server = None |
||
59 | |||
60 | self._current_page = 0 |
||
61 | self._page_max = 0 |
||
62 | self._page_max_lines = 0 |
||
63 | |||
64 | self.is_end = False |
||
65 | self._revesed_sorting = False |
||
66 | self._stats_list = None |
||
67 | |||
68 | @property |
||
69 | def active_server(self): |
||
70 | """Return the active server or None if it's the browser list.""" |
||
71 | return self._active_server |
||
72 | |||
73 | @active_server.setter |
||
74 | def active_server(self, index): |
||
75 | """Set the active server or None if no server selected.""" |
||
76 | self._active_server = index |
||
77 | |||
78 | @property |
||
79 | def cursor(self): |
||
80 | """Get the cursor position.""" |
||
81 | return self.cursor_position |
||
82 | |||
83 | @cursor.setter |
||
84 | def cursor(self, position): |
||
85 | """Set the cursor position.""" |
||
86 | self.cursor_position = position |
||
87 | |||
88 | def get_pagelines(self, stats): |
||
0 ignored issues
–
show
This method should have a docstring.
The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods: class SomeClass:
def some_method(self):
"""Do x and return foo."""
If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.
Loading history...
|
|||
89 | if self._current_page == self._page_max - 1: |
||
90 | page_lines = len(stats) % self._page_max_lines |
||
91 | else: |
||
92 | page_lines = self._page_max_lines |
||
93 | return page_lines |
||
94 | |||
95 | def _get_status_count(self, stats): |
||
0 ignored issues
–
show
This method could be written as a function/class method.
If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example class Foo:
def some_method(self, x, y):
return x + y;
could be written as class Foo:
@classmethod
def some_method(cls, x, y):
return x + y;
Loading history...
|
|||
96 | counts = {} |
||
97 | for item in stats: |
||
98 | color = item['status'] |
||
99 | counts[color] = counts.get(color, 0) + 1 |
||
100 | |||
0 ignored issues
–
show
|
|||
101 | result = '' |
||
102 | for key in counts.keys(): |
||
0 ignored issues
–
show
|
|||
103 | result += key + ': ' + str(counts[key]) + ' ' |
||
104 | |||
0 ignored issues
–
show
|
|||
105 | return result |
||
106 | |||
107 | def _get_stats(self, stats): |
||
108 | stats_list = None |
||
109 | if self._stats_list is not None: |
||
110 | stats_list = self._stats_list |
||
111 | stats_list.sort(reverse = self._revesed_sorting, |
||
0 ignored issues
–
show
|
|||
112 | key = lambda x: { 'UNKNOWN' : 0, |
||
0 ignored issues
–
show
|
|||
113 | 'OFFLINE' : 1, |
||
114 | 'PROTECTED' : 2, |
||
115 | 'SNMP' : 3, |
||
116 | 'ONLINE': 4 }.get(x['status'], 99)) |
||
0 ignored issues
–
show
|
|||
117 | else: |
||
118 | stats_list = stats |
||
119 | |||
0 ignored issues
–
show
|
|||
120 | return stats_list |
||
121 | |||
0 ignored issues
–
show
|
|||
122 | def cursor_up(self, stats): |
||
123 | """Set the cursor to position N-1 in the list.""" |
||
124 | if 0 <= self.cursor_position - 1: |
||
0 ignored issues
–
show
|
|||
125 | self.cursor_position -= 1 |
||
126 | else: |
||
127 | if self._current_page - 1 < 0 : |
||
0 ignored issues
–
show
|
|||
128 | self._current_page = self._page_max - 1 |
||
129 | self.cursor_position = (len(stats) - 1) % self._page_max_lines |
||
0 ignored issues
–
show
|
|||
130 | else: |
||
131 | self._current_page -= 1 |
||
132 | self.cursor_position = self._page_max_lines - 1 |
||
133 | |||
134 | def cursor_down(self, stats): |
||
135 | """Set the cursor to position N-1 in the list.""" |
||
136 | |||
0 ignored issues
–
show
|
|||
137 | if self.cursor_position + 1 < self.get_pagelines(stats): |
||
138 | self.cursor_position += 1 |
||
139 | else: |
||
140 | if self._current_page + 1 < self._page_max: |
||
141 | self._current_page += 1 |
||
142 | else: |
||
143 | self._current_page = 0 |
||
144 | self.cursor_position = 0 |
||
145 | |||
146 | def cursor_pageup(self, stats): |
||
0 ignored issues
–
show
|
|||
147 | """Set prev page.""" |
||
148 | if self._current_page - 1 < 0: |
||
149 | self._current_page = self._page_max - 1 |
||
150 | else: |
||
151 | self._current_page -= 1 |
||
152 | self.cursor_position = 0 |
||
153 | |||
154 | def cursor_pagedown(self, stats): |
||
0 ignored issues
–
show
|
|||
155 | """Set next page.""" |
||
156 | if self._current_page + 1 < self._page_max: |
||
157 | self._current_page += 1 |
||
158 | else: |
||
159 | self._current_page = 0 |
||
160 | self.cursor_position = 0 |
||
161 | |||
162 | def __catch_key(self, stats): |
||
163 | # Catch the browser pressed key |
||
164 | self.pressedkey = self.get_key(self.term_window) |
||
165 | refresh = False |
||
166 | if self.pressedkey != -1: |
||
167 | logger.debug("Key pressed. Code=%s" % self.pressedkey) |
||
168 | |||
169 | # Actions... |
||
170 | if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'): |
||
171 | # 'ESC'|'q' > Quit |
||
172 | self.end() |
||
173 | logger.info("Stop Glances client browser") |
||
174 | # sys.exit(0) |
||
175 | self.is_end = True |
||
176 | elif self.pressedkey == 10: |
||
177 | # 'ENTER' > Run Glances on the selected server |
||
178 | self.active_server = self._current_page * self._page_max_lines + self.cursor_position |
||
0 ignored issues
–
show
|
|||
179 | logger.debug("Server {}/{} selected".format(self.active_server, len(stats))) |
||
180 | elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65: |
||
181 | # 'UP' > Up in the server list |
||
182 | self.cursor_up(stats) |
||
183 | logger.debug("Server {}/{} selected".format(self.cursor + 1, len(stats))) |
||
184 | elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66: |
||
185 | # 'DOWN' > Down in the server list |
||
186 | self.cursor_down(stats) |
||
187 | logger.debug("Server {}/{} selected".format(self.cursor + 1, len(stats))) |
||
188 | elif self.pressedkey == curses.KEY_PPAGE: |
||
189 | # 'Page UP' > Prev page in the server list |
||
190 | self.cursor_pageup(stats) |
||
191 | logger.debug("PageUP: Server ({}/{}) pages.".format(self._current_page + 1, self._page_max)) |
||
0 ignored issues
–
show
|
|||
192 | elif self.pressedkey == curses.KEY_NPAGE: |
||
193 | # 'Page Down' > Next page in the server list |
||
194 | self.cursor_pagedown(stats) |
||
195 | logger.debug("PageDown: Server {}/{} pages".format(self._current_page + 1, self._page_max)) |
||
0 ignored issues
–
show
|
|||
196 | elif self.pressedkey == ord('1'): |
||
197 | self._stats_list = None |
||
198 | refresh = True |
||
199 | elif self.pressedkey == ord('2'): |
||
200 | self._revesed_sorting = False |
||
0 ignored issues
–
show
|
|||
201 | self._stats_list = stats.copy() |
||
202 | refresh = True |
||
203 | elif self.pressedkey == ord('3'): |
||
204 | self._revesed_sorting = True |
||
205 | self._stats_list = stats.copy() |
||
206 | refresh = True |
||
207 | |||
208 | if refresh: |
||
209 | self._current_page = 0 |
||
210 | self.cursor_position = 0 |
||
211 | self.flush(stats) |
||
212 | |||
213 | # Return the key code |
||
214 | return self.pressedkey |
||
215 | |||
216 | def update(self, |
||
217 | stats, |
||
218 | duration=3, |
||
219 | cs_status=None, |
||
220 | return_to_browser=False): |
||
221 | """Update the servers' list screen. |
||
222 | |||
223 | Wait for __refresh_time sec / catch key every 100 ms. |
||
224 | |||
225 | stats: Dict of dict with servers stats |
||
226 | """ |
||
227 | # Flush display |
||
228 | logger.debug('Servers list: {}'.format(stats)) |
||
229 | self.flush(stats) |
||
230 | |||
231 | # Wait |
||
232 | exitkey = False |
||
233 | countdown = Timer(self.__refresh_time) |
||
234 | while not countdown.finished() and not exitkey: |
||
235 | # Getkey |
||
236 | pressedkey = self.__catch_key(stats) |
||
237 | # Is it an exit or select server key ? |
||
238 | exitkey = ( |
||
239 | pressedkey == ord('\x1b') or pressedkey == ord('q') or pressedkey == 10) |
||
240 | if not exitkey and pressedkey > -1: |
||
241 | # Redraw display |
||
242 | self.flush(stats) |
||
243 | # Wait 100ms... |
||
244 | self.wait() |
||
245 | |||
246 | return self.active_server |
||
247 | |||
248 | def flush(self, stats): |
||
249 | """Update the servers' list screen. |
||
250 | |||
251 | stats: List of dict with servers stats |
||
252 | """ |
||
253 | self.erase() |
||
254 | self.display(stats) |
||
255 | |||
256 | def display(self, stats, cs_status=None): |
||
257 | """Display the servers list. |
||
258 | |||
259 | Return: |
||
260 | True if the stats have been displayed |
||
261 | False if the stats have not been displayed (no server available) |
||
262 | """ |
||
263 | # Init the internal line/column for Glances Curses |
||
264 | self.init_line_column() |
||
265 | |||
266 | # Get the current screen size |
||
267 | screen_x = self.screen.getmaxyx()[1] |
||
268 | screen_y = self.screen.getmaxyx()[0] |
||
269 | stats_max = screen_y - 3 |
||
270 | stats_len = len(stats) |
||
271 | |||
272 | self._page_max_lines = stats_max |
||
273 | self._page_max = int(math.ceil(stats_len / stats_max)) |
||
0 ignored issues
–
show
|
|||
274 | # Init position |
||
275 | x = 0 |
||
276 | y = 0 |
||
277 | |||
278 | # Display top header |
||
279 | if stats_len == 0: |
||
280 | if self.first_scan and not self.args.disable_autodiscover: |
||
281 | msg = 'Glances is scanning your network. Please wait...' |
||
282 | self.first_scan = False |
||
283 | else: |
||
284 | msg = 'No Glances server available' |
||
285 | elif len(stats) == 1: |
||
286 | msg = 'One Glances server available' |
||
287 | else: |
||
288 | msg = '{} Glances servers available'.format(stats_len) |
||
289 | if self.args.disable_autodiscover: |
||
290 | msg += ' (auto discover is disabled)' |
||
291 | if screen_y > 1: |
||
292 | self.term_window.addnstr(y, x, |
||
293 | msg, |
||
294 | screen_x - x, |
||
295 | self.colors_list['TITLE']) |
||
296 | |||
297 | msg = '{}'.format(self._get_status_count(stats)) |
||
298 | self.term_window.addnstr(y + 1, x, |
||
299 | msg, |
||
300 | screen_x - x) |
||
301 | |||
302 | if stats_len > stats_max and screen_y > 2: |
||
303 | msg = '{} servers displayed.({}/{}) {}'.format(self.get_pagelines(stats), |
||
0 ignored issues
–
show
|
|||
304 | self._current_page + 1, |
||
0 ignored issues
–
show
|
|||
305 | self._page_max, |
||
0 ignored issues
–
show
|
|||
306 | self._get_status_count(stats)) |
||
0 ignored issues
–
show
|
|||
307 | self.term_window.addnstr(y + 1, x, |
||
308 | msg, |
||
309 | screen_x - x) |
||
310 | |||
0 ignored issues
–
show
|
|||
311 | if stats_len == 0: |
||
312 | return False |
||
313 | |||
314 | # Display the Glances server list |
||
315 | # ================================ |
||
316 | |||
317 | # Table of table |
||
318 | # Item description: [stats_id, column name, column size] |
||
319 | column_def = [ |
||
320 | ['name', 'Name', 16], |
||
321 | ['alias', None, None], |
||
322 | ['load_min5', 'LOAD', 6], |
||
323 | ['cpu_percent', 'CPU%', 5], |
||
324 | ['mem_percent', 'MEM%', 5], |
||
325 | ['status', 'STATUS', 9], |
||
326 | ['ip', 'IP', 15], |
||
327 | # ['port', 'PORT', 5], |
||
328 | ['hr_name', 'OS', 16], |
||
329 | ] |
||
330 | y = 2 |
||
331 | |||
332 | # Display table header |
||
333 | xc = x + 2 |
||
334 | for cpt, c in enumerate(column_def): |
||
335 | if xc < screen_x and y < screen_y and c[1] is not None: |
||
336 | self.term_window.addnstr(y, xc, |
||
337 | c[1], |
||
338 | screen_x - x, |
||
339 | self.colors_list['BOLD']) |
||
340 | xc += c[2] + self.space_between_column |
||
341 | y += 1 |
||
342 | |||
343 | # If a servers has been deleted from the list... |
||
344 | # ... and if the cursor is in the latest position |
||
345 | if self.cursor > len(stats) - 1: |
||
346 | # Set the cursor position to the latest item |
||
347 | self.cursor = len(stats) - 1 |
||
348 | |||
349 | stats_list = self._get_stats(stats) |
||
350 | start_line = self._page_max_lines * self._current_page |
||
0 ignored issues
–
show
|
|||
351 | end_line = start_line + self.get_pagelines(stats_list) |
||
352 | current_page = stats_list[start_line:end_line] |
||
353 | |||
0 ignored issues
–
show
|
|||
354 | # Display table |
||
355 | line = 0 |
||
356 | for v in current_page: |
||
357 | # Limit the number of displayed server (see issue #1256) |
||
358 | if line >= stats_max: |
||
359 | continue |
||
360 | # Get server stats |
||
361 | server_stat = {} |
||
362 | for c in column_def: |
||
363 | try: |
||
364 | server_stat[c[0]] = v[c[0]] |
||
365 | except KeyError as e: |
||
366 | logger.debug( |
||
367 | "Cannot grab stats {} from server (KeyError: {})".format(c[0], e)) |
||
368 | server_stat[c[0]] = '?' |
||
369 | # Display alias instead of name |
||
370 | try: |
||
371 | if c[0] == 'alias' and v[c[0]] is not None: |
||
372 | server_stat['name'] = v[c[0]] |
||
373 | except KeyError: |
||
374 | pass |
||
375 | |||
376 | # Display line for server stats |
||
377 | cpt = 0 |
||
378 | xc = x |
||
379 | |||
380 | # Is the line selected ? |
||
381 | if line == self.cursor: |
||
382 | # Display cursor |
||
383 | self.term_window.addnstr( |
||
384 | y, xc, ">", screen_x - xc, self.colors_list['BOLD']) |
||
385 | |||
386 | # Display the line |
||
387 | xc += 2 |
||
388 | for c in column_def: |
||
389 | if xc < screen_x and y < screen_y and c[1] is not None: |
||
390 | # Display server stats |
||
391 | self.term_window.addnstr( |
||
392 | y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']]) |
||
393 | xc += c[2] + self.space_between_column |
||
394 | cpt += 1 |
||
395 | # Next line, next server... |
||
396 | y += 1 |
||
397 | line += 1 |
||
398 | |||
399 | return True |
||
400 |