Test Failed
Pull Request — develop (#3355)
by
unknown
02:48
created

glances.plugins.smart   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 171
dl 0
loc 297
rs 9.44
c 0
b 0
f 0
wmc 37

5 Methods

Rating   Name   Duplication   Size   Complexity  
A SmartPlugin.get_key() 0 3 1
A SmartPlugin.hide_attributes() 0 4 1
A SmartPlugin.update() 0 20 4
D SmartPlugin.msg_curse() 0 50 12
A SmartPlugin.__init__() 0 21 4

3 Functions

Rating   Name   Duplication   Size   Complexity  
A convert_attribute_to_dict() 0 13 1
D get_smart_data() 0 75 12
A convert_nvme_attribute_to_dict() 0 14 1
1
#
2
# This file is part of Glances.
3
#
4
# Copyright (C) 2018 Tim Nibert <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""
10
Hard disk SMART attributes plugin.
11
Depends on pySMART and smartmontools
12
Must execute as root
13
"usermod -a -G disk USERNAME" is not sufficient unfortunately
14
SmartCTL (/usr/sbin/smartctl) must be in system path for python2.
15
16
Regular PySMART is a python2 library.
17
We are using the pySMART.smartx updated library to support both python 2 and 3.
18
19
If we only have disk group access (no root):
20
$ smartctl -i /dev/sda
21
smartctl 6.6 2016-05-31 r4324 [x86_64-linux-4.15.0-30-generic] (local build)
22
Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org
23
24
25
Probable ATA device behind a SAT layer
26
Try an additional '-d ata' or '-d sat' argument.
27
28
This is not very hopeful: https://medium.com/opsops/why-smartctl-could-not-be-run-without-root-7ea0583b1323
29
30
So, here is what we are going to do:
31
Check for admin access.  If no admin access, disable SMART plugin.
32
33
If smartmontools is not installed, we should catch the error upstream in plugin initialization.
34
"""
35
36
from glances.globals import is_admin
37
from glances.logger import logger
38
from glances.main import disable
39
from glances.plugins.plugin.model import GlancesPluginModel
40
41
# Import plugin specific dependency
42
try:
43
    from pySMART import DeviceList
44
    from pySMART.interface.nvme import NvmeAttributes
45
except ImportError as e:
46
    import_error_tag = True
47
    logger.warning(f"Missing Python Lib ({e}), HDD Smart plugin is disabled")
48
else:
49
    import_error_tag = False
50
51
52
def convert_attribute_to_dict(attr):
53
    return {
54
        'name': attr.name,
55
        'key': attr.name,
56
        'num': attr.num,
57
        'flags': attr.flags,
58
        'raw': attr.raw,
59
        'value': attr.value,
60
        'worst': attr.worst,
61
        'threshold': attr.thresh,
62
        'type': attr.type,
63
        'updated': attr.updated,
64
        'when_failed': attr.when_failed,
65
    }
66
67
68
NVME_ATTRIBUTE_LABELS = {
69
    "criticalWarning": "Number of critical warnings",
70
    "_temperature": "Temperature (°C)",
71
    "availableSpare": "Available spare (%)",
72
    "availableSpareThreshold": "Available spare threshold (%)",
73
    "percentageUsed": "Percentage used (%)",
74
    "dataUnitsRead": "Data units read",
75
    "bytesRead": "Bytes read",
76
    "dataUnitsWritten": "Data units written",
77
    "bytesWritten": "Bytes written",
78
    "hostReadCommands": "Host read commands",
79
    "hostWriteCommands": "Host write commands",
80
    "controllerBusyTime": "Controller busy time (min)",
81
    "powerCycles": "Power cycles",
82
    "powerOnHours": "Power-on hours",
83
    "unsafeShutdowns": "Unsafe shutdowns",
84
    "integrityErrors": "Integrity errors",
85
    "errorEntries": "Error log entries",
86
    "warningTemperatureTime": "Warning temperature time (min)",
87
    "criticalTemperatureTime": "Critical temperature time (min)",
88
    "_logical_sector_size": "Logical sector size",
89
    "_physical_sector_size": "Physical sector size",
90
    "errors": "Errors",
91
    "tests": "Self-tests",
92
}
93
94
def convert_nvme_attribute_to_dict(key,value):
95
    label = NVME_ATTRIBUTE_LABELS.get(key, key)
96
97
    return {
98
        'name': label,
99
        'key': key,
100
        'value': value,
101
        'flags': None,
102
        'raw': value,
103
        'worst': None,
104
        'threshold': None,
105
        'type': None,
106
        'updated': None,
107
        'when_failed': None
108
    }
109
110
def get_smart_data(hide_attributes):
111
    """
112
    Get SMART attribute data
113
    :return: list of multi leveled dictionaries
114
             each dict has a key "DeviceName" with the identification of the device in smartctl
115
             also has keys of the SMART attribute id, with value of another dict of the attributes
116
             [
117
                {
118
                    "DeviceName": "/dev/sda blahblah",
119
                    "1":
120
                    {
121
                        "flags": "..",
122
                        "raw": "..",
123
                        etc,
124
                    }
125
                    ...
126
                }
127
             ]
128
    """
129
    stats = []
130
    # get all devices
131
    try:
132
        devlist = DeviceList()
133
    except TypeError as e:
134
        # Catch error  (see #1806)
135
        logger.debug(f'Smart plugin error - Can not grab device list ({e})')
136
        global import_error_tag
137
        import_error_tag = True
138
        return stats
139
140
    for dev in devlist.devices:
141
        stats.append(
142
            {
143
                'DeviceName': f'{dev.name} {dev.model}',
144
            }
145
        )
146
        for attribute in dev.attributes:
147
            if attribute is None:
148
                pass
149
            elif attribute.name in hide_attributes:
150
                pass
151
            else:
152
                attrib_dict = convert_attribute_to_dict(attribute)
153
154
                # we will use the attribute number as the key
155
                num = attrib_dict.pop('num', None)
156
                try:
157
                    assert num is not None
158
                except Exception as e:
159
                    # we should never get here, but if we do, continue to next iteration and skip this attribute
160
                    logger.debug(f'Smart plugin error - Skip the attribute {attribute} ({e})')
161
                    continue
162
163
                stats[-1][num] = attrib_dict
164
165
        if isinstance(dev.if_attributes, NvmeAttributes):
166
            idx = 0
167
            for attr in dev.if_attributes.__dict__.keys():
168
                idx +=1
169
170
                attrib_dict = convert_nvme_attribute_to_dict(attr,  dev.if_attributes.__dict__[attr])
171
                if attrib_dict['name'] in hide_attributes:
172
                    pass
173
                else:
174
                    try:
175
                        if dev.if_attributes.__dict__[attr] is not None: # make sure the value is serializable to prevent errors in rendering
176
                            serialized = str(dev.if_attributes.__dict__[attr])
177
                    except Exception as e:
178
                        logger.debug(f'Unable to serialize attribute {attr} from NVME')
179
                        attrib_dict['value'] = None
180
                        attrib_dict['raw'] = None
181
                    finally:
182
                        stats[-1][idx] = attrib_dict
183
184
    return stats
185
186
187
class SmartPlugin(GlancesPluginModel):
188
    """Glances' HDD SMART plugin."""
189
190
    def __init__(self, args=None, config=None, stats_init_value=[]):
191
        """Init the plugin."""
192
        # check if user is admin
193
        if not is_admin() and args:
194
            disable(args, "smart")
195
            logger.debug("Current user is not admin, HDD SMART plugin disabled.")
196
197
        super().__init__(args=args, config=config)
198
199
        # We want to display the stat in the curse interface
200
        self.display_curse = True
201
202
        if 'hide_attributes' in config.as_dict()['smart']:
203
            logger.info(
204
                'Followings SMART attributes wil not be displayed: {}'.format(
205
                    config.as_dict()['smart']['hide_attributes']
206
                )
207
            )
208
            self.hide_attributes = config.as_dict()['smart']['hide_attributes'].split(',')
209
        else:
210
            self.hide_attributes = []
211
212
    @property
213
    def hide_attributes(self):
214
        """Set hide_attributes list"""
215
        return self._hide_attributes
216
217
    @hide_attributes.setter
218
    def hide_attributes(self, attr_list):
219
        """Set hide_attributes list"""
220
        self._hide_attributes = [i for i in attr_list]
221
222
    @GlancesPluginModel._check_decorator
223
    @GlancesPluginModel._log_result_decorator
224
    def update(self):
225
        """Update SMART stats using the input method."""
226
        # Init new stats
227
        stats = self.get_init_value()
228
229
        if import_error_tag:
230
            return self.stats
231
232
        if self.input_method == 'local':
233
            # Update stats and hide some sensors(#2996)
234
            stats = [s for s in get_smart_data(self.hide_attributes) if self.is_display(s[self.get_key()])]
235
        elif self.input_method == 'snmp':
236
            pass
237
238
        # Update the stats
239
        self.stats = stats
240
241
        return self.stats
242
243
    def get_key(self):
244
        """Return the key of the list."""
245
        return 'DeviceName'
246
247
    def msg_curse(self, args=None, max_width=None):
248
        """Return the dict to display in the curse interface."""
249
        # Init the return message
250
        ret = []
251
252
        # Only process if stats exist...
253
        if import_error_tag or not self.stats or self.is_disabled():
254
            return ret
255
256
        # Max size for the interface name
257
        if max_width:
258
            name_max_width = max_width - 6
259
        else:
260
            # No max_width defined, return an empty curse message
261
            logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.")
262
            return ret
263
264
        # Header
265
        msg = '{:{width}}'.format('SMART disks', width=name_max_width)
266
        ret.append(self.curse_add_line(msg, "TITLE"))
267
        # Data
268
        for device_stat in self.stats:
269
            # New line
270
            ret.append(self.curse_new_line())
271
            msg = '{:{width}}'.format(device_stat['DeviceName'][:max_width], width=max_width)
272
            ret.append(self.curse_add_line(msg))
273
            try:
274
                device_stat_sorted = sorted([i for i in device_stat if i != 'DeviceName'], key=int)
275
            except ValueError:
276
                # Catch ValueError, see #2904
277
                device_stat_sorted = [i for i in device_stat if i != 'DeviceName']
278
            for smart_stat in device_stat_sorted:
279
                ret.append(self.curse_new_line())
280
                msg = ' {:{width}}'.format(
281
                    device_stat[smart_stat]['name'][: name_max_width - 1].replace('_', ' '), width=name_max_width - 1
282
                )
283
                ret.append(self.curse_add_line(msg))
284
                try:
285
                    raw = device_stat[smart_stat]['raw']
286
                    if device_stat[smart_stat]['key'] in ["bytesWritten", "bytesRead", "dataUnitsRead", "dataUnitsWritten", "hostReadCommands", "hostWriteCommands" ]:
287
                        msg = '{:>8}'.format("" if raw is None else self.auto_unit(raw))
288
                    else:    
289
                        msg = '{:>8}'.format("" if raw is None else str(raw))
290
                    ret.append(self.curse_add_line(msg))
291
                except Exception as e:
292
                    logger.debug(f"Failed to serialize {smart_stat}")
293
                    meg = ""
294
                    ret.append(self.curse_add_line(msg))
295
296
        return ret
297