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

convert_nvme_attribute_to_dict()   A

Complexity

Conditions 1

Size

Total Lines 11
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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