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

glances.plugins.smart.convert_attribute_to_dict()   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nop 1
dl 0
loc 12
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
                idx +=1
136
                attrib_dict = convert_nvme_attribute_to_dict(attr,  dev.if_attributes.__dict__[attr])
137
                try:
138
                    if dev.if_attributes.__dict__[attr] is not None:
139
                        serialized = str(dev.if_attributes.__dict__[attr])
140
                except Exception as e:
141
                    logger.debug(f'Unable to serialize attribute {attr} from NVME')
142
                    attrib_dict['value'] = None
143
                    attrib_dict['raw'] = None
144
                finally:
145
                    stats[-1][idx] = attrib_dict
146
147
    return stats
148
149
150
class SmartPlugin(GlancesPluginModel):
151
    """Glances' HDD SMART plugin."""
152
153
    def __init__(self, args=None, config=None, stats_init_value=[]):
154
        """Init the plugin."""
155
        # check if user is admin
156
        if not is_admin() and args:
157
            disable(args, "smart")
158
            logger.debug("Current user is not admin, HDD SMART plugin disabled.")
159
160
        super().__init__(args=args, config=config)
161
162
        # We want to display the stat in the curse interface
163
        self.display_curse = True
164
165
    @GlancesPluginModel._check_decorator
166
    @GlancesPluginModel._log_result_decorator
167
    def update(self):
168
        """Update SMART stats using the input method."""
169
        # Init new stats
170
        stats = self.get_init_value()
171
172
        if import_error_tag:
173
            return self.stats
174
175
        if self.input_method == 'local':
176
            # Update stats and hide some sensors(#2996)
177
            stats = [s for s in get_smart_data() if self.is_display(s[self.get_key()])]
178
        elif self.input_method == 'snmp':
179
            pass
180
181
        # Update the stats
182
        self.stats = stats
183
184
        return self.stats
185
186
    def get_key(self):
187
        """Return the key of the list."""
188
        return 'DeviceName'
189
190
    def msg_curse(self, args=None, max_width=None):
191
        """Return the dict to display in the curse interface."""
192
        # Init the return message
193
        ret = []
194
195
        # Only process if stats exist...
196
        if import_error_tag or not self.stats or self.is_disabled():
197
            return ret
198
199
        # Max size for the interface name
200
        if max_width:
201
            name_max_width = max_width - 6
202
        else:
203
            # No max_width defined, return an empty curse message
204
            logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.")
205
            return ret
206
207
        # Header
208
        msg = '{:{width}}'.format('SMART disks', width=name_max_width)
209
        ret.append(self.curse_add_line(msg, "TITLE"))
210
        # Data
211
        for device_stat in self.stats:
212
            # New line
213
            ret.append(self.curse_new_line())
214
            msg = '{:{width}}'.format(device_stat['DeviceName'][:max_width], width=max_width)
215
            ret.append(self.curse_add_line(msg))
216
            try:
217
                device_stat_sorted = sorted([i for i in device_stat if i != 'DeviceName'], key=int)
218
            except ValueError:
219
                # Catch ValueError, see #2904
220
                device_stat_sorted = [i for i in device_stat if i != 'DeviceName']
221
            for smart_stat in device_stat_sorted:
222
                ret.append(self.curse_new_line())
223
                msg = ' {:{width}}'.format(
224
                    device_stat[smart_stat]['name'][: name_max_width - 1].replace('_', ' '), width=name_max_width - 1
225
                )
226
                ret.append(self.curse_add_line(msg))
227
                try:
228
                    raw = device_stat[smart_stat]['raw']
229
                    msg = '{:>8}'.format("" if raw is None else str(raw))
230
                    ret.append(self.curse_add_line(msg))
231
                except Exception as e:
232
                    logger.debug(f"Failed to serialize {smart_stat}")
233
                    meg = ""
234
                    ret.append(self.curse_add_line(msg))
235
236
        return ret
237