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

glances.plugins.smart.SmartPlugin.update()   A

Complexity

Conditions 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nop 1
dl 0
loc 20
rs 9.8
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
def convert_nvme_attribute_to_dict(key,value):
67
    return {
68
        'name': key,
69
        'value': value,
70
        'flags': None,
71
        'raw': value,
72
        'worst': None,
73
        'threshold': None,
74
        'type': None,
75
        'updated': None,
76
        'when_failed': None
77
    }
78
79
def get_smart_data():
80
    """
81
    Get SMART attribute data
82
    :return: list of multi leveled dictionaries
83
             each dict has a key "DeviceName" with the identification of the device in smartctl
84
             also has keys of the SMART attribute id, with value of another dict of the attributes
85
             [
86
                {
87
                    "DeviceName": "/dev/sda blahblah",
88
                    "1":
89
                    {
90
                        "flags": "..",
91
                        "raw": "..",
92
                        etc,
93
                    }
94
                    ...
95
                }
96
             ]
97
    """
98
    stats = []
99
    # get all devices
100
    try:
101
        devlist = DeviceList()
102
    except TypeError as e:
103
        # Catch error  (see #1806)
104
        logger.debug(f'Smart plugin error - Can not grab device list ({e})')
105
        global import_error_tag
106
        import_error_tag = True
107
        return stats
108
109
    for dev in devlist.devices:
110
        stats.append(
111
            {
112
                'DeviceName': f'{dev.name} {dev.model}',
113
            }
114
        )
115
        for attribute in dev.attributes:
116
            if attribute is None:
117
                pass
118
            else:
119
                attrib_dict = convert_attribute_to_dict(attribute)
120
121
                # we will use the attribute number as the key
122
                num = attrib_dict.pop('num', None)
123
                try:
124
                    assert num is not None
125
                except Exception as e:
126
                    # we should never get here, but if we do, continue to next iteration and skip this attribute
127
                    logger.debug(f'Smart plugin error - Skip the attribute {attribute} ({e})')
128
                    continue
129
130
                stats[-1][num] = attrib_dict
131
132
        if isinstance(dev.if_attributes, NvmeAttributes):
133
          idx = 0
134
          for attr in dev.if_attributes.__dict__.keys():
135
              try:
136
                  attrib_dict = convert_nvme_attribute_to_dict(attr,  dev.if_attributes.__dict__[attr])
137
                  if dev.if_attributes.__dict__[attr] is not None:
138
                     serialized = str(dev.if_attributes.__dict__[attr])
139
              except Exception as e:
140
                    logger.debug(f'Unable to serialize attribute {attr} from NVME')
141
142
              idx +=1
143
144
              stats[-1][idx] = attrib_dict
145
146
    return stats
147
148
149
class SmartPlugin(GlancesPluginModel):
150
    """Glances' HDD SMART plugin."""
151
152
    def __init__(self, args=None, config=None, stats_init_value=[]):
153
        """Init the plugin."""
154
        # check if user is admin
155
        if not is_admin() and args:
156
            disable(args, "smart")
157
            logger.debug("Current user is not admin, HDD SMART plugin disabled.")
158
159
        super().__init__(args=args, config=config)
160
161
        # We want to display the stat in the curse interface
162
        self.display_curse = True
163
164
    @GlancesPluginModel._check_decorator
165
    @GlancesPluginModel._log_result_decorator
166
    def update(self):
167
        """Update SMART stats using the input method."""
168
        # Init new stats
169
        stats = self.get_init_value()
170
171
        if import_error_tag:
172
            return self.stats
173
174
        if self.input_method == 'local':
175
            # Update stats and hide some sensors(#2996)
176
            stats = [s for s in get_smart_data() if self.is_display(s[self.get_key()])]
177
        elif self.input_method == 'snmp':
178
            pass
179
180
        # Update the stats
181
        self.stats = stats
182
183
        return self.stats
184
185
    def get_key(self):
186
        """Return the key of the list."""
187
        return 'DeviceName'
188
189
    def msg_curse(self, args=None, max_width=None):
190
        """Return the dict to display in the curse interface."""
191
        # Init the return message
192
        ret = []
193
194
        # Only process if stats exist...
195
        if import_error_tag or not self.stats or self.is_disabled():
196
            return ret
197
198
        # Max size for the interface name
199
        if max_width:
200
            name_max_width = max_width - 6
201
        else:
202
            # No max_width defined, return an empty curse message
203
            logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.")
204
            return ret
205
206
        # Header
207
        msg = '{:{width}}'.format('SMART disks', width=name_max_width)
208
        ret.append(self.curse_add_line(msg, "TITLE"))
209
        # Data
210
        for device_stat in self.stats:
211
            # New line
212
            ret.append(self.curse_new_line())
213
            msg = '{:{width}}'.format(device_stat['DeviceName'][:max_width], width=max_width)
214
            ret.append(self.curse_add_line(msg))
215
            try:
216
                device_stat_sorted = sorted([i for i in device_stat if i != 'DeviceName'], key=int)
217
            except ValueError:
218
                # Catch ValueError, see #2904
219
                device_stat_sorted = [i for i in device_stat if i != 'DeviceName']
220
            for smart_stat in device_stat_sorted:
221
                ret.append(self.curse_new_line())
222
                msg = ' {:{width}}'.format(
223
                    device_stat[smart_stat]['name'][: name_max_width - 1].replace('_', ' '), width=name_max_width - 1
224
                )
225
                ret.append(self.curse_add_line(msg))
226
                try:
227
                    raw = device_stat[smart_stat]['raw']
228
                    msg = '{:>8}'.format("" if raw is None else str(raw))
229
                    ret.append(self.curse_add_line(msg))
230
                except Exception as e:
231
                    logger.debug(f"Failed to serialize {smart_stat}")
232
                    meg = ""
233
                    ret.append(self.curse_add_line(msg))
234
235
        return ret
236