diff --git a/NEWS b/NEWS index 04082a84..759c0536 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ Glances Version 2.x Version 2.X =========== + * Add the RAID plugins (issue #447) * Fix incorrect kernel thread detection with --hide-kernel-threads (issue #457) * Handle IOError exception if no /etc/os-release to use Glances on Synology DSM (issue #458) * Check issue error in client/server mode (issue #459) diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py index ab997dde..3b69c368 100644 --- a/glances/core/glances_main.py +++ b/glances/core/glances_main.py @@ -75,6 +75,8 @@ class GlancesMain(object): dest='disable_fs', help=_('disable filesystem module')) parser.add_argument('--disable-sensors', action='store_true', default=False, dest='disable_sensors', help=_('disable sensors module')) + parser.add_argument('--disable-raid', action='store_true', default=False, + dest='disable_raid', help=_('disable RAID module')) parser.add_argument('--disable-left-sidebar', action='store_true', default=False, dest='disable_left_sidebar', help=_('disable network, disk io, FS and sensors modules')) parser.add_argument('--disable-process', action='store_true', default=False, diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 318c307c..97ef82db 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -306,6 +306,9 @@ class _GlancesCurses(object): elif self.pressedkey == ord('r'): # 'r' > Reset history self.reset_history_tag = not self.reset_history_tag + elif self.pressedkey == ord('R'): + # 'R' > Hide RAID plugins + self.args.disable_raid = not self.args.disable_raid elif self.pressedkey == ord('s'): # 's' > Show/hide sensors stats (Linux-only) self.args.disable_sensors = not self.args.disable_sensors @@ -424,6 +427,8 @@ class _GlancesCurses(object): 'diskio').get_stats_display(args=self.args) stats_fs = stats.get_plugin('fs').get_stats_display( args=self.args, max_width=plugin_max_width) + stats_raid = stats.get_plugin('raid').get_stats_display( + args=self.args, max_width=plugin_max_width) stats_sensors = stats.get_plugin( 'sensors').get_stats_display(args=self.args) stats_now = stats.get_plugin('now').get_stats_display() @@ -507,7 +512,8 @@ class _GlancesCurses(object): # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time) self.init_column() if (not (self.args.disable_network and self.args.disable_diskio - and self.args.disable_fs and self.args.disable_sensors)) \ + and self.args.disable_fs and self.args.disable_raid + and self.args.disable_sensors)) \ and not self.args.disable_left_sidebar: self.new_line() self.display_plugin(stats_network) @@ -516,6 +522,8 @@ class _GlancesCurses(object): self.new_line() self.display_plugin(stats_fs) self.new_line() + self.display_plugin(stats_raid) + self.new_line() self.display_plugin(stats_sensors) self.new_line() self.display_plugin(stats_now) diff --git a/glances/plugins/glances_raid.py b/glances/plugins/glances_raid.py new file mode 100644 index 00000000..ff0f24b1 --- /dev/null +++ b/glances/plugins/glances_raid.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# Copyright (C) 2014 Nicolargo +# +# 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 . + +"""RAID plugin.""" + +# Import Glances libs +from glances.core.glances_logging import logger +from glances.plugins.glances_plugin import GlancesPlugin + +# pymdstat only available on GNU/Linux OS +try: + from pymdstat import MdStat +except ImportError: + logger.debug("pymdstat library not found. Glances cannot grab RAID info.") + pass + + +class Plugin(GlancesPlugin): + + """Glances' RAID plugin. + + stats is a dict (see pymdstat documentation) + """ + + def __init__(self, args=None): + """Init the plugin.""" + GlancesPlugin.__init__(self, args=args) + + # We want to display the stat in the curse interface + self.display_curse = True + + # Init the stats + self.reset() + + def reset(self): + """Reset/init the stats.""" + self.stats = {} + + @GlancesPlugin._log_result_decorator + def update(self): + """Update RAID stats using the input method.""" + # Reset stats + self.reset() + + if self.get_input() == 'local': + # Update stats using the PyMDstat lib (https://github.com/nicolargo/pymdstat) + try: + # !!! Path ONLY for dev + mds = MdStat('/home/nicolargo/dev/pymdstat/tests/mdstat.09') + self.stats = mds.get_stats()['arrays'] + except Exception as e: + logger.debug("Can not grab RAID stats (%s)" % e) + return self.stats + + elif self.get_input() == 'snmp': + # Update stats using SNMP + # No standard way for the moment... + pass + + return self.stats + + def msg_curse(self, args=None): + """Return the dict to display in the curse interface.""" + # Init the return message + ret = [] + + # Only process if stats exist and display plugin enable... + if not self.stats or args.disable_raid: + return ret + + # Build the string message + # Header + msg = '{0:10}'.format(_('RAID disks')) + ret.append(self.curse_add_line(msg, "TITLE")) + msg = '{0:>6}'.format(_("Used")) + ret.append(self.curse_add_line(msg)) + msg = '{0:>7}'.format(_("Avail")) + ret.append(self.curse_add_line(msg)) + # Data + arrays = self.stats.keys() + arrays.sort() + for array in arrays: + # New line + ret.append(self.curse_new_line()) + # Display the current status + status = self.raid_alert(self.stats[array]['status'], self.stats[array]['used'], self.stats[array]['available']) + # Data: RAID type name | disk used | disk available + msg = '{0:<5}{1:>6}'.format(self.stats[array]['type'].upper(), array) + ret.append(self.curse_add_line(msg)) + if self.stats[array]['status'] == 'active': + msg = '{0:>5}'.format(self.stats[array]['used']) + ret.append(self.curse_add_line(msg, status)) + msg = '{0:>7}'.format(self.stats[array]['available']) + ret.append(self.curse_add_line(msg, status)) + elif self.stats[array]['status'] == 'inactive': + ret.append(self.curse_new_line()) + msg = '|_ Status {}'.format(self.stats[array]['status']) + ret.append(self.curse_add_line(msg, status)) + components = self.stats[array]['components'].keys() + components.sort() + for component in components: + ret.append(self.curse_new_line()) + msg = ' |_ disk {0}: '.format(self.stats[array]['components'][component]) + ret.append(self.curse_add_line(msg)) + msg = '{0}'.format(component) + ret.append(self.curse_add_line(msg)) + if self.stats[array]['used'] < self.stats[array]['available']: + # Display current array configuration + ret.append(self.curse_new_line()) + msg = '|_ Degraded mode' + ret.append(self.curse_add_line(msg, status)) + if len(self.stats[array]['config']) < 17: + ret.append(self.curse_new_line()) + msg = ' |_ {0}'.format(self.stats[array]['config'].replace('_', 'A')) + ret.append(self.curse_add_line(msg)) + + return ret + + def raid_alert(self, status, used, available): + """ + [available/used] means that ideally the array would have _available_ devices however, _used_ devices are in use. + Obviously when used >= available then things are good. + """ + if status == 'inactive': + return 'CRITICAL' + if used < available: + return 'WARNING' + return 'OK' diff --git a/setup.py b/setup.py index 34cbaad6..7bf90e85 100755 --- a/setup.py +++ b/setup.py @@ -65,7 +65,8 @@ setup( 'BATINFO': ['batinfo'], 'SNMP': ['pysnmp'], 'CHART': ['matplotlib'], - 'BROWSER': ['zeroconf>=0.16', 'netifaces'] + 'BROWSER': ['zeroconf>=0.16', 'netifaces'], + 'RAID': ['pymdstat'] }, packages=['glances'], include_package_data=True,