Skip to content

Commit bba7563

Browse files
committed
added diagnostic class to encapsulate SCSI/SAS data
old diag dict is now deprecated but still accsible as a property
1 parent 067070c commit bba7563

File tree

12 files changed

+926
-178
lines changed

12 files changed

+926
-178
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pySMART.egg-info
66
.vscode
77
.pypirc
88
*.pyc
9+
__pycache__
910
build/
1011
dist/
1112
.coverage

pySMART/device.py

Lines changed: 56 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@
3131
import os
3232
import re
3333
import warnings
34-
from subprocess import Popen, PIPE
3534
from time import time, strptime, mktime, sleep
36-
from typing import Tuple, Union, List
35+
from typing import Tuple, Union, List, Dict
3736

3837
# pySMART module imports
3938
from .attribute import Attribute
39+
from .diagnostics import Diagnostics
4040
from .testentry import TestEntry
4141
from .smartctl import Smartctl
4242
from .utils import smartctl_type, smartctl_isvalid_type, any_in, all_in
@@ -192,9 +192,9 @@ def __init__(self, name: str, interface: str = None, abridged: bool = False, sma
192192
"""
193193
**(int):** Estimate progress percantage of the running SMART selftest.
194194
"""
195-
self.diags = {}
195+
self.diagnostics: Diagnostics = Diagnostics()
196196
"""
197-
**(dict of str):** Contains parsed and processed diagnostic information
197+
**Diagnostics** Contains parsed and processed diagnostic information
198198
extracted from the SMART information. Currently only populated for
199199
SAS and SCSI devices, since ATA/SATA SMART attributes are manufacturer
200200
proprietary.
@@ -273,6 +273,12 @@ def capacity(self) -> str:
273273
"""
274274
return self._capacity
275275

276+
@property
277+
def diags(self) -> Dict[str, str]:
278+
"""Gets the old/deprecated version of SCSI/SAS diags atribute.
279+
"""
280+
return self.diagnostics.get_classic_format()
281+
276282
@property
277283
def size_raw(self) -> str:
278284
"""Returns the capacity in the raw smartctl format.
@@ -317,7 +323,7 @@ def __getstate__(self, all_info=True):
317323
'messages': self.messages,
318324
'test_capabilities': self.test_capabilities.copy(),
319325
'tests': [t.__getstate__() for t in self.tests] if self.tests else [],
320-
'diagnostics': self.diags.copy(),
326+
'diagnostics': self.diagnostics.__getstate__(),
321327
'temperature': self.temperature,
322328
'attributes': [attr.__getstate__() if attr else None for attr in self.attributes]
323329
}
@@ -972,69 +978,71 @@ class members, including the SMART attribute table and self-test log.
972978
# the place of similar ATA SMART information
973979
if 'used endurance' in line:
974980
pct = int(line.split(':')[1].strip()[:-1])
975-
self.diags['Life_Left'] = str(100 - pct) + '%'
981+
self.diagnostics.Life_Left = 100 - pct
976982
if 'Specified cycle count' in line:
977-
self.diags['Start_Stop_Spec'] = line.split(':')[1].strip()
978-
if self.diags['Start_Stop_Spec'] == '0':
979-
self.diags['Start_Stop_Pct_Left'] = '-'
983+
self.diagnostics.Start_Stop_Spec = int(
984+
line.split(':')[1].strip())
980985
if 'Accumulated start-stop cycles' in line:
981-
self.diags['Start_Stop_Cycles'] = line.split(':')[1].strip()
982-
if 'Start_Stop_Pct_Left' not in self.diags:
983-
self.diags['Start_Stop_Pct_Left'] = str(int(round(
984-
100 - (int(self.diags['Start_Stop_Cycles']) /
985-
int(self.diags['Start_Stop_Spec'])), 0))) + '%'
986+
self.diagnostics.Start_Stop_Cycles = int(
987+
line.split(':')[1].strip())
988+
if self.diagnostics.Start_Stop_Spec != 0:
989+
self.diagnostics.Start_Stop_Pct_Left = int(round(
990+
100 - (self.diagnostics.Start_Stop_Cycles /
991+
self.diagnostics.Start_Stop_Spec), 0))
986992
if 'Specified load-unload count' in line:
987-
self.diags['Load_Cycle_Spec'] = line.split(':')[1].strip()
988-
if self.diags['Load_Cycle_Spec'] == '0':
989-
self.diags['Load_Cycle_Pct_Left'] = '-'
993+
self.diagnostics.Load_Cycle_Spec = int(
994+
line.split(':')[1].strip())
990995
if 'Accumulated load-unload cycles' in line:
991-
self.diags['Load_Cycle_Count'] = line.split(':')[1].strip()
992-
if 'Load_Cycle_Pct_Left' not in self.diags:
993-
self.diags['Load_Cycle_Pct_Left'] = str(int(round(
994-
100 - (int(self.diags['Load_Cycle_Count']) /
995-
int(self.diags['Load_Cycle_Spec'])), 0))) + '%'
996+
self.diagnostics.Load_Cycle_Count = int(
997+
line.split(':')[1].strip())
998+
if self.diagnostics.Load_Cycle_Spec != 0:
999+
self.diagnostics.Load_Cycle_Pct_Left = int(round(
1000+
100 - (self.diagnostics.Load_Cycle_Count /
1001+
self.diagnostics.Load_Cycle_Spec), 0))
9961002
if 'Elements in grown defect list' in line:
997-
self.diags['Reallocated_Sector_Ct'] = line.split(':')[
998-
1].strip()
1003+
self.diagnostics.Reallocated_Sector_Ct = int(
1004+
line.split(':')[1].strip())
9991005
if 'read:' in line:
10001006
line_ = ' '.join(line.split()).split(' ')
10011007
if line_[1] == '0' and line_[2] == '0' and line_[3] == '0' and line_[4] == '0':
1002-
self.diags['Corrected_Reads'] = '0'
1008+
self.diagnostics.Corrected_Reads = 0
10031009
elif line_[4] == '0':
1004-
self.diags['Corrected_Reads'] = str(
1005-
int(line_[1]) + int(line_[2]) + int(line_[3]))
1010+
self.diagnostics.Corrected_Reads = int(
1011+
line_[1]) + int(line_[2]) + int(line_[3])
10061012
else:
1007-
self.diags['Corrected_Reads'] = line_[4]
1008-
self.diags['Reads_GB'] = line_[6]
1009-
self.diags['Uncorrected_Reads'] = line_[7]
1013+
self.diagnostics.Corrected_Reads = int(line_[4])
1014+
self.diagnostics.Reads_GB = float(line_[6])
1015+
self.diagnostics.Uncorrected_Reads = int(line_[7])
10101016
if 'write:' in line:
10111017
line_ = ' '.join(line.split()).split(' ')
10121018
if (line_[1] == '0' and line_[2] == '0' and
10131019
line_[3] == '0' and line_[4] == '0'):
1014-
self.diags['Corrected_Writes'] = '0'
1020+
self.diagnostics.Corrected_Writes = 0
10151021
elif line_[4] == '0':
1016-
self.diags['Corrected_Writes'] = str(
1017-
int(line_[1]) + int(line_[2]) + int(line_[3]))
1022+
self.diagnostics.Corrected_Writes = int(
1023+
line_[1]) + int(line_[2]) + int(line_[3])
10181024
else:
1019-
self.diags['Corrected_Writes'] = line_[4]
1020-
self.diags['Writes_GB'] = line_[6]
1021-
self.diags['Uncorrected_Writes'] = line_[7]
1025+
self.diagnostics.Corrected_Writes = int(line_[4])
1026+
self.diagnostics.Writes_GB = float(line_[6])
1027+
self.diagnostics.Uncorrected_Writes = int(line_[7])
10221028
if 'verify:' in line:
10231029
line_ = ' '.join(line.split()).split(' ')
10241030
if (line_[1] == '0' and line_[2] == '0' and
10251031
line_[3] == '0' and line_[4] == '0'):
1026-
self.diags['Corrected_Verifies'] = '0'
1032+
self.diagnostics.Corrected_Verifies = 0
10271033
elif line_[4] == '0':
1028-
self.diags['Corrected_Verifies'] = str(
1029-
int(line_[1]) + int(line_[2]) + int(line_[3]))
1034+
self.diagnostics.Corrected_Verifies = int(
1035+
line_[1]) + int(line_[2]) + int(line_[3])
10301036
else:
1031-
self.diags['Corrected_Verifies'] = line_[4]
1032-
self.diags['Verifies_GB'] = line_[6]
1033-
self.diags['Uncorrected_Verifies'] = line_[7]
1037+
self.diagnostics.Corrected_Verifies = int(line_[4])
1038+
self.diagnostics.Verifies_GB = float(line_[6])
1039+
self.diagnostics.Uncorrected_Verifies = int(line_[7])
10341040
if 'non-medium error count' in line:
1035-
self.diags['Non-Medium_Errors'] = line.split(':')[1].strip()
1041+
self.diagnostics.Non_Medium_Errors = int(
1042+
line.split(':')[1].strip())
10361043
if 'Accumulated power on time' in line:
1037-
self.diags['Power_On_Hours'] = line.split(':')[1].split(' ')[1]
1044+
self.diagnostics.Power_On_Hours = int(
1045+
line.split(':')[1].split(' ')[1])
10381046
if 'Current Drive Temperature' in line or ('Temperature:' in
10391047
line and interface == 'nvme'):
10401048
try:
@@ -1061,22 +1069,9 @@ class members, including the SMART attribute table and self-test log.
10611069
# corresponding warnings for non-SCSI disks
10621070
self._make_smart_warnings()
10631071
else:
1064-
# For SCSI disks, any diagnostic attribute which was not captured
1065-
# above gets set to '-' to indicate unsupported/unavailable.
1066-
for diag in ['Corrected_Reads', 'Corrected_Writes',
1067-
'Corrected_Verifies', 'Uncorrected_Reads',
1068-
'Uncorrected_Writes', 'Uncorrected_Verifies',
1069-
'Reallocated_Sector_Ct',
1070-
'Start_Stop_Spec', 'Start_Stop_Cycles',
1071-
'Load_Cycle_Spec', 'Load_Cycle_Count',
1072-
'Start_Stop_Pct_Left', 'Load_Cycle_Pct_Left',
1073-
'Power_On_Hours', 'Life_Left', 'Non-Medium_Errors',
1074-
'Reads_GB', 'Writes_GB', 'Verifies_GB']:
1075-
if diag not in self.diags:
1076-
self.diags[diag] = '-'
1077-
# If not obtained above, make a direct attempt to extract power on
1072+
# If not obtained Power_On_Hours above, make a direct attempt to extract power on
10781073
# hours from the background scan results log.
1079-
if self.diags['Power_On_Hours'] == '-':
1074+
if self.diagnostics.Power_On_Hours is None:
10801075
raw, returncode = self.smartctl.generic_call(
10811076
[
10821077
'-d',
@@ -1088,8 +1083,8 @@ class members, including the SMART attribute table and self-test log.
10881083

10891084
for line in raw:
10901085
if 'power on time' in line:
1091-
self.diags['Power_On_Hours'] = line.split(':')[
1092-
1].split(' ')[1]
1086+
self.diagnostics.Power_On_Hours = int(
1087+
line.split(':')[1].split(' ')[1])
10931088
# map temperature
10941089
if self.temperature is None:
10951090
# in this case the disk is probably ata

pySMART/diagnostics.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright (C) 2021 Rafael Leira, Naudit HPCN S.L.
2+
#
3+
# This program is free software; you can redistribute it and/or
4+
# modify it under the terms of the GNU General Public License,
5+
# version 2, as published by the Free Software Foundation.
6+
#
7+
# This program is distributed in the hope that it will be useful,
8+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
# GNU General Public License for more details.
11+
#
12+
# You should have received a copy of the GNU General Public License
13+
# along with this program; if not, write to the Free Software
14+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
15+
# MA 02110-1301, USA.
16+
#
17+
################################################################
18+
"""
19+
This module contains the definition of the `Diagnostics` class/structure, used to
20+
represent SMART SCSI attributes associated with a `Device`.
21+
"""
22+
23+
import copy
24+
import re
25+
from typing import Dict
26+
from pySMART.utils import get_object_properties
27+
28+
29+
class Diagnostics(object):
30+
"""
31+
Contains all of the information associated with every SMART SCSI/SAS attribute
32+
in a `Device`'s SMART table. This class pretends to provide a better view
33+
of the data recolected from smartctl and its types.
34+
35+
Values set to None reflects that disk does not support such info
36+
"""
37+
38+
def __init__(self):
39+
"""Initialize the structure with every field set to None
40+
"""
41+
42+
# Generic counters
43+
self.Reallocated_Sector_Ct: int = None
44+
"""**(int):** Reallocated sector count."""
45+
46+
self.Start_Stop_Spec: int = None
47+
self.Start_Stop_Cycles: int = None
48+
self.Start_Stop_Pct_Left: int = None
49+
"""**(int):** Percent left of the life-time start-stop cycles."""
50+
51+
self.Load_Cycle_Spec: int = None
52+
self.Load_Cycle_Count: int = None
53+
self.Load_Cycle_Pct_Left: int = None
54+
"""**(int):** Percent left of the life-time load cycles."""
55+
56+
self.Power_On_Hours: int = None
57+
"""**(int):** Number of hours the device have been powered on."""
58+
self.Life_Left: int = None
59+
"""**(int):** Percent left of the whole disk life."""
60+
61+
# Error counters
62+
self.Corrected_Reads: int = None
63+
"""**(float):** Total number of read operations that had an error but were corrected."""
64+
self.Corrected_Writes: int = None
65+
"""**(float):** Total number of write operations that had an error but were corrected."""
66+
self.Corrected_Verifies: int = None
67+
68+
self.Uncorrected_Reads: int = None
69+
"""**(float):** Total number of read operations that had an uncorrectable error."""
70+
self.Uncorrected_Writes: int = None
71+
"""**(float):** Total number of write operations that had an uncorrectable error."""
72+
self.Uncorrected_Verifies: int = None
73+
74+
self.Reads_GB: float = None
75+
"""**(float):** Total number of GBs readed in the disk life."""
76+
self.Writes_GB: float = None
77+
"""**(float):** Total number of GBs written in the disk life."""
78+
self.Verifies_GB: float = None
79+
80+
self.Non_Medium_Errors: int = None
81+
"""**(int):** Other errors not caused by this disk."""
82+
83+
def get_classic_format(self) -> Dict[str, str]:
84+
"""This method pretends to generate the previously/depreceted diag dictionary structure
85+
86+
Returns:
87+
Dict[str,str]: the <1.1.0 PySMART diags structure
88+
"""
89+
ret_dict = copy.deepcopy(vars(self))
90+
91+
# replace Non_Medium_Errors -> Non-Medium_Errors
92+
ret_dict['Non-Medium_Errors'] = ret_dict['Non_Medium_Errors']
93+
del ret_dict['Non_Medium_Errors']
94+
95+
# replace None with '-'
96+
for value in ret_dict:
97+
if ret_dict[value] is None:
98+
ret_dict[value] = '-'
99+
100+
# ensure everything is a string
101+
for value in ret_dict:
102+
ret_dict[value] = str(ret_dict[value])
103+
104+
# include percent %
105+
percent_values = [
106+
'Life_Left',
107+
'Start_Stop_Pct_Left',
108+
'Load_Cycle_Pct_Left'
109+
]
110+
for pv in percent_values:
111+
if ret_dict[pv] != '-':
112+
ret_dict[pv] = ret_dict[pv] + '%'
113+
114+
return ret_dict
115+
116+
def __getstate__(self, all_info=True):
117+
"""
118+
Allows us to send a pySMART diagnostics object over a serializable
119+
medium which uses json (or the likes of json) payloads
120+
"""
121+
return vars(self)
122+
123+
def __setstate__(self, state):
124+
self.__dict__.update(state)
125+
126+
127+
__all__ = ['Diagnostics']

pySMART/utils.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
by the other submodules of the `pySMART` package.
2121
"""
2222

23+
import copy
2324
import io
2425
import logging
2526
import logging.handlers
2627
import os
2728
import traceback
29+
from typing import Dict, Any
2830
from shutil import which
2931

3032
_srcfile = __file__
@@ -155,4 +157,31 @@ def smartctl_type(interface_type: str) -> str:
155157
return None
156158

157159

158-
__all__ = ['smartctl_type', 'SMARTCTL_PATH', 'all_in', 'any_in']
160+
def get_object_properties(obj: Any, deep_copy: bool = True, remove_private: bool = False) -> Dict[str, Any]:
161+
type_name = type(obj).__name__
162+
prop_names = dir(obj)
163+
164+
if deep_copy:
165+
ret = copy.deepcopy(vars(obj))
166+
else:
167+
ret = vars(obj)
168+
169+
available_types = ['dict', 'str', 'int', 'float', 'list', 'NoneType']
170+
171+
for prop_name in prop_names:
172+
prop_val = getattr(obj, prop_name)
173+
prop_val_type_name = type(prop_val).__name__
174+
175+
if (prop_name[0] != '_') and (prop_val_type_name in available_types) and (prop_name not in ret):
176+
ret[prop_name] = prop_val
177+
178+
if remove_private:
179+
for key in ret.keys():
180+
if key[0] == '_':
181+
del ret[key]
182+
183+
return ret
184+
185+
186+
__all__ = ['smartctl_type', 'SMARTCTL_PATH',
187+
'all_in', 'any_in', 'get_object_properties']

0 commit comments

Comments
 (0)