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

SmartPlugin.hide_attributes()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
        'num': attr.num,
56
        'flags': attr.flags,
57
        'raw': attr.raw,
58
        'value': attr.value,
59
        'worst': attr.worst,
60
        'threshold': attr.thresh,
61
        'type': attr.type,
62
        'updated': attr.updated,
63
        'when_failed': attr.when_failed,
64
    }
65
66
67
NVME_ATTRIBUTE_LABELS = {
68
    "criticalWarning": "Number of critical warnings",
69
    "_temperature": "Temperature (°C)",
70
    "availableSpare": "Available spare (%)",
71
    "availableSpareThreshold": "Available spare threshold (%)",
72
    "percentageUsed": "Percentage used (%)",
73
    "dataUnitsRead": "Data units read",
74
    "bytesRead": "Bytes read",
75
    "dataUnitsWritten": "Data units written",
76
    "bytesWritten": "Bytes written",
77
    "hostReadCommands": "Host read commands",
78
    "hostWriteCommands": "Host write commands",
79
    "controllerBusyTime": "Controller busy time (min)",
80
    "powerCycles": "Power cycles",
81
    "powerOnHours": "Power-on hours",
82
    "unsafeShutdowns": "Unsafe shutdowns",
83
    "integrityErrors": "Integrity errors",
84
    "errorEntries": "Error log entries",
85
    "warningTemperatureTime": "Warning temperature time (min)",
86
    "criticalTemperatureTime": "Critical temperature time (min)",
87
    "_logical_sector_size": "Logical sector size",
88
    "_physical_sector_size": "Physical sector size",
89
    "errors": "Errors",
90
    "tests": "Self-tests",
91
}
92
93
def convert_nvme_attribute_to_dict(key,value):
94
    label = NVME_ATTRIBUTE_LABELS.get(key, key)
95
96
    return {
97
        'name': label,
98
        'value': value,
99
        'flags': None,
100
        'raw': value,
101
        'worst': None,
102
        'threshold': None,
103
        'type': None,
104
        'updated': None,
105
        'when_failed': None
106
    }
107
108
def get_smart_data(hide_attributes):
109
    """
110
    Get SMART attribute data
111
    :return: list of multi leveled dictionaries
112
             each dict has a key "DeviceName" with the identification of the device in smartctl
113
             also has keys of the SMART attribute id, with value of another dict of the attributes
114
             [
115
                {
116
                    "DeviceName": "/dev/sda blahblah",
117
                    "1":
118
                    {
119
                        "flags": "..",
120
                        "raw": "..",
121
                        etc,
122
                    }
123
                    ...
124
                }
125
             ]
126
    """
127
    stats = []
128
    # get all devices
129
    try:
130
        devlist = DeviceList()
131
    except TypeError as e:
132
        # Catch error  (see #1806)
133
        logger.debug(f'Smart plugin error - Can not grab device list ({e})')
134
        global import_error_tag
135
        import_error_tag = True
136
        return stats
137
138
    for dev in devlist.devices:
139
        stats.append(
140
            {
141
                'DeviceName': f'{dev.name} {dev.model}',
142
            }
143
        )
144
        for attribute in dev.attributes:
145
            if attribute is None:
146
                pass
147
            elif attribute.name in hide_attributes:
148
                pass
149
            else:
150
                attrib_dict = convert_attribute_to_dict(attribute)
151
152
                # we will use the attribute number as the key
153
                num = attrib_dict.pop('num', None)
154
                try:
155
                    assert num is not None
156
                except Exception as e:
157
                    # we should never get here, but if we do, continue to next iteration and skip this attribute
158
                    logger.debug(f'Smart plugin error - Skip the attribute {attribute} ({e})')
159
                    continue
160
161
                stats[-1][num] = attrib_dict
162
163
        if isinstance(dev.if_attributes, NvmeAttributes):
164
            idx = 0
165
            for attr in dev.if_attributes.__dict__.keys():
166
                idx +=1
167
168
                attrib_dict = convert_nvme_attribute_to_dict(attr,  dev.if_attributes.__dict__[attr])
169
                if attrib_dict['name'] in hide_attributes:
170
                    pass
171
                else:
172
                    try:
173
                        if dev.if_attributes.__dict__[attr] is not None: # make sure the value is serializable to prevent errors in rendering
174
                            serialized = str(dev.if_attributes.__dict__[attr])
175
                    except Exception as e:
176
                        logger.debug(f'Unable to serialize attribute {attr} from NVME')
177
                        attrib_dict['value'] = None
178
                        attrib_dict['raw'] = None
179
                    finally:
180
                        stats[-1][idx] = attrib_dict
181
182
    return stats
183
184
185
class SmartPlugin(GlancesPluginModel):
186
    """Glances' HDD SMART plugin."""
187
188
    def __init__(self, args=None, config=None, stats_init_value=[]):
189
        """Init the plugin."""
190
        # check if user is admin
191
        if not is_admin() and args:
192
            disable(args, "smart")
193
            logger.debug("Current user is not admin, HDD SMART plugin disabled.")
194
195
        super().__init__(args=args, config=config)
196
197
        # We want to display the stat in the curse interface
198
        self.display_curse = True
199
200
        if 'hide_attributes' in config.as_dict()['smart']:
201
            logger.info(
202
                'Followings SMART attributes wil not be displayed: {}'.format(
203
                    config.as_dict()['smart']['hide_attributes']
204
                )
205
            )
206
            self.hide_attributes = config.as_dict()['smart']['hide_attributes'].split(',')
207
        else:
208
            self.hide_attributes = []
209
210
    @property
211
    def hide_attributes(self):
212
        """Set hide_attributes list"""
213
        return self._hide_attributes
214
215
    @hide_attributes.setter
216
    def hide_attributes(self, attr_list):
217
        """Set hide_attributes list"""
218
        self._hide_attributes = [i for i in attr_list]
219
220
    @GlancesPluginModel._check_decorator
221
    @GlancesPluginModel._log_result_decorator
222
    def update(self):
223
        """Update SMART stats using the input method."""
224
        # Init new stats
225
        stats = self.get_init_value()
226
227
        if import_error_tag:
228
            return self.stats
229
230
        if self.input_method == 'local':
231
            # Update stats and hide some sensors(#2996)
232
            stats = [s for s in get_smart_data(self.hide_attributes) if self.is_display(s[self.get_key()])]
233
        elif self.input_method == 'snmp':
234
            pass
235
236
        # Update the stats
237
        self.stats = stats
238
239
        return self.stats
240
241
    def get_key(self):
242
        """Return the key of the list."""
243
        return 'DeviceName'
244
245
    def msg_curse(self, args=None, max_width=None):
246
        """Return the dict to display in the curse interface."""
247
        # Init the return message
248
        ret = []
249
250
        # Only process if stats exist...
251
        if import_error_tag or not self.stats or self.is_disabled():
252
            return ret
253
254
        # Max size for the interface name
255
        if max_width:
256
            name_max_width = max_width - 6
257
        else:
258
            # No max_width defined, return an empty curse message
259
            logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.")
260
            return ret
261
262
        # Header
263
        msg = '{:{width}}'.format('SMART disks', width=name_max_width)
264
        ret.append(self.curse_add_line(msg, "TITLE"))
265
        # Data
266
        for device_stat in self.stats:
267
            # New line
268
            ret.append(self.curse_new_line())
269
            msg = '{:{width}}'.format(device_stat['DeviceName'][:max_width], width=max_width)
270
            ret.append(self.curse_add_line(msg))
271
            try:
272
                device_stat_sorted = sorted([i for i in device_stat if i != 'DeviceName'], key=int)
273
            except ValueError:
274
                # Catch ValueError, see #2904
275
                device_stat_sorted = [i for i in device_stat if i != 'DeviceName']
276
            for smart_stat in device_stat_sorted:
277
                ret.append(self.curse_new_line())
278
                msg = ' {:{width}}'.format(
279
                    device_stat[smart_stat]['name'][: name_max_width - 1].replace('_', ' '), width=name_max_width - 1
280
                )
281
                ret.append(self.curse_add_line(msg))
282
                try:
283
                    raw = device_stat[smart_stat]['raw']
284
                    msg = '{:>8}'.format("" if raw is None else str(raw))
285
                    ret.append(self.curse_add_line(msg))
286
                except Exception as e:
287
                    logger.debug(f"Failed to serialize {smart_stat}")
288
                    meg = ""
289
                    ret.append(self.curse_add_line(msg))
290
291
        return ret
292